Skip to main content
🏗️ ament_cmakeROS 2 · June 2026

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.

1

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.

text
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.

2

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
<?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).

3

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
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.

4

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.

cmake
# ─── 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.

5

Generating Custom Messages and Services

Put .msg/.srv/.action files in msg/, srv/, action/ subdirectories, then call rosidl_generate_interfaces to compile them.

cmake
# 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.

6

Build Types and CMake Args

Pass CMake arguments via colcon to control optimization level, enable debug symbols, or configure ccache for faster rebuilds.

bash
# 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.

7

Adding GTest and Linting

ament_cmake integrates with GTest and ament_lint. Add test targets after the BUILD_TESTING guard.

cmake
# 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

8

Overlay Workspaces

ROS 2 uses layered workspaces. Source a base workspace, then build on top. The inner overlay takes priority over the outer underlay.

bash
# 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 rebuild

CMake API Cheatsheet

CMake CallWhat 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