UP | HOME

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

  1. The C++ compilation process follows the same model as the C compilation process preprocess -> compile -> assemble -> link
  2. Compilation lets you do the hard work of translating human-readable source code into machine code once upfront.
  3. 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, although clang is closing the gap
  4. 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?

  1. 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)
  2. There are also a large number of options that affect the behavior of the compiler that need to be managed
  3. 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 called Name.
project_name
├── CMakeLists.txt
├── include
│   └── mylibrary
│       └── header.hpp
├── src
│   ├── libfile1.cpp
│   ├── libfile2.cpp
│   └── Name_main.cpp
└── tests
    └── mytest.cpp
  1. All CMake projects have a CMakeLists.txt in the base directory.
    • This file is the primary file used to setup the build system.
  2. 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.
  3. 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 under mylibrary/header.hpp
  4. The tests directory holds the unit tests
  5. Overall, as the project becomes more complex you may add more directories
    • In this example, we keep all .cpp files under src but other projects could do it differently by, for example, splitting the locations of library .cpp files and executable .cpp files

Basic CMakeLists.txt

  1. 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})
    
  2. After writing a CMakeLists.txt configure the project and build it.

    cmake -B build .
    cmake --build build
    
  3. 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 when find_package is called.
  • Rather than making the exports file the project_name-config.cmake that is run when find_package is called, We instead install the exports file as project_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 called project_name-targets.cmake which contains the exported 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 for gcc)
      • Release - compile with optimizations enables (-O3 for gcc)
      • 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 specified

      if(NOT CMAKE_BUILD_TYPE)        # If no build type is set, set one
        set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "Type of build." FORCE)
      endif()
      
  • Since cmake generates Makefiles, you do not typically need to rerun cmake.
    • 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) then hello is a target and it can be built specifically by using make hello
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 called target_link_target) this will not only link libraries lib1target and lib2target into target 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.
      1. Directory vs Local
        • CMake commands starting with add (such as add_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 using target_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 doing target_link_libraries(target_a library_X)

Basic CMake Syntax

  1. ${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
  2. 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 find X 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
  • 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 with target_link_libraries, they inherit all options provided to options_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.

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 and INTERFACE 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 supports C++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 and algorithms much easier and more intuitive)

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 systems
  • colcon can automatically detect and compile plain CMake projects based on the presence of a CMakeLists.txt
    • A package.xml is optional, but can be included to provide dependency information to colcon 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 than ament_cmake
  • An ament_cmake package is actually just a CMakeLists.txt file that happens to call ament_cmake functions
  • It is possible to invoke cmake directly on an ament_cmake_ package rather than using =colcon
  • The ament functions make it easy to generate custom interface types and also simplifies handling of ROS 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 cmake
  • After 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 called rosidl_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 using rosidl_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
  • The target created by rosidl_generate_interaces is a "utility" target and cannot be used in target_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 by rosidl_generate_interfaces
      • "string" this should be "rosidl_typesupport_cpp" to get the typesupport library for C++.
  • The value stored in the <OUTVAR> from the rosidl_get_type_support_target call can be referenced when using target_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
    • --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

  1. 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!
  2. When not using colcon create a build directory and use cmake from that directory (out of source build).
    • Don't run cmake in a directory that contains code, it will make a mess
  3. 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
  4. 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
  5. Don't directly set CMAKE_CXX_FLAGS or CMAKE_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
  6. 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

  1. When compiling code, there are often items that depend on other items:
    • For example, main.cpp may call a function defined in library.cpp
    • If you change library.cpp, you need to recompile main.cpp also
      • However, if main.cpp changes, library.cpp does not care
  2. 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
  3. 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).
  • To specify a cross-compiler, use cmake -DCMAKE_TOOLCHAIN_FILE=<toolchain>, where <toolchain> is a special cmake file that is used to tell cmake about an alternative compiler
  • Since a toolchain file creates settings that are created really early in the cmake invocation, it must be set when calling cmake: it will not have the desired effect if set from within your CMakeLists.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, and catkin tools are the ROS 1 build tools
  • colcon, 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 standard CMake files than ROS 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 with add_library that you want other catkin_packages to be able to use
    • The CATKIN_DEPENDS is for any catkin package (added to the find_package(catkin REQUIRED COMPONENTS ...) line that packages that use your project need
    • The DEPENDS line is for any non-catkin package (found via find_package) that a project that uses your project needs
  • 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 in catkin_package in all of the packages found by find_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 to project() at the beginning of the file
    • catkin_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 or catkin_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 and target_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 (via catkin_package) and targets required by anything found with find_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 with set_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)) to target_link_libraries
  • Add local compile options and features with target_compile_features and target_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 end C++ headers with .hpp

    install(DIRECTORY include/${PROJECT_NAME}/
      DESTINATION ${CATKIN_PACKAGE_INCLUDE_DESTINATION}
    )
    

Resources

Author: Matthew Elwin