CMake 技术学习笔记
版本:v1.0 更新时间:2026-03-30 说明:CMake 学习过程中的技术总结和最佳实践
目录
- TARGET_OBJECTS 依赖机制
- 静态库构建机制
- add_dependencies 与 target_link_libraries
- OBJECT 库与 STATIC 库对比
- CMake 构建阶段
- 生成器表达式
- 依赖管理最佳实践
1. TARGET_OBJECTS 依赖机制
1.1 基本概念
$<TARGET_OBJECTS:target_name> 是 CMake 生成器表达式,用于传递 OBJECT 库的对象文件。
语法:
target_link_libraries(target_name
PRIVATE
$<TARGET_OBJECTS:object_lib>>
1.2 TARGET_OBJECTS 的作用
作用 1:源文件传递
在 CMake 配置阶段,将 object_lib 的源文件添加到 target_name:
# 定义 OBJECT 库
add_library(opbase_util_objs OBJECT
${OPBASE_SOURCE_PATH}/src/op_common/util/*.cpp
${OPBASE_SOURCE_PATH}/src/op_common/log/*.cpp
)
# 使用 TARGET_OBJECTS 传递源文件
target_link_libraries(${GRAPH_PLUGIN_NAME}_obj
PRIVATE
$<$<TARGET_EXISTS:opbase_util_objs>:$<TARGET_OBJECTS:opbase_util_objs>>
)
作用 2:对象文件链接
在链接阶段,包含 object_lib 编译后的 .o 文件:
# 实际链接命令(简化)
gcc -o output \
${GRAPH_PLUGIN_NAME}_obj 的 .o 文件 \
opbase_util_objs 的 .o 文件
1.3 重要特性
特性 1:不建立构建依赖
target_link_libraries(A PRIVATE $<TARGET_OBJECTS:B>)
# ✅ 传递 B 的源文件到 A
# ✅ 链接时包含 B 的对象文件
# ❌ 不保证 B 的对象文件先编译完成
特性 2:需要显式依赖
# 正确做法
if(TARGET B)
target_link_libraries(A PRIVATE $<TARGET_OBJECTS:B>)
add_dependencies(A B) # 显式建立构建依赖
endif()
1.4 技术细节
生成器表达式求值时机:
| 阶段 | TARGET_OBJECTS 行为 |
|---|---|
| 配置阶段 | 求值,获取源文件列表 |
| 构建阶段 | 链接时使用对象文件 |
| 依赖关系 | 不自动建立 |
源文件传递机制:
opbase_util_objs (OBJECT 库)
├─ log.cpp
├─ util.cpp
└─ ...
使用 $<TARGET_OBJECTS:opbase_util_objs> 后:
${GRAPH_PLUGIN_NAME}_obj (OBJECT 库)
├─ 自己的源文件
└─ log.cpp, util.cpp (从 opbase_util_objs 传递)
2. 静态库构建机制
2.1 静态库定义
基本语法:
add_library(target_name STATIC
source1.cpp
source2.cpp
)
2.2 静态库打包过程
打包时机:在所有依赖的对象文件编译完成后
打包命令(以 ar 为例):
ar rcs libstatic_lib.a
object1.o
object2.o
...
打包要求:
- ✅ 所有
.o文件必须存在 - ✅ 所有
.o文件必须编译完成 - ❌ 不允许缺失任何对象文件
2.3 静态库依赖 OBJECT 库
# 定义 OBJECT 库
add_library(opbase_util_objs OBJECT
log.cpp
util.cpp
)
# 定义静态库,依赖 OBJECT 库
add_library(${OP_GRAPH_MODULE_NAME}_static_lib STATIC)
target_link_libraries(
${OP_GRAPH_MODULE_NAME}_static_lib PRIVATE
$<TARGET_OBJECTS:opbase_util_objs>
)
打包流程:
1. 编译 opbase_util_objs 的源文件
→ log.o
→ util.o
2. 编译 static_lib 的其他源文件
3. 打包静态库
ar rcs libstatic_lib.a
log.o
util.o
...
2.4 静态库与动态库对比
| 特性 | 静态库 (.a) | 动态库 (.so) |
|---|---|---|
| 打包时机 | 构建时 | 运行时 |
| 对象文件要求 | 必须全部存在 | 延迟解析 |
| 依赖解析 | 立即解析 | 运行时解析 |
| 文件大小 | 较大 | 较小 |
| 共享性 | 不共享 | 可共享 |
3. add_dependencies 与 target_link_libraries
3.1 target_link_libraries
作用:建立链接关系和传递链接依赖
语法:
target_link_libraries(target_name
PRIVATE|PUBLIC|INTERFACE
item1
item2
...
)
链接类型说明:
| 关键字 | 含义 | 依赖传递 |
|---|---|---|
| PRIVATE | 仅当前目标链接 | 不传递 |
| PUBLIC | 当前目标和依赖者都链接 | 传递给依赖者 |
| INTERFACE | 仅依赖者链接 | 仅传递给依赖者 |
示例:
# 链接静态库
target_link_libraries(myapp PRIVATE libmath.a)
# 链接系统库
target_link_libraries(myapp PRIVATE pthread dl)
# 链接 OBJECT 库
target_link_libraries(myapp PRIVATE $<TARGET_OBJECTS:obj_lib>>)
建立依赖关系:
- ✅ 建立构建依赖:如果 B 是库目标,A 会等待 B 构建完成
- ✅ 建立链接依赖:A 会链接 B 的输出
- ✅ 传递使用要求:包含目录、编译选项等
3.2 add_dependencies
作用:显式建立构建顺序依赖
语法:
add_dependencies(target_name
dependency1
dependency2
...
)
特性:
- ✅ 确保在构建
target_name之前,所有dependency已构建完成 - ✅ 不影响链接关系
- ✅ 仅控制构建顺序
与 target_link_libraries 的区别:
| 特性 | add_dependencies | target_link_libraries |
|---|---|---|
| 建立构建顺序 | ✅ 是 | ✅ 是(如果是库目标) |
| 建立链接关系 | ❌ 否 | ✅ 是 |
| 传递使用要求 | ❌ 否 | ✅ 是 |
| 适用目标 | 任意目标 | 库目标、可执行文件 |
3.3 使用场景
场景 1:链接库文件
# 使用 target_link_libraries
target_link_libraries(myapp PRIVATE libcurl.a)
场景 2:传递对象文件(需要额外依赖)
# 需要同时使用两种机制
if(TARGET opbase_util_objs)
target_link_libraries(myobj PRIVATE $<TARGET_OBJECTS:opbase_util_objs>>)
add_dependencies(myobj opbase_util_objs)
endif()
场景 3:自定义目标依赖
# 使用 add_dependencies 控制构建顺序
add_custom_command(
OUTPUT generated.cpp
COMMAND python generate.py
)
add_custom_target(generate_cpp
DEPENDS generated.cpp
)
add_library(mylib STATIC generated.cpp)
add_dependencies(mylib generate_cpp)
4. OBJECT 库与 STATIC 库对比
4.1 定义方式
OBJECT 库:
add_library(obj_lib OBJECT
source1.cpp
source2.cpp
)
STATIC 库:
add_library(static_lib STATIC
source1.cpp
source2.cpp
)
4.2 输出产物
| 库类型 | 输出文件 | 文件内容 | 位置 |
|---|---|---|---|
| OBJECT | .o 文件 | 对象文件(未打包) | 构建目录 CMakeFiles/xxx.dir/ |
| STATIC | .a 文件 | 归档的对象文件 | 构建目录或指定位置 |
4.3 使用特点
OBJECT 库特点:
| 特性 | 说明 |
|---|---|
| 源文件容器 | 本身不产生输出文件 |
| 可传递性 | 可通过 $<TARGET_OBJECTS:> 传递源文件 |
| 编译时机 | 延迟到使用它的目标 |
| 多次使用 | 源文件可被多个目标使用 |
STATIC 库特点:
| 特性 | 说明 |
|---|---|
| 归档文件 | 产生 .a 文件 |
| 链接方式 | 通过 -l 或直接指定 .a 文件 |
| 编译时机 | 打包时编译所有源文件 |
| 多次使用 | .a 文件可被多个目标链接 |
4.4 源文件传递机制
OBJECT 库传递:
# 定义两个 OBJECT 库
add_library(obj1 OBJECT source1.cpp)
add_library(obj2 OBJECT source2.cpp)
# 使用 TARGET_OBJECTS 传递
add_library(combined_obj OBJECT)
target_link_libraries(combined_obj PRIVATE
$<TARGET_OBJECTS:obj1>
$<TARGET_OBJECTS:obj2>
)
# combined_obj 拥有 source1.cpp 和 source2.cpp
STATIC 库打包:
# 定义 OBJECT 库
add_library(util_objs OBJECT util.cpp)
# 定义静态库
add_library(mylib STATIC main.cpp)
target_link_libraries(mylib PRIVATE $<TARGET_OBJECTS:util_objs>)
# 打包时包含 main.o 和 util.o
4.5 性能考虑
编译速度:
- OBJECT 库:源文件在多个目标中共享,减少重复编译
- STATIC 库:每次打包时需要所有源文件
链接速度:
- OBJECT 库:直接包含对象文件
- STATIC 库:需要解析归档文件
灵活性:
- OBJECT 库:更高,可细粒度控制
- STATIC 库:较低,但更易用
5. CMake 构建阶段
5.1 配置阶段
时间:运行 cmake .. 或 cmake -S . -B build
主要工作:
- 解析 CMakeLists.txt
- 处理生成器表达式
- 建立目标依赖关系图
- 生成构建文件(Makefile、build.ninja 等)
关键变量求值:
PROJECT_SOURCE_DIRCMAKE_CURRENT_SOURCE_DIRCMAKE_BINARY_DIRTARGET相关变量
生成器表达式求值:
$<TARGET_EXISTS:target_name>
$<TARGET_OBJECTS:target_name>
$<BUILD_INTERFACE:xxx>
$<INSTALL_INTERFACE:xxx>
5.2 生成阶段
时间:部分生成器表达式求值
内容:
- 根据配置阶段的结果生成具体命令
- 确定编译选项、链接选项
- 生成依赖关系
示例:
target_compile_options(mylib
PRIVATE
$<$<CONFIG:Debug>:-g -O0>
$<$<CONFIG:Release>:-O3>
)
5.3 构建阶段
时间:运行 cmake --build . 或 make
工作流程:
1. 读取构建文件(Makefile 或 build.ninja)
2. 解析依赖关系
3. 按拓扑排序执行构建命令
├─ 编译源文件
├─ 打包静态库
├─ 链接动态库
└─ 链接可执行文件
依赖顺序:
# 定义依赖链
add_library(obj1 OBJECT source1.cpp)
add_library(obj2 OBJECT source2.cpp)
add_dependencies(obj2 obj1)
add_library(static_lib STATIC)
target_link_libraries(static_lib PRIVATE $<TARGET_OBJECTS:obj2>)
add_dependencies(static_lib obj2)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE static_lib)
# 构建顺序:
# 1. obj1 (source1.cpp → source1.o)
# 2. obj2 (source2.cpp → source2.o, 等待 obj1)
# 3. static_lib (等待 obj2, 打包 source1.o 和 source2.o)
# 4. app (等待 static_lib, 链接 main.o 和 static_lib.a)
5.4 阶段对比
| 阶段 | 输入 | 输出 | 关键操作 |
|---|---|---|---|
| 配置 | CMakeLists.txt | 构建文件 | 解析、求值、生成 |
| 生成 | 构建文件 | 构建命令 | 条件判断、命令生成 |
| 构建 | 构建命令 | 二进制文件 | 编译、链接、打包 |
6. 生成器表达式
6.1 基本语法
格式:$<条件:真值>
简单示例:
$<CONFIG:Debug> # 如果是 Debug 配置
$<TARGET_EXISTS:tgt> # 如果目标存在
$<BOOL:var> # 如果变量为真
6.2 常用生成器表达式
条件判断:
# 配置判断
$<CONFIG:Debug>
$<CONFIG:Release>
# 目标存在性
$<TARGET_EXISTS:target_name>
# 布尔值
$<BOOL:variable_name>
# 字符串比较
$<STREQUAL:${var},${value}>
$<MATCHES:${pattern},${string}>
路径相关:
# 构建接口
$<BUILD_INTERFACE:xxx>
# 安装接口
$<INSTALL_INTERFACE:xxx>
# 目标属性
$<TARGET_PROPERTY:tgt,prop>
目标对象:
# 对象文件
$<TARGET_OBJECTS:target_name>
# 目标文件(如果是库)
$<TARGET_FILE:target_name>
# 目标文件名
$<TARGET_FILE_NAME:target_name>
6.3 条件逻辑
逻辑与:
# 方式 1:嵌套
$<$<CONDITION1>:and_result>
$<$<CONDITION2:$<CONDITION1:and_result>>
# 方式 2:使用 AND 连接(CMake 3.8+)
$<AND:$<CONDITION1>,$<CONDITION2>>
逻辑或:
# CMake 3.8+
$<OR:$<CONDITION1>,$<CONDITION2>>
# 早期版本
$<$<CONDITION1>:result1>$<$<NOT:$<CONDITION1>>:<result2>>
逻辑非:
$<NOT:CONDITION>
6.4 实际应用
应用 1:条件编译选项:
target_compile_options(mylib
PRIVATE
$<$<CONFIG:Debug>:-g -O0 -DDEBUG>
$<$<CONFIG:Release>:-O3 -DNDEBUG>
)
应用 2:条件链接:
target_link_libraries(myapp
PRIVATE
$<$<TARGET_EXISTS:optional_lib>:optional_lib>
)
应用 3:传递使用要求:
target_link_libraries(mylib
PUBLIC
$<BUILD_INTERFACE:includedir>
$<INSTALL_INTERFACE:includedir>
)
6.5 求值时机
| 表达式类型 | 求值时机 | 次数 |
|---|---|---|
| 目标存在性 | 配置阶段 | 1 次 |
| 配置类型 | 生成阶段 | 每种配置 1 次 |
| 条件逻辑 | 使用时 | 每次 |
| TARGET_OBJECTS | 生成阶段 | 1 次 |
7. 依赖管理最佳实践
7.1 OBJECT 库使用
最佳实践 1:使用 TARGET_OBJECTS 时添加显式依赖
# 推荐做法
if(TARGET opbase_util_objs)
target_link_libraries(
${GRAPH_PLUGIN_NAME}_obj
PRIVATE
$<TARGET_OBJECTS:opbase_util_objs>>
add_dependencies(${GRAPH_PLUGIN_NAME}_obj opbase_util_objs)
endif()
# 或者始终添加
add_dependencies(${GRAPH_PLUGIN_NAME}_obj opbase_util_objs)
最佳实践 2:检查目标存在性
# 使用生成器表达式
target_link_libraries(A
PRIVATE
$<$<TARGET_EXISTS:B>:$<TARGET_OBJECTS:B>>
)
# 或者在 CMake 代码中检查
if(TARGET B)
target_link_libraries(A PRIVATE $<TARGET_OBJECTS:B>)
endif()
7.2 静态库依赖管理
最佳实践 3:分层依赖
# 第 1 层:基础 OBJECT 库
add_library(base_objs OBJECT base.cpp)
# 第 2 层:中间 OBJECT 库
add_library(mid_objs OBJECT mid.cpp)
target_link_libraries(mid_objs PRIVATE $<TARGET_OBJECTS:base_objs>)
add_dependencies(mid_objs base_objs)
# 第 3 层:静态库
add_library(mylib STATIC)
target_link_libraries(mylib PRIVATE $<TARGET_OBJECTS:mid_objs>)
add_dependencies(mylib mid_objs)
# 第 4 层:可执行文件
add_executable(app main.cpp)
target_link_libraries(app PRIVATE mylib)
add_dependencies(app mylib)
最佳实践 4:避免循环依赖
# ❌ 错误:循环依赖
add_library(A OBJECT a.cpp)
target_link_libraries(A PRIVATE $<TARGET_OBJECTS:B>>)
add_library(B OBJECT b.cpp)
target_link_libraries(B PRIVATE $<TARGET_OBJECTS:A>>)
# ✅ 正确:提取公共部分
add_library(common OBJECT common.cpp)
add_library(A OBJECT a.cpp)
target_link_libraries(A PRIVATE $<TARGET_OBJECTS:common>>)
add_library(B OBJECT b.cpp)
target_link_libraries(B PRIVATE $<TARGET_OBJECTS:common>>)
7.3 接口库设计
最佳实践 5:使用 INTERFACE 传递
# 定义接口库
add_library(mylib_interface INTERFACE)
target_include_directories(mylib_interface
INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/include
)
target_link_libraries(mylib_interface
INTERFACE
dependency_lib
)
# 使用接口库
add_library(mylib_impl STATIC impl.cpp)
target_link_libraries(mylib_impl
PUBLIC
mylib_interface
)
# 依赖者自动继承 include 目录和依赖
target_link_libraries(app PRIVATE mylib_impl)
7.4 现代化 CMake 实践
推荐用法(CMake 3.16+):
# 1. 使用目标属性
set_target_properties(mylib
PROPERTIES
CXX_STANDARD 17
CXX_STANDARD_REQUIRED ON
POSITION_INDEPENDENT_CODE ON
)
# 2. 使用生成器表达式
target_compile_options(mylib
PUBLIC
$<$<COMPILE_LANG_AND_ID:CXX,GNU,Clang>:-Wall>
$<$<COMPILE_LANG_AND_ID:CXX,MSVC>:/W4>
)
# 3. 使用别名
add_library(mylib::mylib ALIAS mylib)
# 4. 使用 IMPORTED 目标
add_library(external_lib STATIC IMPORTED)
set_target_properties(external_lib
PROPERTIES
IMPORTED_LOCATION ${EXTERNAL_LIB_PATH}/lib/libexternal.a
)
7.5 调试技巧
查看依赖关系:
# 查看目标的依赖
cmake --build . --target target_name --verbose
# 查看所有依赖图
cmake --graphviz=deps.dot ..
dot -Tpng deps.dot -o deps.png
检查源文件:
# 在 CMakeLists.txt 中打印
get_target_property(sources target_name SOURCES)
message(STATUS "Sources of target_name: ${sources}")
# 或者在构建目录检查
grep -r "log.cpp" build.ninja
验证对象文件:
# 检查 .o 文件
find build -name "*.o" | grep opbase
# 检查构建规则
grep -A 5 "log.cpp.o" build.ninja
附录
A. CMake 版本特性
| 版本 | 重要特性 |
|---|---|
| 3.16 | 当前最低要求版本 |
| 3.18 | 改进的依赖图 |
| 3.20 | 改进的生成器表达式 |
| 3.25 | 最新稳定版 |
B. 常用命令参考
构建命令:
# 配置
cmake -S . -B build
cmake .. -G Ninja
# 构建
cmake --build build
cmake --build . --target target_name
# 清理
cmake --build . --target clean
查询命令:
# 查看缓存
cmake -LA -N build
# 查看属性
get_target_property(var target_name PROPERTY_NAME)
get_cmake_property(var PROPERTY_NAME)
# 查看依赖
cmake --build . --target target_name --verbose
C. 参考资源
- 官方文档: https://cmake.org/cmake/help/latest/
- Modern CMake: https://cliutils.gitlab.io/modern-cmake/
- CMake Practices: https://github.com/cpp-best-practices/cmake_practices
文档版本:v1.0 最后更新:2026-03-30 基于 CMake 3.16+