跳到主要内容

在实际开发中,一个机器人项目往往包含多个模块,例如感知、定位、控制等。每个模块可能需要单独构建、维护,甚至可能由不同开发人员协作完成。为了管理这种结构,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 构建系统(如 colconrosdep)解析,决定安装哪些依赖包

2. 子模块部分 (1)src/ 主控制节点

src/
└── main_controller.cpp
  • 主节点的源文件,例如用于控制机器人主逻辑(路径规划、行为控制等)

  • 可能会依赖其他模块的功能(如 utils、navigation 提供的函数/服务)

(2) utils/ 工具模块

utils/
├── CMakeLists.txt ← 底层构建文件
├── math_utils.cpp
└── include/utils/math_utils.hpp
  • math_utils.hppmath_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 及其依赖的子模块(utilsnavigationthird_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 工程的组织方式、掌握多模块项目的构建流程提供了坚实基础,也为后续开展复杂系统开发(如机器人导航、感知、决策等)打下了良好准备。