在实际开发中,一个机器人项目往往包含多个模块,例如感知、定位、控制等。每个模块可能需要单独构建、维护,甚至可能由不同开发人员协作完成。为了管理这种结构,ROS2 支持在一个功能包或工作空间中使用多个 CMakeLists.txt 文件,形成子模块(子项目)结构。本章将详细介绍多级 CMake 构建的基本原理。
多级 CMake 的结构
在 ROS2 中,CMake 支持递归构建,即在顶层 CMakeLists.txt 中使用 add_subdirectory() 指令,将子模块作为子项目加入构建系统。
这样做的优点包括:
- 模块化管理,便于团队协作
- 子模块可以单独测试与维护
- 减少主 CMakeLists.txt 复杂度
- 支持子模块构建为共享库供主节点调用
- 支持每个子模块链接自己需要的第三方库
以下是一个典型的多模块功能包结构:
my_robot_package/
├── CMakeLists.txt
├── package.xml
├── src/
│ └── main_controller.cpp
├── utils/
│ ├── CMakeLists.txt
│ ├── math_utils.cpp
│ └── include/utils/math_utils.hpp
├── navigation/
│ ├── CMakeLists.txt
│ └── navigation_node.cpp
├── third_party_lib/
│ ├── CMakeLists.txt
│ └── camera_processor.cpp
1.顶层目录结构说明
my_robot_package/
├── CMakeLists.txt ← 顶层构建文件
├── package.xml ← ROS2 功能包元信息和依赖管理
├── src/
├── utils/
├── navigation/
├── third_party_lib/
(1)CMakeLists.txt(顶层)
-
主构建脚本,配置整个功能包的构建过程。
-
通常包括:
-
查找 ROS2 依赖(如
rclcpp) -
调用
add_subdirectory(utils)等,递归构建子模块 -
安装主节点或组织链接
-
该文件是整个功能包的“控制中心”。
(2) package.xml
-
定义 ROS2 功能包的元数据(如名称、版本、依赖、许可证等)
-
被 ROS 构建系统(如
colcon、rosdep)解析,决定安装哪些依赖包
2. 子模块部分
(1)src/ 主控制节点
src/
└── main_controller.cpp
-
主节点的源文件,例如用于控制机器人主逻辑(路径规划、行为控制等)
-
可能会依赖其他模块的功能(如 utils、navigation 提供的函数/服务)
(2) utils/ 工具模块
utils/
├── CMakeLists.txt ← 底层构建文件
├── math_utils.cpp
└── include/utils/math_utils.hpp
-
math_utils.hpp和math_utils.cpp定义了数学工具函数(如角度计算、坐标转换) -
utils/CMakeLists.txt工具函数编译为静态/共享库 -
通过头文件暴露接口,供其他模块(如
main_controller.cpp)调用
(3)navigation/ 导航模块
navigation/
├── CMakeLists.txt
└── navigation_node.cpp
-
实现机器人导航相关功能(如路径规划、位置估计)
-
通常会被视为单独的 ROS 2 节点
-
可在子模块中通过
add_executable()构建为独立节点 -
该模块也可以依赖
utils/提供的函数
(4)third_party_lib/ 第三方处理库模块
third_party_lib/
├── CMakeLists.txt
└── camera_processor.cpp
-
封装了一个第三方处理库(如图像识别、深度相机接口等)
-
可以作为共享库编译
-
独立于 ROS 依赖,也可用于非 ROS 项目
-
可以隐藏细节,只暴露给主节点或其他模块调用
各级文件的代码
1.顶层 CMakeLists.txt 示例
cmake_minimum_required(VERSION 3.8)
project(my_robot_package)
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
add_executable(main_controller src/main_controller.cpp)
ament_target_dependencies(main_controller rclcpp)
# 链接 utils 模块生成的库
target_link_libraries(main_controller utils_library)
install(TARGETS
main_controller
DESTINATION lib/${PROJECT_NAME})
# 添加子模块目录
add_subdirectory(utils)
add_subdirectory(navigation)
add_subdirectory(third_party_lib)
ament_package()
这个文件用于组织并构建主节点 main_controller 及其依赖的子模块(utils、navigation、third_party_lib)。主要代码解释如下:
(1) 添加主可执行文件
add_executable(main_controller src/main_controller.cpp)
ament_target_dependencies(main_controller rclcpp)
-
将
src/main_controller.cpp编译为可执行程序main_controller。 -
通过
ament_target_dependencies告诉 ROS 构建系统,该程序依赖rclcpp功能包。
(2) 链接子模块生成的库
target_link_libraries(main_controller utils_library)
-
将
utils模块中构建的库(比如add_library(utils_library ...))链接到主程序main_controller。 -
这允许
main_controller.cpp使用utils中的函数、类等功能。
要确保
utils/CMakeLists.txt中有构建并命名该库为utils_library。
(3) 安装目标
install(TARGETS
main_controller
DESTINATION lib/${PROJECT_NAME})
构建完成后将 main_controller 安装到安装路径下的 lib/my_robot_package/ 目录,供部署使用。
(4) 添加子模块目录
add_subdirectory(utils)
add_subdirectory(navigation)
add_subdirectory(third_party_lib)
-
添加
utils/、navigation/、third_party_lib/子目录。 -
每个子目录中必须包含自己的
CMakeLists.txt,用于定义:-
源文件编译方式
-
构建库或节点
-
安装路径
-
与其他模块的依赖关系
-
2.utils/CMakeLists.txt(构建为静态库)
add_library(utils_library STATIC math_utils.cpp)
target_include_directories(utils_library PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>)
install(TARGETS utils_library DESTINATION lib)
install(DIRECTORY include/ DESTINATION include)
这段 CMake 代码是 utils 子模块的 CMakeLists.txt 中的内容,用于构建并安装一个名为 utils_library 的静态库。
(1) 生成静态库
add_library(utils_library STATIC math_utils.cpp)
-
表示将
math_utils.cpp编译为一个名为utils_library的 静态库(.a文件)。 -
STATIC表示编译为静态链接库(与SHARED共享库相对)。 -
静态库可以被其他目标程序(如主节点
main_controller)通过target_link_libraries链接使用。
(2) 设置头文件路径
target_include_directories(utils_library PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>)
这一段设置了该库的头文件路径,让其他模块(或主程序)能正确包含头文件。
PUBLIC:表示这些头文件路径不仅当前库可见,而且链接它的其他目标也能看到。
两种路径含义:
-
$<BUILD_INTERFACE:...>-
编译阶段使用的头文件路径。
-
这里是
utils/include,比如:
~/ros2_ws/src/my_robot_package/utils/include/
-
-
$<INSTALL_INTERFACE:include>-
安装阶段(即
colcon install之后)使用的头文件路径。 -
表示将来安装到
install/目录下的include子目录。
-
(3) 安装库文件
install(TARGETS utils_library DESTINATION lib)
-
指定将构建生成的
utils_library.a静态库安装到路径:install/lib/ -
其中
install是 ROS 2 安装目录(如~/ros2_ws/install/my_robot_package)。
(4) 安装头文件
install(DIRECTORY include/ DESTINATION include)
- 把当前子模块下的
include/目录安装到工作空间的install/include/路径下。 - 这样其他包(或主程序)在查找头文件时可以使用:
#include "utils/math_utils.hpp"
3. navigation/CMakeLists.txt(构建为节点)
find_package(rclcpp REQUIRED)
add_executable(navigation_node navigation_node.cpp)
ament_target_dependencies(navigation_node rclcpp)
install(TARGETS navigation_node DESTINATION lib/${PROJECT_NAME})
这段 CMake 配置代码是为 ROS2 中的 navigation 子模块设置构建规则,首先查找并加载 rclcpp 功能包,然后把 navigation_node.cpp 编译为一个名为 navigation_node 的可执行程序,接下来为 navigation_node 这个目标程序声明依赖关系,最后指定将构建好的可执行程序 navigation_node 安装到 ROS2 工作空间的安装目录中,安装路径示例(~/ros2_ws/install/my_robot_package/lib/my_robot_package/navigation_node),这样 ROS2 的运行系统(如 ros2 run my_robot_package navigation_node)就能找到这个节点并执行。
4. third_party_lib/CMakeLists.txt(模拟第三方库)
在实际开发中,Ubuntu 24.04 已默认安装 了一些第三方库,例如 OpenCV(libopencv-dev)。如果希望让子模块使用这些第三方库,可以进行下面的设置,此例以 OpenCV 为例,CMakeLists.txt 文件配置如下:
find_package(rclcpp REQUIRED)
find_package(OpenCV REQUIRED)
add_executable(camera_processor camera_processor.cpp)
ament_target_dependencies(camera_processor rclcpp)
target_include_directories(camera_processor PUBLIC ${OpenCV_INCLUDE_DIRS})
target_link_libraries(camera_processor ${OpenCV_LIBRARIES})
install(TARGETS camera_processor DESTINATION lib/${PROJECT_NAME})
当使用 find_package(OpenCV REQUIRED) 后,- 加载后会自动设置变量:
OpenCV_INCLUDE_DIRS:OpenCV 的头文件路径,OpenCV_LIBRARIES:OpenCV 所需链接的库文件(如opencv_core,opencv_imgproc等) 命令行
target_include_directories(camera_processor PUBLIC ${OpenCV_INCLUDE_DIRS})
会将 OpenCV 的头文件路径添加到 camera_processor 的编译中。例如支持:
#include <opencv2/opencv.hpp>。
target_link_libraries(camera_processor ${OpenCV_LIBRARIES})命令 将 OpenCV 的库链接到 camera_processor 可执行文件中。若没有这一步,在链接阶段会报错(如找不到 cv::Mat 等函数实现)。
示例源代码
1. main_controller.cpp
#include "rclcpp/rclcpp.hpp"
#include "utils/math_utils.hpp"
int main(int argc, char **argv) {
rclcpp::init(argc, argv);
auto result = utils::add(3, 4);
RCLCPP_INFO(rclcpp::get_logger("main_controller"), "3 + 4 = %d", result);
rclcpp::shutdown();
return 0;
}
2. utils/include/utils/math_utils.hpp
#ifndef MATH_UTILS_HPP
#define MATH_UTILS_HPP
namespace utils {
int add(int a, int b);
}
#endif // MATH_UTILS_HPP
3. utils/math_utils.cpp
#include "utils/math_utils.hpp"
namespace utils {
int add(int a, int b) {
return a + b;
}
}
4. navigation/navigation_node.cpp
#include "rclcpp/rclcpp.hpp"
int main(int argc, char **argv) {
rclcpp::init(argc, argv);
RCLCPP_INFO(rclcpp::get_logger("navigation_node"), "Navigation node started.");
rclcpp::shutdown();
return 0;
}
5. third_party_lib/camera_processor.cpp
#include "rclcpp/rclcpp.hpp"
#include <opencv2/opencv.hpp>
int main(int argc, char **argv) {
rclcpp::init(argc, argv); // 创建一张黑色图像(模拟图像处理)
cv::Mat img = cv::Mat::zeros(480, 640, CV_8UC3);
RCLCPP_INFO(rclcpp::get_logger("camera_processor"),
"OpenCV created image of size: %d x %d", img.cols, img.rows);
rclcpp::shutdown();
return 0;
}
构建与运行
cd ~/ros2_ws
colcon build --packages-select my_robot_package
source install/setup.bash
运行节点:
ros2 run my_robot_package main_controller
ros2 run my_robot_package navigation_node
ros2 run my_robot_package camera_processor
本章小结
本章节深入介绍了 ROS2 功能包中 CMakeLists.txt 文件的标准结构及其作用,重点讲解了如何利用 CMake 实现模块化开发,通过 add_subdirectory() 实现子模块的递归构建,不仅提高了系统的可维护性,也便于团队协作与单元测试。这些内容为读者理解大型 ROS2 工程的组织方式、掌握多模块项目的构建流程提供了坚实基础,也为后续开展复杂系统开发(如机器人导航、感知、决策等)打下了良好准备。