【CMake】C++系统构建的具体实现
写在前面
本文配合基于CMake构建规范c++系统的指南一起食用,旨在讲清楚cmake如何组织完整的一套程序;
我原来有一篇基于ROS的cmake构建指南,但是因为用到了ROS的一些东西,导致不具有泛用性,因此重新写一篇完整的说明
本文删改了好多次,甚至快写完了又重写,总是觉得讲不到位,像是在repeat api手册,主要还是对目标的理解不够深刻;
因此,这次我尝试从整体的角度来讲cmakelists的编写
CMake的任务目标
可以概括为五个核心阶段,它们也是我们将程序从代码到执行需要经过的最完整的步骤:
| 阶段 | 现实问题 | CMake 要做的事 | 关键命令 |
|---|---|---|---|
| 1️⃣ 定义项目 | 我是谁?要编译什么语言? | 创建工程、指定语言和标准 | project()、set(CMAKE_CXX_STANDARD) |
| 2️⃣ 找依赖 | 我需要哪些外部库?头文件和.so 在哪? | 搜索路径、找到并记录依赖 | find_path()、find_library()、find_package() |
| 3️⃣ 生成目标 | 我要编译哪些文件?生成可执行文件或库 | 声明目标(executable/library) | add_executable()、add_library() |
| 4️⃣ 编译链接 | 编译时要 -I 哪些目录?链接哪些库? | 设置 include 路径、库路径、链接命令 | target_include_directories()、target_link_libraries() |
| 5️⃣ 运行分发 | 运行时要能找到.so,怎么安装? | 设置 RPATH、安装规则 | set(CMAKE_INSTALL_RPATH)、install() |
这五个阶段就是任何 CMake 项目背后的逻辑骨架
阶段一,定义项目部分不再赘述
举例:让程序能直接 #include <zmq.hpp> 并调用 API
想象我们要写个程序 imu.cpp,里面有:
#include <zmq.hpp>
int main() {
zmq::context_t ctx;
zmq::socket_t sock(ctx, zmq::socket_type::pub);
sock.bind("ipc:///tmp/imu_pub");
}
现在编译器吐槽:
fatal error: zmq.hpp: No such file or directory
undefined reference to zmq_ctx_new
这两个报错其实对应两个现实问题:
| 错误 | 原因 | 解决思路 |
|---|---|---|
| “找不到 zmq.hpp” | 编译器不知道去哪找头文件 | 设置 include 路径 |
| “undefined reference” | 链接器没把 libzmq.so 加进来 | 设置链接库路径 |
于是,我们需要用CMake 解决这俩问题。
问题1:让编译器能找到头文件
现实情况:我把 zmq.hpp 放在deps/libzmq/include/zmq.hpp
但编译器默认只找系统路径(/usr/include),找不到
CMake 解决它的方法:
find_path(ZMQ_INCLUDE_DIR
NAMES zmq.hpp zmq.h
HINTS "${CMAKE_SOURCE_DIR}/deps/libzmq/include"
NO_DEFAULT_PATH
)
这行命令的作用就像:
“去指定目录里看看有没有 zmq.hpp,找到了就把它的路径存在变量 ZMQ_INCLUDE_DIR 里”
然后告诉编译器:
target_include_directories(imu PRIVATE ${ZMQ_INCLUDE_DIR})
这句等价于给 g++ 增加 -I/path/to/deps/libzmq/include。
编译器于是知道去哪找 <zmq.hpp>。
问题2:让链接器能找到库
现在编译没错,但链接时报:
undefined reference to zmq_ctx_new
意思是:代码里声明了函数,但链接器找不到实现
实现其实就在 libzmq.so 里
CMake 让我们解决这个问题的方式是:
find_library(ZMQ_LIBRARY
NAMES zmq libzmq
HINTS "${CMAKE_SOURCE_DIR}/deps/libzmq/lib"
NO_DEFAULT_PATH
)
这相当于:
“去指定目录里看看有没有 libzmq.so,把路径记下来。”
然后告诉链接器:
target_link_libraries(imu PRIVATE ${ZMQ_LIBRARY})
等价于给 g++ 加上 -Ldeps/libzmq/lib -lzmq
于是链接通过,程序能用 ZMQ 的 API
Question 2.5: use function find_package() instead of find_path() and find_library()
find_package() 是 CMake 的高级封装,用于查找一整个库的使用环境
它不仅能找到 include、lib,还能生成可直接使用的 target
在现代 CMake(>=3.x)生态中,推荐:
- 直接使用 find_package(),而不是find_path和find_library;
- 库作者提供自己的 <Package>Config.cmake;
- 通过 target 引入依赖,不再关心路径变量,不用再手动 find_path / find_library
这通常是更推荐的,以及更加现代的写法,但是它有个前提:
- 当前安装的目标库 third-party lib 包含 abcdConfig.cmake文件
- 它们通常存放在三方库安装路径下的
/lib/cmakeor/share/cmake文件夹中
find_package(ZeroMQ REQUIRED CONFIG
PATHS "${ZMQ_ROOT}"
NO_DEFAULT_PATH
)
target_link_libraries(imu PRIVATE
libzmq
Threads::Threads
)
需要注意的是:
- 目标名就是find_path / find_library / find_package 被调用后导入的对象,像上面的程序就是libzmq
- 从中不难看出包名不等于目标名,上面的程序中,包名是ZeroMQ,而目标名是libzmq,因此在链接时也需要使用目标名libzmq
- 因此,find_package并不是一个完全可控的api,依赖于库的作者,在库比较简单,或者find_package效果不对时,也可以考虑回退到手动 find_path / find_library
前两个问题是编译时会遇到的,现在我们运行时还有新的问题产生
问题3:运行时找不到库(libzmq.so)
我们运行时又遇到:
error while loading shared libraries: libzmq.so: cannot open shared object file
原因:运行时动态链接器不知道去哪找 .so
Linux 默认只看 /usr/lib,但我们把 .so 放在项目的 deps/libzmq/lib
CMake 的解决方案:
在生成可执行文件时,把库路径写进 ELF 的 RPATH 字段
set(CMAKE_BUILD_RPATH "${CMAKE_SOURCE_DIR}/deps/libzmq/lib")
set(CMAKE_INSTALL_RPATH "\$ORIGIN/../deps/libzmq/lib")
解释:
- BUILD_RPATH:在
build/bin里直接跑的时候,用绝对路径 - INSTALL_RPATH:安装后(比如在
install/bin下)运行,用相对路径$ORIGIN,表示“当前可执行文件目录”
这一部分就是在明确告诉系统,在不同的状态下运行时,分别去哪里找动态库依赖
问题4:安装分发程序,让程序脱离原环境也能运行
当我们要把程序发给别人时,我们希望他只需:
cd install/bin
./imu
就能跑。
所以要让 CMake 在安装时把所有必要的文件拷过去:
install(TARGETS imu RUNTIME DESTINATION bin)
install(DIRECTORY ${CMAKE_SOURCE_DIR}/deps/libzmq/lib/ DESTINATION lib FILES_MATCHING PATTERN "libzmq*.so*")
这两行干了两件事:
- 把可执行文件安装到
install/bin - 把
.so库安装到install/lib
结合上面的 RPATH 设置,程序一运行就能自己找到依赖库
问题5:多个组件协同(子目录)
当我们的项目有多个可执行文件或模块(比如 imu、controller),就需要组织结构
这时顶层只管资源路径与依赖,具体编译逻辑交给子目录:
add_subdirectory(src/imu)
add_subdirectory(src/controller)
子目录各自有:
add_executable(imu src/imu_pub.cpp)
target_link_libraries(imu PRIVATE ${ZMQ_LIBRARY})
完整流程总结
需要说明的是,cmakelists的编写并没有严格的顺序,函数与函数之间通常是并列的关系,但是存在一些惯例,出于美观上的考量,下文中会遵循这些惯例
项目顶级根目录(例如 ./CMakeLists.txt):
cmake_minimum_requiredproject+ 语言/标准(set(CMAKE_CXX_STANDARD …))- 依赖根路径
set() - 依赖查找
find_path / find_library / find_package+message(STATUS …) - 全局输出目录、安装前缀、RPATH 策略
set(...) - 线程库
find_package(Threads REQUIRED) - 共享变量下发到子目录
set(... CACHE INTERNAL ...) - 顶层资源/依赖的
install(DIRECTORY ...) add_subdirectory(...)引入子工程
10.(可选)便捷构建目标add_custom_target(copy_...)
子目录(例如 src/imu/CMakeLists.txt):
add_executable(imu ...)target_include_directories(imu ...)target_link_libraries(imu ...)install(TARGETS imu RUNTIME DESTINATION bin)
5.(可选)add_custom_command(TARGET imu POST_BUILD ...)
还是需要强调,这些惯例只作推荐,并非绝对
我不再逐条解释这些api的使用,这实在没意思,需要了直接搜就好了,我们只需要知道在每一步需要用什么api即可,形象的记忆比抽象的事情容易的多
更多推荐


所有评论(0)