CMake Basics
Overview
This document explains the C++ build process, CMake, and colcon. It is meant as a practical and somewhat minimal guide targeted at users of ROS who have some experience with C++. For a broader overview of ROS packages/colcon see Colcon Notes
Compilation Process Overview
- The C++ compilation process follows the same model as the C compilation process
preprocess -> compile -> assemble -> link - Compilation lets you do the hard work of translating human-readable source code into machine code once upfront.
- We will use the GNU C++ compiler
g++. Another popular choice on Linux isclang++- You can install
clangon Ubuntu as well clang++generally takes the same command line arguments asg++. Compile-time errors are explained using different language than withg++so sometimes compiling with both compilers helps with understanding errors.
- You can install
- Basic command-line:
g++ -o output_file -Wall -Wext -Wpedantic -std=c++23 file1.cpp file2.cpp-Wallturns on all warnings. These help you avoid errors so always use them.-Wextturns on extra warnings. These help you avoid errors so always use them.-Wpedanticturns on warnings related to conformance with the standard. This helps ensure your code will work with multiple compilers.-std=c++23lets you use C++23 features.- You will not often invoke the compiler directly, because this quickly becomes too tedious for most projects, but it is good to know how to, particularly if you want to quickly test a small piece of C++ code.
What is CMake?
- CMake is a cross-platform scripting language primarily used to generate instructions that other build tools (such as Make, Ninja, Xcode, or Microsoft Visual Studio) can use to compile your program.
- CMake is responsible for finding all system libraries and dependencies, determining which files constitute which executables and libraries, managing compile flags, and determining which files go where upon installation.
- Although other tools exist, CMake is a popular choice for building C++ projects and is almost becoming a defacto standard.
Why do we need build tools?
- Non-trivial C++ projects require the use of multiple source files and libraries that all must be combined together
- Libraries may be located in different locations on different computers
- People may want to use different compilers with different types of compiler options
- People may want to customize your program by selecting different options at compile time
- You may want someone to be able to run their code on a different operating system (not us) or even a computer with a different system architecture system (definitely us)
- There are also a large number of options that affect the behavior of the compiler that need to be managed
- Without a build tools, you would have to manually enter the commands to compile the program
- And not just you, everyone who wants to use your code
- The ideal of a working project is to have your code be able to compile and do all setup related steps in a single command
- Computers are good at automating repetitive and tedious tasks!
Basics
File Layout
- While there is no mandated file layout, there are common best practices.
- The directory structure below provides an example for a project called
project_name. - The project creates a library called
mylibraryand an executable calledName.
project_name
├── CMakeLists.txt
├── include
│ └── mylibrary
│ └── header.hpp
├── src
│ ├── libfile1.cpp
│ ├── libfile2.cpp
│ └── Name_main.cpp
└── tests
└── mytest.cpp
- All
CMakeprojects have aCMakeLists.txtin the base directory.- This file is the primary file used to configure the build system.
CMakeLists.txtis responsible for:- Finding build dependencies of the project.
- Determining compile options.
- Determining what files must be compiled to create the libraries and executables produced by the project.
- Providing instructions for how to install the libraries and executables.
- The
include/mylibrarydirectory is there so that every project using the library can include headers using#include"mylibrary/header.hpp"- The project is configured to search for include files in
include/. - The directory structure means that the files themselves are under
mylibrary/header.hpp. - This convention avoids issues that could arise if two libraries had header files with the same name.
- The project is configured to search for include files in
- The
testsdirectory holds the unit tests - Overall, as the project becomes more complex you may add more directories under
src,include, andinclude/mylibrary- For example, we could split the locations of library
.cppfiles and executable.cppfiles
- For example, we could split the locations of library
- The directory structure is merely convention: the
CMakeLists.txtcan be written to accommodate any file layout
Basic CMakeLists.txt
Below is a template for
CMakeLists.txtas well as comments describing each aspect# Lines that begin with a # are comments # set the minimum required version of cmake, usually the first line cmake_minimum_required(VERSION 3.22) # project_name sets the name of the project and causes cmake to # find the c and c++ compilers project(project_name) # Find dependencies. # Many libraries ship with files that allow CMake to find them # Then general behavior is to call "find_package" but the options # provided are package specific. Usually there is then a CMAKE variable # That is defined to reference the library # here: we find the eigen library as per the instruction # https://eigen.tuxfamily.org/dox/TopicCMakeGuide.html find_package(Eigen3 3.3 REQUIRED NO_MODULE) # Create a library. Can specify if it is shared or static but usually # you don't need or want to. # name is the name of the library without the extension or lib prefix # name creates a cmake "target" (in this case the target is called "libname" add_library(libname src/libfile1.cpp src/libfile2.cpp) # Use target_include_directories so that #include"mylibrary/header.hpp" works # The use of the <BUILD_INTERFACE> and <INSTALL_INTERFACE> is because when # using the library from the build directory or after installation the header # files are actually in different locations. # During the build, the headers are read from the source code directory # When used from the installed location, headers are in the # system include/ directory target_include_directories(libname PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include/> $<INSTALL_INTERFACE:include/>) # specify additional compilation flags for the library # Public causes the flags to propagate to anything # that links against this library target_compile_options(libname PUBLIC -Wall -Wextra -pedantic) # Enable c++23 support. # PUBLIC causes the features to propagate to anything # that links against this library # whereas PRIVATE would make it apply only to the library itself target_compile_features(libname PUBLIC cxx_std_23) # Create an executable from the following source code files # The Name of the executable creates a cmake "target" add_executable(Name src/Name_main.cpp) # Use target_link_libraries to add dependencies to a "target" # (e.g., a library or executable) # This will automatically add all required library files # that need to be linked # and paths to th locations of header files. # Because libname has PUBLIC cxx_std_23 as a compile feature # Name will also be compiled with c++23 as the standard. target_link_libraries(Name Eigen3::Eigen libname) # install the include files by copying the whole include directory install(DIRECTORY include/mylibrary DESTINATION include) # Create a CMake Exported Target containing the lib and exe. # Also create CMake Export called project_name-targets # The CMake Export contains files that allow other CMake projects # to find this project. It must be installed separately. install(TARGETS Name libname EXPORT project_name-targets) # The project_name-targets created by install(TARGETS) needs to be installed. # install(EXPORT ...) will generate a file called project_name-config.cmake # that contains the exported targets. # After installation this file will then be found when calling # find_package(project_name) from another cmake project # A user can then target_link_libraries(target project_name::library) # to use the libraries installed here install(EXPORT project_name-targets FILE project_name-config.cmake NAMESPACE project_name:: DESTINATION lib/cmake/${PROJECT_NAME})
After writing a
CMakeLists.txtconfigure the project and build it.cmake -B build . cmake --build build
- This creates an "out-of-source" build. That is, all generated files are stored under the
build/directory
- This creates an "out-of-source" build. That is, all generated files are stored under the
The above commands are equivalent to the following:
mkdir build cd build cmake .. make
Doxygen
CMake has the ability to generate doxygen documentation automatically during the build
find_package(Doxygen) # Building documentation should be optional. # To build documentation pass -DBUILD_DOCS=ON when generating the build system option(BUILD_DOCS "Build the documentation" OFF) # build the documentation if(${DOXYGEN_FOUND} AND ${BUILD_DOCS}) # Turn the README.md into the homepage of the doxygen docs set(DOXYGEN_USE_MDFILE_AS_MAINPAGE README.md) # Tell Doxygen where to find the documentation doxygen_add_docs(doxygen include/ src/ README.md ALL) # The documentation will be in the build/html directory # The main page is build/html/index.html endif()
Unit Testing
CMake provides a framework for calling unit tests called CTest that enables coordinating tests with the build system in a testing-framework agnostic manner
include(CTest) # CTest sets BUILD_TESTING to on. To disable tests add -DBUILD_TESTING=OFF when invoking cmake if(BUILD_TESTING) # Find the Unit testing framework. In this example, Catch2 find_package(Catch2 3 REQUIRED) # A test is just an executable that is linked against the unit testing library add_executable(my_test_exe tests/mytest.cpp) target_link_libraries(my_test_exe Catch2::Catch2WithMain AnyOtherLibrariesAsNeeded) # register the test with CTest, telling it what executable to run add_test(NAME the_test_name COMMAND my_test_exe) endif()
To run the tests, enter the build/ directory and run ctest --verbose.
Advanced Installation
- If a library that you write had dependencies, sometimes the users of your library will also need these dependencies.
- In this case, you can provide a custom
project_name-config.cmakefile that automatically finds the required dependencies whenfind_packageis called. - Rather than making the exports file the
project_name-config.cmakethat is run whenfind_packageis called, We instead install the exports file asproject_name-targets.cmake. We then create and install a custom file that finds the dependencies and then includesproject_name-targets.cmake. Here is an example
CMakeLists.txtsnippet.install(DIRECTORY include/mylibrary DESTINATION include) # Create the targets install(TARGETS Name libname EXPORT project_name-targets) # Create the exports file install(EXPORT project_name-targets FILE project_name-targets.cmake NAMESPACE project_name:: DESTINATION lib/cmake/${PROJECT_NAME}) configure_file(project_name-config.cmake.in project_name-config.cmake @ONLY) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/project_name-config.cmake DESTINATION lib/cmake/${PROJECT_NAME})
The above example requires a custom
project_name-config.cmake.in:include(CMakeFindDependencyMacro) find_dependency(a_dep_your_library_needs REQUIRED) # Once all dependencies are found, include the exported targets # We expect that the project_name-targets.cmake file is installed # next to project_name-config.cmake (this file). include(${CMAKE_CURRENT_LIST_DIR}/project_name-targets.cmake)
- The
install(EXPORT)now creates a file calledproject_name-targets.cmakewhich contains theexportedtargets - There are even more advanced cases (such as when creating CMake configuration files for non-CMake projects) that can make use of the CMakePackageConfigHelpers package.
Out-Of-Source Builds
- The preferred method in CMake is to do an "out-of-source" build
- It is possible to accidentally do an "in-source" build, which will scatter generated files all over your source repository (which is a nightmare).
The following custom code prevents mistakenly doing an "in-source" build.
if(${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_BINARY_DIR}) message(FATAL_ERROR "No in source builds allowed. Create a separate build directory. SOURCE_DIR=${CMAKE_SOURCE_DIR} BINARY_DIR=${CMAKE_BINARY_DIR} ") endif()
CMake Variables
- You can set
cmakevariables when invoking cmake.- Usually you want to
cmake -DCMAKE_BUILD_TYPE=<Type>where<Type>is:Debug- compile with debugging options enabled (-gforgcc)Release- compile with optimizations enables (-O3forgcc)- There are other release types as well
- If you don't specify a build type you get no additional compile flags
I typically use the following code to have the build type default to
Debugif not otherwise specifiedif(NOT CMAKE_BUILD_TYPE) # If no build type is set, set one set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "Type of build." FORCE) endif()
- Usually you want to
- Since
cmakegenerates Makefiles, you do not typically need to reruncmake.- Even if you modify
CMakeLists.txt, the Makefiles will invoke CMake automatically
- Even if you modify
- When debugging CMake problems, it can sometimes be helpful to completely delete the contents of the
build/directory and regenerate everything from scratch
Important CMake Concepts
- generation
- Since CMake is a build-system generator (we are using
makeas the build system), it is generating build files.- You are actually using CMake Code to Generate Makefile Code to Compile C++ Code
- target
- Anything that will ultimately be created by the build system is considered a cmake target
- For example: executables and libraries
- When generating Makefiles, targets correspond to makefile targets and can be built by running
make <target_name>in thebuild/directory- For example if you
add_executable(hello myworld.cpp)thenhellois a target and it can be built specifically by usingmake hello
- For example if you
- properties
- various entities in CMake have properties that can be set
- For targets, properties are set using functions prefixed with
target_settarget_set_include_directories(targetname list_of_directories_where_include_files_are_found)target_set_compile_options()target_link_libraries(target lib1target lib2target)a bit of a misnomer (it should really be calledtarget_link_target). This function will not only link libraries lib1target and lib2target intotargetbut also add their include paths and some other options to target For example, if lib1target has include files in/home/user/include,targetnamewill be able to include those files as well.- Directory vs Local
- CMake commands starting with
add(such asadd_compile_optionsgenerally apply to all CMake files in the current directory or any sub directories. (They technically set properties on these directories) - CMake commands starting with
targetapply only to the target that is specified. They can optionally be transferred to other targets usingtarget_link_libraries Modern CMake Best practices favor using
targetlevel properties- Allows finer control
- Passing on these options to dependencies with
target_link_librariesmeans that when executable target_a depends on library_X which depends on library_Y which
depends on library_Z,
target_ais compiled with all the necessary include file paths and library paths just by doingtarget_link_libraries(target_a library_X)
- CMake commands starting with
- Directory vs Local
- For targets, properties are set using functions prefixed with
Basic CMake Syntax
${VARIABLE}gets the value of a variable. This is a direct-string replacement- In most project, you want to avoid variables in
CMakeas much as possible and instead list files directly
- In most project, you want to avoid variables in
- Functions in cmake can have a type of named argument, usually in capital letters.
So for example
message(FATAL_ERROR "The value of VARIABLE is ${VARIABLE}")will cause a fatal cmake error and print The value of VARIABLE is <variable value here>. (this is a decent cmake debugging technique. message(WARNING "The value of VARIABLE is ${VARIABLE}")~ will just print a warning
Dependencies
- If you ever get an error about
find_packagecould not findXthen this likely means you are missing a dependency- If you are lucky, that package might be available for
Ubuntuand you can install it - If using ROS 2, you can use
rosdep install --from-paths src --ignore-src -r -yto install all dependencies of all packages in the source space
- If you are lucky, that package might be available for
- You may occasionally encounter C++ packages that are not ROS packages and are not distributed with Ubuntu, meaning that you will need to compile them yourself
- Due to the popularity of CMake, there is a good chance that this project will be using CMake as it's build system
- By specifying
-DCMAKE_PREFIX_PATH=/home/user/location_where_to_installand-DCMAKE_INSTALL_PREFIX=/home/user/location_where_to_installyou are able to- Compile the package(s) you want
- Install it to the location you want (
-DCMAKE_INSTALL_PREFIXtells CMake to treat the specified path as the base location for all installations rather than the default (which is/) without needing root privileges or affecting the rest of your system - Use the installed package in other CMake packages (
-DCMAKE_PREFIX_PATHtells CMake to treat the specified directory as if it were/when looking for files)
Useful compile options
Local vs Global
In CMake you can set options for the compilation both globally and on a per-target level.
- The advantage of setting options globally is that they are used for all targets and can be changed in one place.
- The advantage of setting options locally is that you can use different options for different targets. Additionally, the options can be passed on to targets that depend on the existing target.
- There is some debate about whether setting compiler options and flags globally or locally is best. The
CMakecommunity (in my view) favors the local approach, whereas ROS favors the global approach.
A Compromise Approach
- One compromise is to create a CMake target (call it
options_target) with no source files, that just carries the compiler options.- By linking your targets to
options_targetwithtarget_link_libraries, they inherit all options provided tooptions_target - This way you have not defined anything globally (so different targets can have different options if needed), but all options commonly used are defined in the same place.
- By linking your targets to
C++ Standard
Setting the C++ standard has its own syntax in CMake. This is because every compiler has different options, but CMake is cross-platform and provides a common standard-setting method across all compilers.
- In advanced usage, CMake can compute the standard needed, given particular language features that are desired.
Locally (Per-Target)
To set the standard for a given target
# use c++23 when compiling targetName target_compile_features(targetName PUBLIC cxx_std_23) # don't use gnu extensions set_target_properties(targetName PROPERTIES CXX_EXTENSIONS OFF)
PUBLICmeans the features are used by the target and anything that depends on it- Other possibilities are
PRIVATEwhich means just use the flags directly for the target not dependencies andINTERFACEwhich means just used by dependencies but not the target itself - In other words,
PUBLIC = PRIVATE + INTERFACE
Globally (all targets)
These settings can also be done globally.
set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF)
Standards And ROS
- Technically ROS 2 only supports C++17
- However, the compiler that ships with
Ubuntu 24.04fully supportsC++23and ROS kilted is officially supported on Ubuntu 24.04 - C++ standards are very backwards compatible (as in older C++ code almost always compiles with a newer standard enabled)
- We will use C++23 in this class
- Some key features of C++20 are:
- Concepts (which will benefit you without even needing to know what they are!)
- Ranges (this standard makes working with
vectorsandalgorithmsmuch easier and more intuitive) - Modules (which is not enabled with just C++23 as it is still not fully supported or accepted)
- Some key features of C++20 are:
Compile Options
Locally (Per-Target)
- Use
target_compile_options(target -Wall -Wextra)to add flags (in this case-Wall -Wextra) to the build
Globally (all targets)
- Global compile options are added with
add_compile_options(-Wall -Wextra)(-Wall -Wextrain this case).
Ament CMake
Relationship between Ament, Colcon, CMake
ament_cmakeis ROS 2cmakecode that can be useful when building ROS 2 packages withcmakecolconis a build tool and can compile projects written with multiple build systems- So,
colconcallscmakeandament_cmakeis additionalcmakefunctions/macros that can be used in yourcmakefiles- It is possible to invoke
cmakedirectly on anament_cmake_ package rather than using =colcon
- It is possible to invoke
colconautomatically detects and compilesCMakeprojects based on the presence of aCMakeLists.txt- This feature allows using ROS 2 agnostic C++ libraries directly in the workspace without configuration
- A
package.xmlis optional when not usingamnet_cmakebut can be used to provide dependency information tocolcon, ensuring that items in the workspace are compiled in the correct order - Such a package should specify
<export><build_type>cmake</build_type></export>, rather than<export><build_type>ament_cmake</build_type></export>
- The
amentfunctions provide methods to generate custom interface types and simplify handling ofROS 2packages - They also assume a certain directory structure
ros2 pkg createcan create a template for anament_cmakepackage:mkdir -p ws/src cd ws/src ros2 pkg create --build-type ament_cmake testpkg cd .. mkdir build cd build cmake ../src/testpkg make # nothing will happen since we didn't add any files
Installation of ament_libraries
amentprovides it's own method to install librariesAfter creating the libraries call
install(TARGETS my_library EXPORT my_library-targets LIBRARY DESTINATION lib ARCHIVE DESTINATION lib RUNTIME DESTINATION bin) ament_export_targets(my_library-targets HAS_LIBRARY_TARGET) ament_export_dependencies(each dependency of my_library)
- See Ament Guide for more information
When to use ament_cmake
- It is good practice to separate general-purpose C++ code from
ROS 2related code - It makes sense for general-purpose C++ code to use plain
cmake, whereasROS 2code should useament_cmake.
ROS IDL Generation
- ROS provides custom cmake functions for generating messages, services, and actions.
- Everything related to IDL in ROS is in the rosidl repository
- In particular rosidl_cmake contains the CMake functionality (which is documented inside the source files)
- The
package.xmlmust contain<member_of_group>rosidl_interface_packages</member_of_group>
- The package is for generating code from IDL files (e.g.
.srv,.msg, and.action) files is calledrosidl_default_generators.- This package includes code for generating code from ROS IDL files in several default programming languages (e.g., Python, C++).
- After making
rosidl_default_generatorsavailable, the next step is to generate the code usingrosidl_generate_interfaces rosidl_generate_interfaces(${PROJECT_NAME} msgfiles... servicefiles...)- By default, this will create a library named
${PROJECT_NAME}, which is what the library must be named in order to work - However, it also creates a
cmaketarget named${PROJECT_NAME}, so this call will fail if you have another cmake target (such as a node) with the same name as your project - Instead, you can call this as
rosidl_generate_interfaces(<any_name_you_want_here> msgfiles... servicefiles... LIBRARY_NAME ${PROJECT_NAME})- Here, the library will still have the proper name (based on
${PROJECT_NAME}), but the name of the target can be any valid CMake target name
- Here, the library will still have the proper name (based on
- By default, this will create a library named
- The target created by
rosidl_generate_interacesis a "utility" target and cannot be used intarget_link_libraries- To get a target suitable for use with
target_link_librariescall rosidl_get_typesupport_target.cmake rosidl_get_type_support_target(<OUTVAR> <target> "string")<OUTVAR>is the name of a cmake variable for storing the target name<target>is the name of the target created byrosidl_generate_interfaces- "string" this should be
"rosidl_typesupport_cpp"to get the typesupport library for C++.
- To get a target suitable for use with
- The value stored in the
<OUTVAR>from therosidl_get_type_support_targetcall can be referenced when usingtarget_link_libraries Here is a complete example:
rosidl_generate_interfaces(myservices "srv/ThisService.srv" LIBRARY_NAME ${PROJECT_NAME}) rosidl_get_typesupport_target(cpp_typesupport_target myservices "rosidl_typesupport_cpp") ament_export_dependencies(rosidl_default_runtime) add_executable(mynode myfile.cpp) target_link_libraries(mynode ${cpp_typesupport_target})
Types of build
- With
colconyou can create three types of installation:- The default: all build files are copied to the install space and all packages have their own sub-folder
- This keeps packages isolated for easier debugging, but it also means that the PATH's needed to run your ROS program become longer (need a new PATH entry per project)
--merge-install: Everything is copied, but they are all installed to a common root- Then, only some locations in the
install spaceneed to be in your path
- Then, only some locations in the
--symlink-install: This option allows you to directly edit python files in the source space and have the changes take effect immediately in the install space without re-running colcon- For C++ you need to compile after every change you make anyway, so this option is not useful.
CMake Practices
- All projects should include, at a minimum, the following compile flags:
-WallEnable all warnings-WextraEnable extra warnings-Wpedanticconform to the C++ standard- ROS 2 automatically adds these options to the
CMakeLists.txttemplate it generates!
- When not using
colconcreate a build directory and usecmakefrom that directory (out of source build).- Don't run
cmakein a directory that contains code, it will make a mess
- Don't run
- Don't use
GLOBto find files (I didn't discuss this but you will find it on many websites).- Some examples use
file(GLOB *.cpp)to find all source files - Since this code executes only when CMake generates the build files, new source files will not be detected
- The proper way to include files for compilation is to explicitly record each file
- Some examples use
- Don't directly set
CMAKE_CXX_FLAGSorCMAKE_C_FLAGS(I didn't discuss here there is a lot information out there about these)- This can overwrite important settings set elsewhere
- It globally changes the compile flags for every project
- If you are not careful it is not platform independent
- Don't make a hierarchy of CMakeLists.txt files in each sub-directory
- This used to be best practice for CMake, but now it is better to do everything for a project in a top-level CMakeLists.txt
Build Process as Directed Acyclic Graph
- When compiling code, there are often items that depend on other items:
- For example,
main.cppmay call a function defined inlibrary.cpp - If you change
library.cpp, you need to recompilemain.cppalso- However, if
main.cppchanges,library.cppdoes not care
- However, if
- For example,
- Dependencies can be viewed as Directed Acyclic Graph (DAG)
- Each file is a node in the graph
- If B depends on A, then an edge exists from A to B
- If a node changes, follow the arrows and compile everything along all paths
- Although technically you could create circular dependencies in C++ you should avoid doing this
at all costs.
- Adds complexity and brittleness to the build process
- If A depends on B and B depends on A, then chances are A and B should actually be one unit
- Alternatively, create a new dependency C that both B and A depend on
makeis a tool that reads a description of your project and the file dependencies and compiles it- You tell it the DAG describing the dependencies and only what is necessary gets compiled
Cross Compilation
- Cross-compilation is the process of compiling code for one system on a different (and incompatible) system
- For example, compiling code meant to run on a raspberry pi on your PC
- This is hugely advantageous because compilation is significantly faster on a modern PC than on a raspberry pi (seconds versus hours).
- For example, compiling code meant to run on a raspberry pi on your PC
- To specify a cross-compiler, use
cmake -DCMAKE_TOOLCHAIN_FILE=<toolchain>, where<toolchain>is a special cmake file that is used to tellcmakeabout an alternative compiler - Since a
toolchainfile creates settings that are created really early in thecmakeinvocation, it must be set when callingcmake: it will not have the desired effect if set from within yourCMakeLists.txt - I have provided a
cmaketoolchain file to allow you to cross-compile for the raspberry pi.
ROS 1 and Catkin
- These notes are left for reference in case you encounter a ROS 1 project.
catkin_make,catkin_make_isolated, andcatkin toolsare the ROS 1 build toolscolcon, however, can also build ROS 1 packages and that is what is recommended- All ROS 1 packages, however, use
catkinCMake files which follow a specific form - One big difference between ROS 1 and ROS 2 is that
ament_cmaketries to hew much more closely to standardCMakefiles thanROS 1did.
A Deep Dive into a Catkin CMakeLists.txt
Preamble, same as a normal
# minimum cmake Version 3.9, rather outdated cmake_minimum_required(VERSION 3.9) # name of project, must match package.xml project(my_project)
- Notice that catkin is found just like any other package would be (because a main component of catkin is a cmake code)
The COMPONENTS part is because catkin sets everythign up so that ROS packages can be brought in as if they were a piece of the catkin cmake package
find_package(catkin REQUIRED COMPONENTS roscpp)Next, find other packages that are non-ros, just as you would in a regular cmake file
find_package(Boost REQUIRED COMPONENTS system)For ROS messages, services, etc to work, C++/python files encapsulating those messages must be created at compile time (that is, when you run make). These functions are provided by catkin to build the messages files into python and C++ code. This part
add_message_files() add_service_files() add_action_files() generate_messages() generate_dynamic_reconfigure_options()
catkin_package is used to let other packages know how to use your package as a dependency
## INCLUDE_DIRS: use if package contains header files ## LIBRARIES: libraries created here that dependent projects need ## CATKIN_DEPENDS: catkin_packages dependent projects need ## DEPENDS: system dependencies of this project that dependent projects also need catkin_package( # INCLUDE_DIRS include # LIBRARIES tmp # CATKIN_DEPENDS other_catkin_pkg # DEPENDS system_lib )
- The
INCLUDE_DIRSis where your package's header files are stored. This let's other catkin packages use these header files - The
LIBRARIESare for anything you created withadd_librarythat you want other catkin_packages to be able to use - The
CATKIN_DEPENDSis for any catkin package (added to thefind_package(catkin REQUIRED COMPONENTS ...)line that packages that use your project need - The
DEPENDSline is for any non-catkin package (found viafind_package) that a project that uses your project needs
- The
Rather than using
target_include_directorieson individual targets,catkinsets include directories that are used by all targets in your package globally. The${catkin_INCLUDE_DIRS}variable is populated with directories set incatkin_packagein all of the packages found byfind_package(catkin REQUIRED COMPONENTS)include_directories( include ${catkin_INCLUDE_DIRS} )- Targets (i.e., libraries and executables) are added as usual
- Target names are always prefixed by ${PROJECT_NAME} to avoid name conflicts.
${PROJECT_NAME}is a CMAKE variable that is filled in by the call toproject()at the beginning of the filecatkin_makeactually turns all the packages in your workspace into a single giant cmake package, and a single package can't have duplicate target names- If you force the users to use either
catkin_make_isolatedorcatkin_toolsthis usage is unnecessary
- In the model presented here, the library name is the same as the catkin_package name. Many times there is a 1-1 correspondance between a C++ library and a ROS package, but there need not be.
If setting flags and standards globally, you would use
target_compile_featuresandtarget_compile_optionsadd_library(${PROJECT_NAME} src/${PROJECT_NAME}/tmp.cpp) target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17) target_compile_options(${PROJECT_NAME} -Wall -Wextra)
- This line says that your library (called
${PROJECT_NAME}) depends on (i.e., will be compiled after) all the targets (i.e., libraries) that the current package exports (viacatkin_package) and targets required by anything found withfind_package(catkin REQUIRED COMPONENTS). It also ensures that any messages/services that are required are built first
add_dependencies(${PROJECT_NAME} ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
- Adding an executable (e.g., a node) is similar to adding a library, but includes a few extra lines.
- I recommend grouping these lines together for all nodes that you add
- The CMake target name is prefixed with
${PROJECT_NAME}to avoid conflicts with other packages. - The actual name of the node is changed to remove with
${PROJECT_NAME}prefix withset_target_properties - The node is made to depend on all the imported
catkinpackages and libraries so it is compiled after them. The executable is also linked against any libraries that were exported by the catkin_packages we have included, and any other non-catkin libraries we need
add_executable(${PROJECT_NAME}_node src/tmp_node.cpp) set_target_properties(${PROJECT_NAME}_node PROPERTIES OUTPUT_NAME node PREFIX "") add_dependencies(${PROJECT_NAME}_node ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS}) target_link_libraries(${PROJECT_NAME}_node ${catkin_LIBRARIES})
- Add libraries from system dependencies (i.e., those found with
find_package(Library)) totarget_link_libraries - Add local compile options and features with
target_compile_featuresandtarget_compile_options - The installation phase is usually not used during development but becomes important if, for example, you wanted to created a binary version of your package so other people could use it without needing to compile your code.
To install python scripts, cmake copies them from the source directory to an installation directory
install(PROGRAMS scripts/my_python_script DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} )
To install your nodes list them on the line below:
install(TARGETS ${PROJECT_NAME}_node RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} )
To install libraries list them below
install(TARGETS ${PROJECT_NAME} ARCHIVE DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION} LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION} RUNTIME DESTINATION ${CATKIN_GLOBAL_BIN_DESTINATION} )
- The header files for your library must also be explicitly installed.
The version in
catkin_pkgonly copies files with a.hextension. I remove this restriction as its unnecessary and I endC++headers with.hppinstall(DIRECTORY include/${PROJECT_NAME}/ DESTINATION ${CATKIN_PACKAGE_INCLUDE_DESTINATION} )
Resources
- Official Documentation
- Often the latest version has better documentation than earlier versions but is still applicable
- ROS 2 Ament CMake How To
- Introduction to Modern CMake
- https://rix0r.nl/blog/2015/08/13/cmake-guide/
- https://pabloariasal.github.io/2018/02/19/its-time-to-do-cmake-right/
- https://gist.github.com/mbinna/c61dbb39bca0e4fb7d1f73b0d66a4fd1
- https://www.slideshare.net/DanielPfeifer1/cmake-48475415
- 19 Reasons Why Cmake Is actually awesome