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 gcc. Another popular choice on Linux is clang
- You can install
clang
on Ubuntu as well - clang generally takes the same command line arguments as gcc. Compile-time errors are explained using different language than with gcc so sometimes compiling with both compilers can help you track down an error more easily.
- The output of
gcc
is generally considered to be faster, althoughclang
is closing the gap
- You can install
- Basic command-line:
g++ -o output_file -Wall -Wext -Wpedantic -std=c++17 file1.cpp file2.cpp
-Wall
turns on all warnings. These help you avoid errors so always use them.-Wext
turns on extra warnings. These help you avoid errors so always use them.-Wpedantic
turns on warnings related to conformance with the standard. This helps ensure your code will work with multiple compilers.-std=c++17
lets you use C++17 features, which are great.
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 people's 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
4.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 below structure is an example for a project called
project_name
. - The project creates a library called
mylibrary
and an executable calledName
.
project_name ├── CMakeLists.txt ├── include │ └── mylibrary │ └── header.hpp ├── src │ ├── libfile1.cpp │ ├── libfile2.cpp │ └── Name_main.cpp └── tests └── mytest.cpp
- All
CMake
projects have aCMakeLists.txt
in the base directory.- This file is the primary file used to setup the build system.
CMakeLists.txt
is 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/mylibrary
directory is there so that every project using the library can include headers by doing#include"mylibrary/header.hpp"
- The project is setup to search for include files in
include/
. The directory structure means that the files themselves are undermylibrary/header.hpp
- The project is setup to search for include files in
- The
tests
directory holds the unit tests - Overall, as the project becomes more complex you may add more directories
- In this example, we keep all
.cpp
files undersrc
but other projects could do it differently by, for example, splitting the locations of library.cpp
files and executable.cpp
files
- In this example, we keep all
Basic CMakeLists.txt
Below is a template for
CMakeLists.txt
as 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 your 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" 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 # During 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++17 support. # Public causes the features to propagate to anything # that links against this library target_compile_features(libname PUBLIC cxx_std_17) # 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 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 projet_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.txt
configure the project and build it.cmake -B build . cmake --build build
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 just because Doxygen is missing 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.cmake
file that automatically finds the required dependencies whenfind_package
is called. - Rather than making the exports file the
project_name-config.cmake
that is run whenfind_package
is 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 includes the exports file. Here is an example
CMakeLists.txt
snippet.install(DIRECTORY include/mylibrary DESTINATION include) install(TARGETS Name libname EXPORT project_name-targets) 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
install(EXPORT)
now creates a file calledproject_name-targets.cmake
which contains theexported
targets You now provide a file called
project_name-config.cmake.in
with the following contents# You can access variables from CMake by using the # @var_name@ syntax. These variables will be replaced with there value # from the current CMake session (useful for configuration) 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)
Out-Of-Source Builds
- Notice that we created a build directory outside our source directory
- This is known as an out-of-source build.
- It is recommended to do out-of-source builds to avoid scattering
a bunch of files generated by CMake everywhere in your source tree
(a
.gitignore
nightmare!) - It is possible to invoke cmake in the same directory as the source code, which makes a mess
I use the following code to prevent me from accidently doing that
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
cmake
variables when invoking cmake.- Usually you want to
cmake -DCMAKE_BUILD_TYPE=<Type>
where<Type>
is:Debug
- compile with debugging options enabled (-g
forgcc
)Release
- compile with optimizations enables (-O3
forgcc
)- 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
Debug
if 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
cmake
generates Makefiles, you do not typically need to reruncmake
.- Even if you modify CMakeLists.txt, the Makefiles will invoke CMake automatically
- 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
make
as the build system), it is generating build files.- This means that you are actually using CMake Code to Generate Makefile Code to Compile C++ Code
- target
- This is anything that will ultimately be created by the build system
- For example: executables and libraries
- When generating Makefiles, targets correspond to makefile targets
- For example if you
add_executable(hello myworld.cpp)
thenhello
is 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, you can set properties using functions that begin with
target_set
target_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 will not only link libraries lib1target and lib2target intotarget
but also add their include paths and some other options to target For example, if lib1target has include files in/home/user/include
,targetname
will be able to include those files as well.- Directory vs Local
- CMake commands starting with
add
(such asadd_compile_options
generally apply to all CMake files in the current directory or any sub directories. (They technically set properties on these directories) - CMake commands starting with
target
apply only to the target that is specified. They can optionally be transferred to other targets usingtarget_link_libraries
Modern CMake Best practices favor using
target
level properties- Allows finer control
- Passing on these options to dependencies with
target_link_libraries
means that when executable target_a depends on library_X which depends on library_Y which
depends on library_Z,
target_a
is 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, you can set properties using functions that begin 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
CMake
as 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_package
could not findX
then this likely means you are missing a dependency- If you are lucky, that package might be available for
Ubuntu
and you can install it - If using ROS2, you can use
rosdep install --from-paths src --ignore-src -r -y
to 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_install
and-DCMAKE_INSTALL_PREFIX=/home/user/location_where_to_install
you are able to- Compile the package(s) you want
- Install it to the location you want (
-DCMAKE_INSTALL_PREFIX
tells 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_PATH
tells 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
CMake
community (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_target
withtarget_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++17 when compiling targetName target_compile_features(targetName PUBLIC cxx_std_17) # don't use gnu extensions set_target_properties(targetName PROPERTIES CXX_EXTENSIONS OFF)
PUBLIC
means the features are used by the target and anything that depends on it- Other possibilities are
PRIVATE
which means just use the flags directly for the target not dependencies andINTERFACE
which 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 17) 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 22.04
mostly supportsC++20
fully and ROS humble is officially supported on Ubuntu 22.04 - C++ standards are very backwards compatible (as in older C++ code will usually compile with a newer standard enabled)
- We will use C++17 in this class
- C++20 came out in 2020, and you may experiment with it if you would like to.
- Some key features of C++20 are:
- Concepts (which will benefit you without even needing to know what they are!)
- Modules (which is not enabled with just C++20 as it is still not fully implemented/tested in
gcc
) - Ranges (this standard makes working with
vectors
andalgorithms
much easier and more intuitive)
- 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 -Wextra
in this case).
Relationship with ament and colcon
colcon
is a build tool and can compile projects written with multiple build systemscolcon
can automatically detect and compile plain CMake projects based on the presence of aCMakeLists.txt
- A
package.xml
is optional, but can be included to provide dependency information tocolcon
to ensure the packages in the workspace are compiled in the correct order - such a package should specify
cmake
as the<export><build_type>
, rather thanament_cmake
- A
- An ament_cmake package is actually just a
CMakeLists.txt
file that happens to callament_cmake
functions - It is possible to invoke
cmake
directly on anament_cmake_ package rather than using =colcon
The
ament
functions make it easy to generate custom interface types and also simplifies handling ofROS 2
packages- Lets try it!
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
Ament provides convenience functions for library installation:
Installation of ament_libraries
ament
makes it easier to install libraries than standard cmakeAfter creating the libraries call
ament_export_targets(my_library-targets HAS_LIBRARY_TARGET) ament_export_dependencies(dependency_of_library) install(DIRECTORY include/ DESTINATION include) install(TARGETS my_library EXPORT my_library-targets)
- See Ament Guide for more information
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.xml
must 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_generators
available, 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
cmake
target 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_interaces
is a "utility" target and cannot be used intarget_link_libraries
- To get a target suitable for use with
target_link_libraries
call 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_target
call 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
colcon
you 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 space
need 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:
-Wall
Enable all warnings-Wextra
Enable extra warnings-Wpedantic
conform to the C++ standard- ROS 2 automatically adds these options to the
CMakeLists.txt
template it generates!
- When not using
colcon
create a build directory and usecmake
from that directory (out of source build).- Don't run
cmake
in a directory that contains code, it will make a mess
- Don't run
To set C++ version options globally
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF)
- Setting using
set(CMAKE_CXX_STANDARD 17)
is still globally applied but it also is compiler independent
- Setting using
- Don't use
GLOB
to 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_FLAGS
orCMAKE_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.cpp
may call a function defined inlibrary.cpp
- If you change
library.cpp
, you need to recompilemain.cpp
also- However, if
main.cpp
changes,library.cpp
does 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
make
is 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 tellcmake
about an alternative compiler - Since a
toolchain
file creates settings that are created really early in thecmake
invocation, it must be set when callingcmake
: it will not have the desired effect if set from within yourCMakeLists.txt
- I have provided a
cmake
toolchain 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 tools
are the ROS 1 build toolscolcon
, however, can also build ROS 1 packages and that is what is recommended- All ROS 1 packages, however, use
catkin
CMake files which follow a specific form - One big difference between ROS 1 and ROS 2 is that
ament_cmake
tries to hew much more closely to standardCMake
files thanROS 1
did.
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_DIRS
is where your package's header files are stored. This let's other catkin packages use these header files - The
LIBRARIES
are for anything you created withadd_library
that you want other catkin_packages to be able to use - The
CATKIN_DEPENDS
is for any catkin package (added to thefind_package(catkin REQUIRED COMPONENTS ...)
line that packages that use your project need - The
DEPENDS
line is for any non-catkin package (found viafind_package
) that a project that uses your project needs
- The
Rather than using
target_include_directories
on individual targets,catkin
sets include directories that are used by all targets in your package globally. The${catkin_INCLUDE_DIRS}
variable is populated with directories set incatkin_package
in 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_make
actually 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_isolated
orcatkin_tools
this 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_features
andtarget_compile_options
add_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
catkin
packages 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_features
andtarget_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_pkg
only copies files with a.h
extension. I remove this restriction as its unnecessary and I endC++
headers with.hpp
install(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
- 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