Build Tool
A build tool operates on a set of packages
- determines the dependency graph
- invokes the specific build system for each package in topological order.
- for a specific package, knows how to setup the environment for it, invokes the build, and sets up the environment to use the built package.
The build system operates on a single package: CMake
, Make
, Python setuptools
. catkin
and ament_cmake
are based on CMake
Dependency Graph
find_package
helps the graph.FindFoo.cmake
orFindFoo.cmake
for the dependency must be in a prefix that CMake searches implicitly, like/usr
, or a location provided through env varsCMAKE_PREFIX_PATH
, orCMAKE_MODULE_PATHCMAKE_MODULE_PATH
- Install a shared_lib in a non-default location, that location needs to be in
LD_LIBRARY_PATH
.
ROS 2 Cpp And Python Package
-
We’ll create a ROS2 Cpp package, which contains a package.xml and CMakeLists.txt.
1 2
cd ~/ros2_ws/src/ ros2 pkg create my_cpp_py_pkg --build-type ament_cmake
-
See:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
my_cpp_py_pkg/ # --> package info, configuration, and compilation ├── CMakeLists.txt ├── package.xml # --> Python stuff ├── my_cpp_py_pkg │ ├── __init__.py │ └── module_to_import.py ├── scripts │ └── py_node.py # --> Cpp stuff ├── include │ └── my_cpp_py_pkg │ └── cpp_header.hpp └── src └── cpp_node.cpp
-
- For Python, no more setup.py and setup.cfg, everything will be done in the CMakeLists.txt.
- Note that we have a sub directory called
"my_cpp_py_pkg"
. Inside, the python code lives there. Like ROS1, they are executableschmod +x
. - The module is visible to other packages:
from my_cpp_py_pkg.module_to_import import ...
- Note that we have a sub directory called
-
package.xml
:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
<?xml version="1.0"?> <?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?> <package format="3"> <name>my_cpp_py_pkg</name> <version>0.0.0</version> <description>TODO: Package description</description> <maintainer email="your@email.com">Name</maintainer> <license>TODO: License declaration</license> <buildtool_depend>ament_cmake</buildtool_depend> <buildtool_depend>ament_cmake_python</buildtool_depend> <depend>rclcpp</depend> <depend>rclpy</depend> <test_depend>ament_lint_auto</test_depend> <test_depend>ament_lint_common</test_depend> <export> <build_type>ament_cmake</build_type> </export> </package>
-
CMakeLists.txt
:cmake_minimum_required(VERSION 3.5) project(my_cpp_py_pkg) # Default to C++14 if(NOT CMAKE_CXX_STANDARD) set(CMAKE_CXX_STANDARD 14) endif() if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") add_compile_options(-Wall -Wextra -Wpedantic) endif() # Find dependencies find_package(ament_cmake REQUIRED) find_package(ament_cmake_python REQUIRED) find_package(rclcpp REQUIRED) find_package(rclpy REQUIRED) # Include Cpp "include" directory include_directories(include) # Create Cpp executable add_executable(cpp_executable src/cpp_node.cpp) ament_target_dependencies(cpp_executable rclcpp) # Install Cpp executables install(TARGETS cpp_executable DESTINATION lib/${PROJECT_NAME} ) # Install Python modules ament_python_install_package(${PROJECT_NAME}) # Install Python executables install(PROGRAMS scripts/py_node.py DESTINATION lib/${PROJECT_NAME} ) ament_package()
Packaging & Build System
- In ROS2, cpp files require
CMakeLists.txt
, python files requiresetup.cfg
andsetup.py
:
1
2
3
4
5
6
7
8
9
10
11
12
cpp_package_1/
CMakeLists.txt
include/cpp_package_1/
package.xml
src/
py_package_1/
package.xml
resource/py_package_1
setup.cfg
setup.py
py_package_1/
- With Colcon, I like:
1
colcon build -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_EXPORT_COMPILE_COMMANDS=1 --packages-select dummy_test --cmake-force-configure
-DCMAKE_BUILD_TYPE=RelWithDebInfo
: This sets the CMake variable CMAKE_BUILD_TYPE to RelWithDebInfo, meaning “Release with Debug Info.”. Despite the existence of the debugging symbols, below can still happen with optimization:- Lines can be merged or removed
- Variables can vanish
- Stepping can feel “jumpy”
- use a pure Debug build (no optimization) or something like -Og (for GCC) or -O1 -g (for Clang) for real debug build
-DCMAKE_EXPORT_COMPILE_COMMANDS=1
: Tells CMake to generate a compile_commands.json file in your build directory. This JSON file lists all compiler invocations for your project, which is extremely useful for tools like clangd, code analyzers, and IDEs that need to know your include paths and compiler flags.--cmake-force-configure
This is not a standard CMake flag; it’s a colcon (ROS 2 build tool) argument. It forces CMake to re-run its configuration step for all packages, even if CMake thinks nothing has changed.
rosdep
rosdep
will:
- check for
package.xml
files in its path or for a specific package and find the rosdep keys stored within.- The dependencies in the package.xml file are generally referred to as “rosdep keys”.
- Build tags
<depend>
are dependencies that should be provided at both build time and run time for your package.- For C++ packages, if in doubt, use this tag.
- Pure Python packages generally don’t have a build phase, so should never use this and should use
<exec_depend>
instead.
<exec_depend>
declares dependencies for shared libraries, executables, Python modules, launch scripts and other files required when running your package.
- Query keys in a central index to find the appropriate ROS packages
- Retrieving the central index on to your local machine (
/etc/ros/rosdep/sources.list.d/20-default.list
) so that it doesn’t have to access the network every time it runs
- Retrieving the central index on to your local machine (
- Install the ROS packages
Finding A Package Depedency and Version
If I have installed behaviortree_cpp
in the /opt
space, and I want to inspect its version,
dpkg -l | grep behaviortree
1
2
ii ros-humble-behaviortree-cpp 4.7.1-1jammy.20250513.175053 amd64 This package provides the Behavior Trees core library.
ii ros-humble-behaviortree-cpp-v3 3.8.7-1jammy.20250429.201614 amd64 This package provides the Behavior Trees core library.
ros2 pkg behaviortree_cpp
: the path to ros-installed binary
1
2
3
4
5
6
7
└─ $ ros2 pkg prefix behaviortree_cpp
/root/my_ws/install/behaviortree_cpp
cd /root/my_ws/install/behaviortree_cpp/lib
dpkg -l | grep behaviortree_cpp
ii ros-humble-behaviortree-cpp 4.7.1-1jammy.20250513.175053 amd64 This package provides the Behavior Trees core library.
ii ros-humble-behaviortree-cpp-v3 3.8.7-1jammy.20250429.201614 amd64 This package provides the Behavior Trees core library.
- The binary is in the
/lib
directory - To see
package.xml
, etc, go tocd /root/my_ws/install/behaviortree_cpp/share
. This can be checked usingros2 pkg prefix behaviortree_cpp --share
C++ & Python Interface Packages
An interface package defines ROS2 messages, services, and common utilities for other ROS2 packages. To create Python utilities, once a Python package is built and installed, and the workspace is sourced, its Python modules in install/MY_INTERFACE/local/lib/python3.10/dist-packages/MY_INTERFACE
are automatically added to the Python path. What we need are as follow:
- Make sure the python package files are here:
- Empty
MY_INTERFACE/MY_INTERFACE/__init__.py
- Module file:
MY_INTERFACE/MY_INTERFACE/My_Module
- Empty
-
Add
MY_INTERFACE/setup.py
:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
from setuptools import setup, find_packages package_name = "MY_PACKAGE" setup( name=package_name, version="0.0.1", packages=find_packages(include=[package_name, f"{package_name}.*"]), install_requires=["setuptools"], zip_safe=True, maintainer="TODO", maintainer_email="your_email@example.com", description="TODO", license="Apache-2.0", tests_require=["pytest"], entry_points={ "console_scripts": [], }, )
-
CMakeLists.txt
1 2 3 4 5 6
install( DIRECTORY mumble_interfaces/ DESTINATION local/lib/python3.10/dist-packages/mumble_interfaces FILES_MATCHING PATTERN "*.py") ament_package()
- The destination
install/MY_INTERFACE/local/lib/python3.10/dist-packages/MY_INTERFACE
is carefully chosen, because that’s where generated srv, msg files go. Why? Because when there are packages with the same name at two different locations, Python will look into one, and throw afile-not-found
error if files are not there. - We are NOT using
ament_python_install_package
because it’s meant for pure Python packages. We need to manually installMY_INTERFACE
ininstall/MY_INTERFACE/lib/python3.10/site-packages
- The destination
- User Code:
1
from MY_INTERFACE import MyFunc
1
2
- Or one can use: `python3 -c import MY_INTERFACE.My_Module` in the same console, because after sourcing `install/setup.bash`, the installed file is added to the Python Path.
- One can check the python path with: `python3 -c "import sys; print(sys.path)"` or `echo $PYTHONPATH`
colcon build
--symlink-install
- Without
--symlink-install
: copies artifacts intoinstall/
. - With the flag: replaces many of those copies with symlinks for quicker iteration.
Mode | What goes into install/… |
Practical consequences | |
---|---|---|---|
Default (no flag) | A copy of every file produced by each package’s normal install step (executables, .so libraries, Python packages, resources…). |
Safe and self-contained, but every rebuild recopies files; editing a Python script in src/ has no effect until you rebuild. |
|
--symlink-install |
Wherever possible, symbolic links that point back into the build or source trees instead of copies. | Much faster iteration during development: change a Python file, re-source `setup.[bash | zsh], and run again—no rebuild needed. Not all artifacts can be symlinked (e.g. versioned .so` chains that CMake creates are still copied). |
More specifically, without --symlink-install
, these files are copied:
- Build tree of each package, e.g.
${WS}/build/my_pkg/lib/libmy.so ${WS}/install/my_pkg/lib/libmy.so
- Compiled binaries and shared objects produced by CMake/ament - Source tree for Python packages, e.g.
${WS}/src/my_py_pkg/my_py_pkg/*.py
(aftersetup.py
install runs inside the build step)${WS}/install/my_py_pkg/lib/python3.x/site-packages/my_py_pkg/*.py
Python modules, entry-point scripts - Resource files declared with install(DIRECTORY …) or ament_index_register_resource, e.g., Launch files, URDFs, icons, etc.
Note, symlinks are NOT what ldd
gives - **ldd resolves all shared library dependencies of an executable, while a symlink is a separate inode whose data is the path to a another inode**
When to Rebuild?
Scenario 1: Sometimes, My ros2 binary didnt seem updated: when I updated a file in behavior_executor
- Solution1:
rm -rf build/<BINARY> install/<BINARY>
. Afterwards, –cmake-clean-cache starts working, without it is also fine- One caveat is, I could build in
~/MY_WS/src
(not in~/MY_WS
). This means you might see a successful build, without actually building in the right place
- One caveat is, I could build in
- Solution2:
--cmake-clean-first
- This flag tells colcon to run the CMake “clean” target before building each CMake‐based package. In practice:
- Without
--cmake-clean-first
, colcon will do an incremental build: it only rebuilds targets whose inputs (source, CMakeLists, headers) have changed, and it won’t clear out old artifacts.
- Without
- However, note: It doesn’t purge the CMake cache. If you need CMake to re-run the configure step (e.g. you changed
find_package
flags ortoolchain settings
), you’ll also want:--cmake-force-configure
- This flag tells colcon to run the CMake “clean” target before building each CMake‐based package. In practice:
Scenario 2: How to build a package from source:
- Clone it
- Build the package ONLY with
--symlink-install
-
Optional: Can mv the package binary you are trying to replace. E.g,
1
sudo mv /opt/ros/humble/lib/libbehaviortree_cpp.so
- Source the setup.bash, then run it/
colcon build --packages-select capacity_manager behavior_executor behaviortree_cpp --cmake-clean-first --allow-overriding behaviortree_cpp