【CMakeLists.txt的写法(基于ROS2)】
CMakeLists.txt的写法(基于ROS2)
- 基本构成
- 1. 配置(环境基础配置,头部声明)
- set的用法 - -(创建变量并给它赋值)
- 2. 查找(依赖包查找- - find_package)
- 完整语法与参数详解
- 常用实例
- 自定义消息/服务/动作
- 自定义工作空间包的查找
- 3. 构建(库和可执行节点)
- 构建库(Library)- - 其他包能够使用
- 包含并使用库
- 处理非ROS2的第三方库
- 构建可执行节点
- 4. 链接
- 5. 安装- - -install
- 完整示例:综合应用
- 🚨 两个关键注意事项
- 完整的代码示例
基本构成
在 ROS2 的 C++ 项目中,CMakeLists.txt 不是普通的编译脚本,而是一份“构建说明书”。它严格按照 “配置 → 查找 → 构建 → 链接 → 安装” 的生命周期组织。
1. 配置(环境基础配置,头部声明)
cmake_minimum_required(VERSION3.8)# 指定最低 CMake 版本project(your_package_name)# 定义项目名称,自动创建两个变量:(后续可直接使用) # ${PROJECT_NAME}(本CMakeLists.txt的project名称) # ${PROJECT_SOURCE_DIR}(本CMakeLists.txt所在的文件夹路径)- 作用:设置构建环境的门槛。ROS2 Humble 强烈建议 CMake 版本 ≥ 3.8。
- 注意:project() 定义的名称必须与package.xml中的< name >标签完全一致,否则 colcon build 会报错。
set的用法 - -(创建变量并给它赋值)
set(变量名 值)- 如果只给一个值,就是普通字符串变量。
- 如果给多个值(用空格或分号隔开),CMake 会自动将其拼接为分号分隔的列表(实际上是字符串列表)。
set(变量名 值 CACHE 类型 描述[FORCE])- 这种变量会写入 CMakeCache.txt,在多次 colcon build 之间保留。
- <类型> 必须是 STRING、BOOL、PATH、FILEPATH 之一。
- 加 FORCE 会强制覆盖已有的缓存值。
变量作用域核心规则
- 局部变量(不带CACHE):作用域仅限于当前 CMakeLists.txt 及其通过 add_subdirectory 包含的子目录。子目录的修改不会影响父目录。
- 缓存变量(带CACHE):全局生效,所有子目录都能读取。
- 引用变量:使用 $ {变量名} 取值。如果变量未定义,${变量名} 会被替换为空字符串(不会报错)。
常见使用案例
1. 强制指定 C++ 标准
# 设置 C++标准为17,且必须严格执行(不允许降级)set(CMAKE_CXX_STANDARD17)set(CMAKE_CXX_STANDARD_REQUIRED ON)# 关闭编译器对非标准 GNU 扩展的支持(保持跨平台兼容性)set(CMAKE_CXX_EXTENSIONS OFF)- 原理:CMAKE_CXX_STANDARD 是 CMake 内置变量,不写这行,编译器默认使用 C++14,会导致 std::make_unique 等 C++17 语法报错。
2. 定义源码列表,提高可维护性(避免 add_executable 写很长)
# 将多个源文件组合成一个变量set(SOURCES src/main.cpp src/controller.cpp src/pid.cpp src/odometry.cpp)# 然后直接在 add_executable 中引用add_executable(my_node ${SOURCES})- 优势:如果新增/删除源文件,只需改 set 这一处,不用动 add_executable 那一长串。
3. 修改编译优化等级(Debug/Release 切换)
# 强制设为 Release 模式(默认通常是 Debug)set(CMAKE_BUILD_TYPE Release)# 或者在 Debug 模式下禁用优化,保留调试符号set(CMAKE_CXX_FLAGS_DEBUG"-g -O0")- 注意:如果不设 CMAKE_BUILD_TYPE,很多编译器默认不给优化,导致运行变慢。在真实机器人上测试时,强烈建议改为 Release。
4. 给编译器加特定参数(如打开所有警告)
# 追加编译选项(不会覆盖原有选项)set(CMAKE_CXX_FLAGS"${CMAKE_CXX_FLAGS} -Wall -Wextra -Wpedantic")# 或者专门针对特定目标(更推荐这种,不影响其他节点)set(MY_NODE_FLAGS"-O3 -march=native")target_compile_options(my_node PRIVATE ${MY_NODE_FLAGS})5. 处理列表变量(追加/删除/遍历)
# 定义初始列表set(MY_PACKAGES rclcpp std_msgs)# 追加元素(方法1:用 list 命令)list(APPEND MY_PACKAGES geometry_msgs visualization_msgs)# 追加元素(方法2:直接 set 拼接,注意引号)set(MY_PACKAGES ${MY_PACKAGES}nav_msgs)# 此时变成5个元素 # 移除某个元素list(REMOVE_ITEM MY_PACKAGES visualization_msgs)# 遍历列表(高级用法,在 ROS2 中常用于批量安装启动文件)foreach(pkg ${MY_PACKAGES})find_package(${pkg}REQUIRED)endforeach()6. 设置缓存变量(让用户在命令行灵活修改)
在 CMakeLists.txt 中写入:
set(USE_SIM_TIMEfalseCACHE BOOL"是否使用仿真时间模式")此时,用户在执行 colcon build 时可以传入参数覆盖:
colcon build--packages-select your_pkg--cmake-args-DUSE_SIM_TIME=ON编译后,这个值会固化到 build/your_pkg/CMakeCache.txt 中,后续编译直接读取缓存值,无需重复输入。
- 注意:列表变量必须带引号
set(FLAGS-Wall-Wextra)# 错误!CMake 会把-Wextra 当成第二个值set(FLAGS"-Wall -Wextra")# 正确!整体作为一个字符串- 注意:覆盖内置变量要谨慎。比如 set(CMAKE_CXX_STANDARD 17) 必须写在 project() 之后,因为 project() 会初始化编译器检测,写在前面可能导致部分检测失效。
2. 查找(依赖包查找- - find_package)
告诉 CMake,去系统里把某个“外部库”的安装位置、头文件路径、库文件路径以及编译宏全部挖出来,并准备好供后续使用。
完整语法与参数详解
find_package(<包名>[版本号][EXACT][QUIET][REQUIRED][COMPONENTS<组件列表>])| 参数 | 含义 | 建议 |
|---|---|---|
| REQUIRED | 必须找到,找不到立即报错并终止 CMake 配置。 | 必须加。如果不加,找不到时只会警告,后续 ament_target_dependencies 会因为变量为空而报更奇怪的链接错误。 |
| 版本号 | 如 find_package(rclcpp 5.0.0 REQUIRED) | 极少用,因为 ROS2 版本由发行版(Humble)整体锁定。 |
| QUIET | 静默模式,不输出“找到了”的提示信息。 | 一般不写,方便观察日志排查路径是否正确。 |
| COMPONENTS | 只查找该包中的特定子组件。 | 例如 find_package(OpenCV REQUIRED COMPONENTS core imgproc),在 ROS2 中对 nav2_msgs 等复合包有用。 |
| EXACT | 默认行为(不加 EXACT) 精确行为(加 EXACT) | find_package(rclcpp 5.0.0 EXACT REQUIRED)表示版本号必须是5.0.0,不加EXACT表示版本号>=5.0.0 |
执行成功后,CMake 会在内存中定义一系列变量(以 rclcpp 为例):
| 变量名 | 含义 | 示例值 |
|---|---|---|
| rclcpp_FOUND | 是否找到(True/False) | TRUE |
| rclcpp_INCLUDE_DIRS | 头文件搜索路径 | /opt/ros/humble/include/rclcpp |
| rclcpp_LIBRARIES | 库文件名 | rclcpp(对应 librclcpp.so) |
| rclcpp_DEFINITIONS | 编译宏定义(极少用到) | -DRCLCPP_BUILD_DLL |
注意:
- 在 ROS2 的现代用法中,几乎不用手动写 target_include_directories 去引用这些变量。我们直接用 ament_target_dependencies,它会自动读取这些变量并附加到目标上。
- CMakeLists.txt 中的 find_package:负责编译时找到头文件和静态/动态库。
- package.xml 中的 < depend> 标签:负责运行时把包路径加入 AMENT_PREFIX_PATH。
如果你只在 CMake 里写了 find_package(rclcpp REQUIRED),却忘了在 package.xml 里写 < depend >rclcpp</ depend >:
👉 编译能通过(因为 find_package 去了系统路径找),但编译完成后,colcon build 生成的 install 目录下的环境脚本中,不会把 rclcpp 写入依赖链。当你运行 ros2 run 时,系统会报错 symbol lookup error 或 cannot open shared object file。
常用实例
# 查找系统或工作空间中已安装的 ROS2 基础包find_package(ament_cmake REQUIRED)# Ament 构建系统的核心(必须)find_package(rclcpp REQUIRED)# ROS2 C++客户端库(写节点必须)find_package(std_msgs REQUIRED)# 标准消息包(如果需要订阅/发布基础类型)find_package(geometry_msgs REQUIRED)# 几何消息(如 Twist,Pose) # 如果自定义了接口(.msg/.srv/.action),必须查找生成器find_package(rosidl_default_generators REQUIRED)- 底层逻辑:find_package 会去 /opt/ros/humble 和 install 目录下寻找对应的 xxx-config.cmake 文件,加载库路径、头文件路径和编译宏定义。
- 黄金法则:CMakeLists.txt 中 find_package 列出的每一个包,必须同时在 package.xml 中用< depend>或<build_depend>声明,否则编译通过但运行时可能缺少动态链接库。
自定义消息/服务/动作
# 查找生成依赖find_package(rosidl_default_generators REQUIRED)# 声明要生成的接口文件(假设 msg/MyMsg.msg 和 srv/MySrv.srv 存在)rosidl_generate_interfaces(${PROJECT_NAME}"msg/MyMsg.msg""srv/MySrv.srv"DEPENDENCIES std_msgs # 生成代码时依赖的包(如 MyMsg 里引用了 std_msgs/Header))# 让当前包也可以使用自己生成的接口头文件ament_export_dependencies(rosidl_default_runtime)- 作用:将 .msg / .srv 文本描述,编译成 C++ 可用的 .hpp 头文件和动态库
- 生成位置:编译后会在 install/your_package/include/your_package/msg/my_msg.hpp 下生成代码。
- 关键点:DEPENDENCIES 必须包含消息字段中引用的所有其他 ROS2 包。
自定义工作空间包的查找
假设你的 src 下有两个包:my_interface(定义消息)和 my_node(使用消息)。
在 my_node/CMakeLists.txt 中,需要这样写才能找到隔壁包:
#1.必须用 find_package 找到自定义接口包(因为它在你的工作空间 install 里)find_package(my_interface REQUIRED)#2.然后才能把它的消息库链接过来add_executable(publisher src/publisher.cpp)ament_target_dependencies(publisher rclcpp my_interface)3. 构建(库和可执行节点)
构建库(Library)- - 其他包能够使用
步骤:
1.构建功能包
首先,创建一个构建类型为ament_cmake的包。
cd~/ros2_ws/src ros2 pkg create my_math_lib--build-type ament_cmake--dependencies rclcpp #--dependencies rclcpp 指定了库可能依赖的ROS2核心包2.编写库的代码
- 头文件:在 include/my_math_lib/my_math_lib.hpp 中声明你的函数或类。
- 源文件:在 src/my_math_lib.cpp 中实现它们。
3.配置CMakeLists.txt
这是构建库最关键的部分,需要完成以下几个任务:
- a. 定义库目标:使用 add_library() 将源文件编译成库。通常使用 SHARED (动态库)。
add_library(${PROJECT_NAME}SHARED src/my_math_lib.cpp)一个包通常只导出一个与包同名的核心库,可以将核心库需要的所有.cpp文件都加在括号里
- b. 链接ROS2依赖:使用 ament_target_dependencies() 为你的库链接它所需的ROS2包
ament_target_dependencies(${PROJECT_NAME}rclcpp)- c. 声明头文件路径:使用 target_include_directories() 声明公共头文件目录,并利用生成器表达式区分构建和安装阶段
target_include_directories(${PROJECT_NAME}PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>$<INSTALL_INTERFACE:include>)- d. 导出目标和依赖:这是让其他包能找到你的库的关键
ament_export_targets():将你的库目标(${PROJECT_NAME})导出,供其他包使用。
ament_export_dependencies():声明你的库所依赖的ROS2包(如rclcpp)。这样,当其他包使用你的库时,会自动链接这些依赖。
ament_export_targets(export_${PROJECT_NAME}HAS_LIBRARY_TARGET)ament_export_dependencies(rclcpp)- e. 安装文件:将头文件和编译好的库文件安装到工作空间的install目录下
install(DIRECTORY include/DESTINATION include)install(TARGETS ${PROJECT_NAME}EXPORT export_${PROJECT_NAME}LIBRARY DESTINATION lib ARCHIVE DESTINATION lib RUNTIME DESTINATION bin)EXPORT参数必须与ament_export_targets()中的名字保持一
- f. 别忘了结尾:在所有配置的最后,必须加上 ament_package()。
4.配置 package.xml
package.xml 无需特殊修改,默认的 buildtool_depend 和 build_type 就足够了。
包含并使用库
如果另一个包 complex_test 需要使用上面构建的my_math_lib库,则需要:
1.在 package.xml 中声明依赖
在 complex_test 的 package.xml 中,添加对 my_math_lib 的依赖。
<depend>my_math_lib</depend>2.在 CMakeLists.txt 中查找并链接
a. 查找库:使用 find_package() 来查找 my_math_lib。
find_package(my_math_lib REQUIRED)b. 创建可执行文件:定义你的节点目标。
add_executable(complex_test src/complex_test.cpp)c. 链接依赖:使用 ament_target_dependencies() 链接 my_math_lib 和它所需的所有依赖。
ament_target_dependencies(complex_test my_math_lib rclcpp)这条命令会自动处理 my_math_lib 的头文件路径、库文件以及它导出的所有依赖(如 rclcpp)。
d. 安装可执行文件:为了让 ros2 run 能找到,需要安装目标。
install(TARGETS complex_test DESTINATION lib/${PROJECT_NAME})3. 在源码中使用库
在你的C++代码中,就可以直接包含并使用库的头文件了。
#include"my_math_lib/my_math_lib.hpp"// ... 在代码中调用 my_math_lib 提供的功能处理非ROS2的第三方库
对于 Eigen3、OpenCV 这类非ROS2的第三方库,处理方式略有不同:
- 如果第三方库支持 find_package():这是最理想的方式。
1.在 CMakeLists.txt 中使用 find_package() 找到它。
2.在 ament_target_dependencies() 中直接添加包名。如果该库导出了自己的依赖,ament_target_dependencies 也能自动处理
find_package(Eigen3 REQUIRED)ament_target_dependencies(complex_test Eigen3)- 如果第三方库不支持 find_package():你需要手动指定路径。
1.将库文件(.so/.a)和头文件(.h/.hpp)放在你包内的目录中,如 lib/ 和 include/。
2.在 CMakeLists.txt 中手动添加头文件路径和链接库。
include_directories(include)add_executable(cantest src/cantest.cpp)#当前功能包的target_link_libraries(cantest ${CMAKE_CURRENT_SOURCE_DIR}/lib/libcanbus.so)构建可执行节点
4. 链接
ROS2 / CMake 中实现链接的 3 种写法:
在 CMakeLists.txt 中,有三种手段来指挥链接器。
① ament_target_dependencies() —— ROS2 的首选(智能链接)
这是 ROS2 封装的最强工具,不仅链接库,还会自动传递头文件路径和依赖关系。
find_package(rclcpp REQUIRED)add_executable(my_node src/main.cpp)ament_target_dependencies(my_node rclcpp std_msgs)它相当于自动帮你写了两行代码:
- 添加头文件:target_include_directories(…)
- 链接库:target_link_libraries(my_node ${rclcpp_LIBRARIES} ${std_msgs_LIBRARIES})
并且,如果 rclcpp 依赖了 librmw,ament_target_dependencies 会递归把 librmw 也链接进来。
② target_link_libraries() —— CMake 原生写法(手动链接)
当你在链接非 ROS2 的纯第三方库(如 OpenCV、Eigen、PCL)或自己写的纯 C++ 库时使用。
find_package(OpenCV REQUIRED)include_directories(${EIGEN3_INCLUDE_DIR}${OpenCV_INCLUDE_DIRS}include)add_executable(my_vision src/vision.cpp)# 手动链接 OpenCV 库target_link_libraries(my_vision ${OpenCV_LIBRARIES})5. 安装- - -install
install() 是一个 CMake 命令,它的作用就像一份 “部署清单” 。它告诉构建系统,在编译完成后,需要把哪些文件(比如编译好的节点程序、自定义的接口文件、启动脚本等)复制到 install 目录下的哪个具体位置。
install() 命令主要用于安装四种不同类型的对象:
- 安装可执行文件 (TARGETS)
用途:安装编译生成的节点程序(可执行文件)和库文件(libxxx.so)。
标准写法:
install(TARGETS<target1><target2>...DESTINATION lib/${PROJECT_NAME})TARGETS:后面跟着你用 add_executable() 或 add_library() 定义的目标名称。
DESTINATION:指定安装的目标文件夹。
对于可执行文件,必须安装在 lib/${PROJECT_NAME} 目录下。
对于库文件,同样建议安装在 lib/${PROJECT_NAME} 目录下。
- 安装目录 (DIRECTORY)
用途:安装一整个文件夹,例如 launch/ 启动文件夹、config/ 配置文件夹、rviz/ 可视化配置文件等。
标准写法:
install(DIRECTORY<目录1><目录2>...DESTINATION share/${PROJECT_NAME})DIRECTORY:后面跟着要安装的文件夹名称,例如 launch 或 config。
DESTINATION:指定安装的目标文件夹。通常安装在 share/${PROJECT_NAME} 目录下。
- 安装文件 (FILES)
用途:安装单个文件,例如根目录下的 package.xml 或一些重要的配置文件。
标准写法:
install(FILES<文件1><文件2>...DESTINATION share/${PROJECT_NAME})FILES:后面跟着要安装的文件名。
DESTINATION:与安装目录类似,通常目标也是 share/${PROJECT_NAME}。
- 安装头文件 (DIRECTORY)
用途:安装公共头文件(.h/.hpp),以便工作空间中的其他功能包能够通过 find_package 找到并调用你编写的库。
标准写法:
install(DIRECTORY include/DESTINATION include)这与安装 launch 目录的写法类似,但目标路径是 include。
注意路径 include/ 末尾的斜杠 /,这表示安装该目录下的内容,而不是整个目录本身。
完整示例:综合应用
#...(find_package,add_executable,target_link_libraries 等)# 安装编译生成的目标文件install(TARGETS talker # 节点1listener # 节点2my_math_lib # 自定义库 DESTINATION lib/${PROJECT_NAME})# 安装头文件(供其他包使用)install(DIRECTORY include/DESTINATION include)# 安装启动文件和配置文件install(DIRECTORY launch config DESTINATION share/${PROJECT_NAME})# 安装自定义接口定义文件(如果有)install(DIRECTORY msg srv action DESTINATION share/${PROJECT_NAME}/)#...ament_package()必须在最后ament_package()🚨 两个关键注意事项
位置必须在 ament_package() 之前:所有的 install() 命令都必须放在 CMakeLists.txt 文件的末尾,但在 ament_package() 这个最终命令的之前。
没有 install 就无法使用:如果缺少对应的 install() 命令,编译生成的文件就不会被复制到 install 目录。这意味着 ros2 run 无法找到节点,ros2 launch 无法找到启动文件,find_package 也无法找到你的库和头文件。
完整的代码示例
参考:ROS2CMakeLists的常见内容
