ament_cmake Guide 2026
Master CMakeLists.txt for ROS 2: package structure, dependency declarations, shared libraries, rosidl_generate_interfaces for custom messages, GTest integration, and overlay workspaces.
ament_cmake vs plain CMake
ament_cmake is a thin wrapper around CMake that adds the ament index (package discovery), export infrastructure (ament_export_targets, ament_export_dependencies), and ROS 2-specific helpers like ament_target_dependencies. You still write standard CMake — ament just handles the ROS 2-specific plumbing.
ament_cmake Package Structure
An ament_cmake package is the standard for C++ ROS 2 packages. It has package.xml declaring dependencies and CMakeLists.txt describing how to build.
my_robot_pkg/
├── package.xml # ROS 2 metadata + dependencies
├── CMakeLists.txt # Build instructions
├── include/
│ └── my_robot_pkg/
│ └── robot_utils.hpp # public headers
├── src/
│ ├── robot_node.cpp # executable source
│ └── robot_utils.cpp # library source
├── srv/
│ └── FindObject.srv # custom service definition
├── msg/
│ └── RobotStatus.msg # custom message definition
└── launch/
└── robot.launch.py⚡ Use ament_cmake_python (not ament_cmake) when the package contains Python nodes. Hybrid C++/Python packages use ament_cmake + ament_cmake_python.
package.xml — Declaring Dependencies
package.xml tells colcon and rosdep what your package needs. Use the right dependency tag: build, exec, or test. Format 3 is current.
<?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_robot_pkg</name>
<version>0.1.0</version>
<description>Example ament_cmake ROS 2 package</description>
<maintainer email="dev@example.com">Dev</maintainer>
<license>Apache-2.0</license>
<!-- ament_cmake is the build system itself -->
<buildtool_depend>ament_cmake</buildtool_depend>
<!-- Packages needed to COMPILE your code -->
<build_depend>rclcpp</build_depend>
<build_depend>std_msgs</build_depend>
<build_depend>geometry_msgs</build_depend>
<!-- Packages needed at RUNTIME (but not to compile) -->
<exec_depend>rclcpp</exec_depend>
<exec_depend>std_msgs</exec_depend>
<!-- Shorthand for both build + exec: -->
<!-- <depend>rclcpp</depend> -->
<!-- Packages for tests only -->
<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>
<test_depend>ament_cmake_gtest</test_depend>
<!-- For custom messages / services -->
<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
<export>
<build_type>ament_cmake</build_type>
</export>
</package>⚡ <depend> = <build_depend> + <exec_depend>. Use it for packages needed both to compile and run. Prefer explicit tags for packages only needed at build time (code generation).
Minimal CMakeLists.txt
The minimum to build a C++ executable: cmake_minimum_required, project, find_package, add_executable, ament_target_dependencies, install, ament_package.
cmake_minimum_required(VERSION 3.8)
project(my_robot_pkg)
# Treat warnings as errors (good practice)
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
# ─── Find packages ────────────────────────────────────────
find_package(ament_cmake REQUIRED) # ament build system — always first
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
find_package(geometry_msgs REQUIRED)
# ─── Executable ───────────────────────────────────────────
add_executable(robot_node
src/robot_node.cpp
src/robot_utils.cpp # add every .cpp that isn't a separate library
)
# Include your own headers AND ament package headers
target_include_directories(robot_node PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
# ament_target_dependencies links and adds include paths in one call
ament_target_dependencies(robot_node
rclcpp
std_msgs
geometry_msgs
)
# Set C++ standard
target_compile_features(robot_node PUBLIC cxx_std_17)
# ─── Install ──────────────────────────────────────────────
install(TARGETS robot_node
DESTINATION lib/${PROJECT_NAME} # required location for ros2 run
)
install(DIRECTORY include/
DESTINATION include # expose public headers
)
install(DIRECTORY launch/
DESTINATION share/${PROJECT_NAME}/launch
)
# ─── ALWAYS last ──────────────────────────────────────────
ament_package()⚡ ament_package() must be the last call in CMakeLists.txt. It registers the package in the ament index and exports CMake config files.
Building a Shared Library
For reusable code that other packages link against: add_library instead of add_executable, then export targets with ament_export_targets.
# ─── Library target ───────────────────────────────────────
add_library(robot_utils SHARED
src/robot_utils.cpp
)
target_include_directories(robot_utils PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
ament_target_dependencies(robot_utils
rclcpp
geometry_msgs
)
target_compile_features(robot_utils PUBLIC cxx_std_17)
# ─── Executable that links the library ────────────────────
add_executable(robot_node src/robot_node.cpp)
ament_target_dependencies(robot_node rclcpp std_msgs)
target_link_libraries(robot_node robot_utils) # link our library
# ─── Install ──────────────────────────────────────────────
install(TARGETS robot_utils robot_node
EXPORT ${PROJECT_NAME}Targets # export set name
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
RUNTIME DESTINATION bin
INCLUDES DESTINATION include
)
install(DIRECTORY include/ DESTINATION include)
# Export so downstream packages can find_package(my_robot_pkg)
ament_export_targets(${PROJECT_NAME}Targets HAS_LIBRARY_TARGET)
ament_export_dependencies(rclcpp geometry_msgs)
ament_package()⚡ HAS_LIBRARY_TARGET tells ament to also export the library file location, not just headers. Omit it for header-only libraries.
Generating Custom Messages and Services
Put .msg/.srv/.action files in msg/, srv/, action/ subdirectories, then call rosidl_generate_interfaces to compile them.
# In CMakeLists.txt — custom interfaces
find_package(rosidl_default_generators REQUIRED)
find_package(geometry_msgs REQUIRED) # if your .srv uses these types
rosidl_generate_interfaces(
${PROJECT_NAME} # target name = package name
"msg/RobotStatus.msg"
"srv/FindObject.srv"
"action/Navigate.action"
DEPENDENCIES # list all packages whose types you import
geometry_msgs
std_msgs
)
# If an executable in THIS package uses the generated types:
rosidl_get_typesupport_target(cpp_typesupport_target
${PROJECT_NAME}
"rosidl_typesupport_cpp"
)
add_executable(robot_node src/robot_node.cpp)
target_link_libraries(robot_node ${cpp_typesupport_target})
ament_target_dependencies(robot_node rclcpp)
ament_export_dependencies(rosidl_default_runtime)
ament_package()⚡ Put ONLY interface packages (packages that define .msg/.srv/.action) in a <member_of_group>rosidl_interface_packages</member_of_group> tag in package.xml.
Build Types and CMake Args
Pass CMake arguments via colcon to control optimization level, enable debug symbols, or configure ccache for faster rebuilds.
# Release build (optimized, no debug symbols)
colcon build --cmake-args -DCMAKE_BUILD_TYPE=Release
# Debug build (unoptimized, full debug info for gdb)
colcon build --cmake-args -DCMAKE_BUILD_TYPE=Debug
# RelWithDebInfo — optimized + debug symbols (best for profiling)
colcon build --cmake-args -DCMAKE_BUILD_TYPE=RelWithDebInfo
# Enable ccache for faster recompilation (install: apt install ccache)
colcon build --cmake-args \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_BUILD_TYPE=RelWithDebInfo
# Parallel workers — default is all CPUs; reduce to avoid OOM
colcon build --parallel-workers 4
# Build only specific packages and their upstream deps
colcon build --packages-up-to my_robot_pkg
# Pass multiple cmake-args
colcon build --cmake-args \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_TESTING=OFF # skip test targets⚡ The default build type is empty (unoptimized, no debug info). Always set -DCMAKE_BUILD_TYPE=Release for production deploys.
Adding GTest and Linting
ament_cmake integrates with GTest and ament_lint. Add test targets after the BUILD_TESTING guard.
# Testing block — only compile when BUILD_TESTING=ON (default for dev)
if(BUILD_TESTING)
find_package(ament_lint_auto REQUIRED)
find_package(ament_cmake_gtest REQUIRED)
# Auto-configure common linting tools (cpplint, cppcheck, uncrustify)
ament_lint_auto_find_test_dependencies()
# Add a GTest executable
ament_add_gtest(test_robot_utils
test/test_robot_utils.cpp # your test file
)
ament_target_dependencies(test_robot_utils rclcpp)
target_link_libraries(test_robot_utils robot_utils)
# For integration tests that spin a node:
# ament_add_gtest_executable(...) — same but not registered automatically
endif()⚡ Run tests with: colcon test --packages-select my_robot_pkg && colcon test-result --verbose
Overlay Workspaces
ROS 2 uses layered workspaces. Source a base workspace, then build on top. The inner overlay takes priority over the outer underlay.
# 1. Underlay: source the base ROS 2 installation
source /opt/ros/jazzy/setup.bash
# 2. Build your overlay workspace
cd ~/ros2_ws
colcon build
# 3. Source your overlay (overwrites same-name packages from underlay)
source ~/ros2_ws/install/setup.bash
# --- Verify which installation is active ---
ros2 pkg prefix my_robot_pkg # → ~/ros2_ws/install/my_robot_pkg
# --- Force rebuild a single package (e.g., after CMakeLists change) ---
rm -rf build/my_robot_pkg install/my_robot_pkg
colcon build --packages-select my_robot_pkg
# --- Environment isolation: check overlay chain ---
echo $AMENT_PREFIX_PATH
# → ~/ros2_ws/install:/opt/ros/jazzy
# --- Python-only change, no rebuild needed ---
colcon build --symlink-install --packages-select my_robot_pkg
# Then edit .py file and run directly — no rebuildCMake API Cheatsheet
| CMake Call | What It Does |
|---|---|
| find_package(pkg REQUIRED) | Import a ROS/CMake package |
| ament_target_dependencies(tgt pkg1 pkg2) | Link target against ament packages (includes + libs) |
| target_link_libraries(tgt mylib) | Link against your own library target |
| rosidl_generate_interfaces(${PROJECT_NAME} ...) | Generate C++/Python bindings from .msg/.srv |
| install(TARGETS t DESTINATION lib/${PROJECT_NAME}) | Install executable (required for ros2 run) |
| install(DIRECTORY launch/ DESTINATION share/${PROJECT_NAME}/launch) | Install launch files |
| ament_export_targets(Targets HAS_LIBRARY_TARGET) | Expose library to downstream packages |
| ament_export_dependencies(pkg) | Propagate dependencies to downstream find_package |
| ament_add_gtest(test_name test/file.cpp) | Add a GTest test case |
| ament_package() | MUST be last — registers package in ament index |