CMake 技术学习笔记

版本:v1.0 更新时间:2026-03-30 说明:CMake 学习过程中的技术总结和最佳实践


目录

  1. TARGET_OBJECTS 依赖机制
  2. 静态库构建机制
  3. add_dependencies 与 target_link_libraries
  4. OBJECT 库与 STATIC 库对比
  5. CMake 构建阶段
  6. 生成器表达式
  7. 依赖管理最佳实践

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)
打包时机 构建时 运行时
对象文件要求 必须全部存在 延迟解析
依赖解析 立即解析 运行时解析
文件大小 较大 较小
共享性 不共享 可共享

作用:建立链接关系和传递链接依赖

语法:

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

主要工作:

  1. 解析 CMakeLists.txt
  2. 处理生成器表达式
  3. 建立目标依赖关系图
  4. 生成构建文件(Makefile、build.ninja 等)

关键变量求值:

  • PROJECT_SOURCE_DIR
  • CMAKE_CURRENT_SOURCE_DIR
  • CMAKE_BINARY_DIR
  • TARGET 相关变量

生成器表达式求值:

$<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. 参考资源


文档版本:v1.0 最后更新:2026-03-30 基于 CMake 3.16+