Writing CML Files
Creating C++ packages can be difficult to navigate for first-time users of ROS2. This tutorial will explain the basic structure of a CMakeLists.txt file, otherwise known as a CML file. Importantly, we will cover how to properly compile and link pacakages with several files and subdirectories found in the include/ and src/ directories of the ROS2 package.
First, we want to guarantee compatibilty by enforcing a lower bound on the CMake version. For example, RoboFlock uses CMake version 3.8, so the first line of the CML file should be:
1cmake_minimum_required(VERSION 3.8)
In the context of ROS2, the project name should be the extactly the same as the package name created in the ROS2 environment. Let’s call our project hello_world_pkg and assume it has the following directory structure:
.
├── build
├── install
├── log
└── src
└── hello_world_pkg
├── CMakeLists.txt
├── include
│ └── hello_world_pkg
│ ├── greetings
│ │ ├── hello_source1.hpp
│ │ └── hello_source2.hpp
│ └── salutations
│ ├── goodbye_source1.hpp
│ └── goodbye_source2.hpp
├── LICENSE
├── package.xml
└── src
├── greetings
│ ├── hello_source1.cpp
│ └── hello_source2.cpp
├── hello_world_node.cpp
└── salutations
├── goodbye_source1.cpp
└── goodbye_source2.cpp
We can use the project() command to give our project a name and to set environmental variables for the absolute paths to the source and binary directories.
2project(hello_world_pkg)
The next few lines set parameters for the compiler, such as the C++ standard being used and various compiler flags.
3if(NOT CMAKE_CXX_STANDARD)
4 set(CMAKE_CXX_STANDARD 14)
5endif()
6
7if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
8 add_compile_options(-Wall -Wextra -Wpedantic)
9endif()
ROS2 allows for a complex network of packages and nodes to interact with one another, and as such, we need to tell the build process what packages have dependencies. In our project, we will use three built-in ROS2 packages (ament_cmake, rclcpp, and std_msgs) that are needed for most of RoboFlock’s nodes.
10# Creates variables like rclcpp_LIBRARIES and and std_msgs_INCLUDE_DIRS
11#
12find_package(ament_cmake REQUIRED) # ROS2 build system
13find_package(rclcpp REQUIRED) # ROS2 C++ client library
14find_package(std_msgs REQUIRED) # ROS2 standard msg types
Now that we have our compatibility requirements, we need to make sure that the compiler can find all our files, properly link libraries, etc. First, let’s tell the compiler where to search for included files:
15# Adds include/ directory to compiler's search path for ALL targets
16# This is what allows for the preprocessor directive "#include" to work on our files
17#
18include_directories(
19 include/${PROJECT_NAME}/
20)
Next, we’ll handle our source files. We can organize all the source files into a single variable so that we don’t have to repeat commands and file names for every single source file. For extra precaution, we’ll ensure that these files exist. The code in our CML file should look like this:
21# Creates the variable LIB_HEADER_FILES used to organize header files for the library
22#
23set (LIB_HEADER_FILES
24 include/${PROJECT_NAME}/greetings/hello_source1.hpp
25 include/${PROJECT_NAME}/greetings/hello_source2.hpp
26 include/${PROJECT_NAME}/salutations/goodbye_source1.hpp
27 include/${PROJECT_NAME}/salutations/goodbye_source2.hpp
28
29)
30
31# Loop through each file in LIB_HEADER_FILES to make sure that they exist
32#
33foreach(hdr_file ${LIB_HEADER_FILES})
34 if(NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${hdr_file})
35 message(WARNING "Header file not found: ${hdr_file}")
36 else()
37 message(STATUS "Found header file: ${hdr_file}")
38 endif()
39endforeach()
40
41# Creates the variable LIB_SOURCE_FILES used to organize source files for the library
42#
43set(LIB_SOURCE_FILES
44 src/greetings/hello_source1.cpp
45 src/greetings/hello_source2.cpp
46 src/salutations/goodbye_source1.cpp
47 src/salutations/goodbye_source2.cpp
48)
49
50# Loop through each file in LIB_SOURCE_FILES to make sure that they exist
51#
52foreach(src_file ${LIB_SOURCE_FILES})
53 if(NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${src_file})
54 message(WARNING "Source file not found: ${src_file}")
55 else()
56 message(STATUS "Found source file: ${src_file}")
57 endif()
58endforeach()
Libraries are collections of pre-written code that help with code reusability and scalability. There are a few different types of libraries that we can make:
Static libraries contain compiled code of all the files and are linked directly to an executable at compile time.
Shared (Dynamic) libraries contain compiled code only for required files and are loaded at runtime
If this package is intended to be used by downstream packages, we can create our own library. To add a library, there are configuration rules that need to be defined so that it uses the correct source code and that everything is properly linked together. The heavy-lifters here will be the target_include_directories() and target_link_libraries() commands:
56# Create the library target
57#
58add_library(${PROJECT_NAME}_lib ${LIB_SOURCE_FILES} ${LIB_HEADER_FILES})
59
60# Add the include path to the library target
61# PUBLIC indicates that both the library target and anything linking to it gets these includes
62#
63target_include_directories(${PROJECT_NAME}_lib PUBLIC
64 "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>"
65 "$<INSTALL_INTERFACE:include/${PROJECT_NAME}>"
66)
67
68# Tell the linker which specific libraries to use when linking the target and its dependents
69# Usage requirements are propagated
70#
71target_link_libraries(${PROJECT_NAME}_lib PUBLIC
72 ${rclcpp_LIBRARIES}
73 ${std_msgs_LIBRARIES}
74)
75
76# Specify that our library's include directory is installed into the workspace's include directory
77#
78install(DIRECTORY include/${PROJECT_NAME}
79 DESTINATION include
80)
81
82# Specify that our library will be installed as a shared library, a static library, and as a binary
83# Also, export the library so that it can be used downstream and provide correct environment variables
84install(TARGETS ${PROJECT_NAME}_lib
85 EXPORT ${PROJECT_NAME}_lib_targets
86 LIBRARY DESTINATION lib # shared library (.so)
87 ARCHIVE DESTINATION lib # static library (.a)
88 RUNTIME DESTINATION bin # executable
89 INCLUDES DESTINATION include/${PROJECT_NAME}
90)
Next, we’ll use ament_cmake, which is the build system for C++ packages in ROS2, to make our package discoverable to other ROS2 packages:
91# Export the corresponding objects for CMake, allowing the library's clients to use the
92# `target_link_libraries(client hello_world_pkg_lib::hello_world_pkg_lib)` syntax
93#
94ament_export_include_directories(include)
95ament_export_libraries(${PROJECT_NAME}_lib)
96ament_export_targets(${PROJECT_NAME}_lib_targets HAS_LIBRARY_TARGET)
97ament_export_dependencies(rclcpp std_msgs)
In order for us to use our ROS2 node, we’re going to need an executable. This is the first step to getting the command ros2 run [package name] [node name] to work. The executable’s name should be the same as the node’s name. Then we’ll set the visibility, configuration, and installation rules.
98# Add an executable target to be built from our node's source code
99#
100add_executable(hello_world_node src/hello_world_node.cpp)
101
102# Tell the compiler what include directories we want to use
103# PRIVATE indicates that our include/ directory is only available to our source code and is not propagated to other targets
104#
105target_include_directories(hello_world_node PRIVATE
106 "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>"
107 "$<INSTALL_INTERFACE:include/${PROJECT_NAME}>"
108)
109
110# Tell the linker which specific libraries to use when linking the executable target
111#
112target_link_libraries(hello_world_node
113 ${PROJECT_NAME}_lib
114 rclcpp::rclcpp
115 ${std_msgs_TARGETS}
116)
117
118# Specify that our node's executable will be installed into lib/hello_world_pkg
119#
120install(TARGETS hello_world_node
121 DESTINATION lib/${PROJECT_NAME}
122)
This last line is essential to the entire CML file because it generates all of the configuration files and setup scripts. It also registers our package in the ROS2 workspace, allowing us to run the node.
123# Last line of the CML file
124ament_package()
Now you should be able to write, build, and run your own C++ packages in ROS2 without CMake semantics slowing you down! Simply substitute your directory and file names for the names used in this example.