
🔥草莓熊Lotso:个人主页
❄️个人专栏: 《C++知识分享》 《Linux 入门到实践:零基础也能懂》
✨生活是默默的坚持,毅力是永久的享受!
🎬 博主简介:

文章目录
- 前言:
- 一. 操作目标和属性的核心 API
-
- 1.1 set_target_properties 与 get_target_properties
- 1.2 add_subdirectory:构建层次化工程
- 二、CMake 内部静态库的生成与定位流程
-
- 2.1 四步定位法:从目标注册到链接命令生成
- 2.2 生成器表达式验证:$<TARGET_FILE> 的妙用
- 2.3 实战验证:查看生成的 link.txt 文件
- 三. 现代 CMake 库发布与查找机制
-
- 3.1 为什么需要导入目标?
- 3.2 库发布完整流程:从编译到安装
- 3.3 库查找完整流程:find_package 的工作原理
- 3.4 export 与 install (EXPORT) 的核心区别
- 四、总结与核心考点提炼
- 结尾:
前言:
作为 C/C++ 开发者,你是否遇到过这些问题:写了一个库却不知道怎么让别人用?第三方库的头文件和库文件路径总是配不对?CMake 的 PUBLIC/PRIVATE/INTERFACE 到底有什么区别?很多人学 CMake 只会写简单的add_executable和add_library,但一到实际项目中就会被库的管理、发布和依赖问题卡住。本文将带你深入理解 CMake 最核心的目标 – 属性 – API模型,从库的属性设置到完整的发布流程,再到find_package的工作原理,一次性解决你 90% 的 CMake 工程问题。
本篇飞书笔记
一. 操作目标和属性的核心 API
现代 CMake 的核心设计思想是基于目标 (Target)。每个可执行文件、静态库、动态库都是一个目标,目标拥有一系列属性 (Property),CMake 通过这些属性来控制编译、链接、安装等整个构建过程。
1.1 set_target_properties 与 get_target_properties
这两个函数是操作目标属性的最底层 API,几乎所有target_*系列函数最终都是通过它们来实现的。
基本语法
# 设置一个或多个目标的多个属性
set_target_properties(<target1> <target2> ...
PROPERTIES
<prop1> <value1>
<prop2> <value2>
...
)
# 获取单个目标的单个属性值
get_target_property(<variable> <target> <property>)
常用属性对照表
| COMPILE_OPTIONS | 编译选项 | -g -O3 -fPIC等 |
| INCLUDE_DIRECTORIES | 自身编译时的头文件搜索路径 | -I |
| INTERFACE_INCLUDE_DIRECTORIES | 下游使用者需要的头文件搜索路径 | -I |
| LINK_LIBRARIES | 自身链接时需要的库列表 | -l |
| INTERFACE_LINK_LIBRARIES | 下游使用者需要链接的库列表 | -l |
| LIBRARY_OUTPUT_DIRECTORY | 库文件输出目录 | – |
| OUTPUT_NAME | 输出文件的名称 | – |
| VERSION | 库的完整版本号 | – |
| SOVERSION | 库的 API 版本号 | – |
| BUILD_RPATH | 构建目录中的运行时库搜索路径 | -Wl,-rpath |
| INSTALL_RPATH | 安装目录中的运行时库搜索路径 | -Wl,-rpath |
实战代码演示
我们创建一个test_set_target_properties工程,演示如何设置和获取目标属性:
顶层 CMakeLists.txt
cmake_minimum_required(VERSION 3.18)
project(TestProp LANGUAGES CXX)
# 添加子目录
add_subdirectory(src)
# 添加可执行文件并链接库
add_executable(main main.cpp)
target_link_libraries(main PRIVATE add)
src/CMakeLists.txt
# 创建动态库目标
add_library(add SHARED add.cpp)
# 设置目标属性
set_target_properties(add PROPERTIES
# 编译类参数
COMPILE_OPTIONS "-g"
COMPILE_OPTIONS "-O3"
COMPILE_OPTIONS "-fPIC"
INCLUDE_DIRECTORIES "/public"
INTERFACE_INCLUDE_DIRECTORIES "/interface"
# 链接类参数
LINK_DIRECTORIES "/public"
INTERFACE_LINK_DIRECTORIES "/interface"
LINK_LIBRARIES "curl"
INTERFACE_LINK_LIBRARIES "jsoncpp"
# 输出类参数
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib"
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib"
# 安装类参数
BUILD_RPATH "${CMAKE_BINARY_DIR}/lib"
INSTALL_RPATH "lib"
OUTPUT_NAME "add"
VERSION "1.2.3"
SOVERSION "20"
)
代码解读:
- COMPILE_OPTIONS设置了编译选项-g -O3 -fPIC,分别用于生成调试信息、开启优化和生成位置无关代码
- INCLUDE_DIRECTORIES是库自身编译时需要的头文件路径,不会传递给下游
- INTERFACE_INCLUDE_DIRECTORIES是下游使用者必须包含的头文件路径,会自动传递
- VERSION和SOVERSION用于生成版本化的库文件,最终会生成libadd.so.1.2.3和libadd.so.20两个符号链接
1.2 add_subdirectory:构建层次化工程
当项目规模变大时,我们需要将代码组织成多个子目录,每个子目录有自己的CMakeLists.txt。add_subdirectory就是 CMake 构建层次结构的 “递归入口”。
基本语法
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
- source_dir:子目录的源代码路径,通常是相对路径
- binary_dir:子目录的构建输出路径,默认与源代码路径同名
- EXCLUDE_FROM_ALL:该子目录的目标不会被默认构建,只有显式指定才会编译
作用域与变量可见性
这是最容易出错的地方!add_subdirectory会创建一个新的目录作用域:
include vs add_subdirectory:核心区别
很多人分不清这两个命令,它们的本质区别在于是否创建新的作用域:
| 作用域 | 继承当前作用域 | 创建新的子目录作用域 |
| 内置变量变化 | CMAKE_CURRENT_SOURCE_DIR不变 | CMAKE_CURRENT_SOURCE_DIR变为子目录 |
| 变量传递 | 直接修改当前作用域变量 | 默认无法修改父目录变量,需用PARENT_SCOPE |
| 典型用途 | 加载工具函数、配置选项 | 构建子模块、库或可执行文件 |
变量变化对比表
使用 add_subdirectory 进入子目录后:
| CMAKE_CURRENT_SOURCE_DIR | ✅ 变化 | 变为子目录的源代码路径 |
| CMAKE_CURRENT_BINARY_DIR | ✅ 变化 | 变为子目录的构建路径 |
| CMAKE_CURRENT_LIST_FILE | ✅ 变化 | 变为子目录的 CMakeLists.txt 路径 |
| CMAKE_CURRENT_LIST_DIR | ✅ 变化 | 变为子目录的 CMakeLists.txt 所在目录 |
使用 include 包含子目录文件后:
| CMAKE_CURRENT_SOURCE_DIR | ❌ 不变 | 还是父目录的源代码路径 |
| CMAKE_CURRENT_BINARY_DIR | ❌ 不变 | 还是父目录的构建路径 |
| CMAKE_CURRENT_LIST_FILE | ✅ 变化 | 变为被包含文件的路径 |
| CMAKE_CURRENT_LIST_DIR | ✅ 变化 | 变为被包含文件所在目录 |
二、CMake 内部静态库的生成与定位流程
很多人都会好奇:当我在target_link_libraries中写了MyMath,CMake 是怎么知道这个库在build/lib目录下的?
我深入阅读了 CMake 的源代码,现在为你揭秘这个从未在官方文档中详细说明的内部流程。
2.1 四步定位法:从目标注册到链接命令生成
整个过程分为四个清晰的步骤:
第一步:目标生成 当你写下add_library(MyMath STATIC add.cpp sub.cpp)时,CMake 会在全局的Targets容器中注册一个名为MyMath的cmTarget对象。
第二步:目标信息存储 每个cmTarget对象内部会存储:
- 目标名称和类型 (静态库 / 动态库 / 可执行文件)
- 源文件列表
- 所有属性 (包括输出路径、编译选项等)
- 依赖关系
当你使用set_target_properties修改LIBRARY_OUTPUT_DIRECTORY属性时,CMake 会立即更新这个目标的输出路径信息。
第三步:生成器阶段 – 定位静态库路径 配置阶段结束后,CMake 进入生成阶段。生成器会遍历所有目标,根据目标的属性和平台规则,推导出每个目标的实际输出路径。例如:
- 静态库:build/lib/libMyMath.a
- 动态库:build/lib/libMyMath.so.1.2.3
第四步:链接命令的生成 生成器会为每个目标生成链接规则,直接将库的完整路径写入链接命令中:
g++ main.o -o myexe build/lib/libMyMath.a
这样,链接器就能直接找到并链接对应的库文件,不需要额外的-L参数。
2.2 生成器表达式验证:$<TARGET_FILE> 的妙用
我们可以使用生成器表达式$<TARGET_FILE:目标名>来验证 CMake 计算出的库路径。生成器表达式是 CMake 中一种特殊的语法,在生成阶段(而不是配置阶段) 动态计算值。
修改app/CMakeLists.txt,添加以下代码:
add_custom_command(
TARGET main POST_BUILD
COMMAND ${CMAKE_COMMAND} -E echo "$<TARGET_FILE:MyMath>"
COMMENT "获取静态库的输出路径"
)
编译后你会看到类似这样的输出:
[ 80%] Linking CXX executable ../bin/main
获取静态库的输出路径
/home/bit/workspace/CMakeClass/my_math/build/lib/libMyMath.a
[100%] Built target main
2.3 实战验证:查看生成的 link.txt 文件
CMake 会将每个目标的链接命令保存在CMakeFiles/目标名.dir/link.txt文件中。我们可以直接查看这个文件来验证上面的结论:
cat app/CMakeFiles/main.dir/link.txt
输出内容大致如下:
/usr/bin/c++ -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.4.sdk
-mmaccosx-version-min=15.3 -Wl,-search_paths_first -Wl,-headerpad_max_install_names
CMakeFiles/main.dir/main.cpp.o -o ../bin/main ../lib/libMyMath.a
可以看到,链接命令中直接使用了静态库的完整路径../lib/libMyMath.a,这就是 CMake 能够自动定位内部库的秘密。
三. 现代 CMake 库发布与查找机制
当你写了一个很棒的库,想要分享给团队其他人使用时,就需要了解现代 CMake 的库发布和查找机制。
3.1 为什么需要导入目标?
在传统的 CMake 中,我们使用find_library和find_path来查找库文件和头文件,然后手动设置INCLUDE_DIRS和LIBRARIES变量。这种方式有很多缺点:
- 容易出错,路径配置繁琐
- 无法传递依赖关系
- 没有版本检查
- 不支持编译选项和定义的传递
现代 CMake 引入了 ** 导入目标 (IMPORTED Target)** 的概念,完美解决了这些问题。导入目标就像一个远程的普通目标,拥有所有必要的属性 (头文件路径、依赖库、编译选项等),使用起来和项目内部的目标完全一样。
3.2 库发布完整流程:从编译到安装
我们以一个数学库MyMath为例,演示完整的库发布流程。
步骤 1:编译库并设置使用要求
# 收集源代码
file(GLOB SRC_LISTS "src/*.cpp")
# 添加静态库目标
add_library(MyMath STATIC ${SRC_LISTS})
# 设置库的使用要求(下游消费者必须包含的头文件路径)
target_include_directories(MyMath INTERFACE
"$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>"
)
# 设置库的输出路径
set_target_properties(MyMath PROPERTIES
ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib
)
关键解读:
- $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>是一个生成器表达式,在安装时会被解析为/usr/local/include(默认安装前缀)
- 使用INTERFACE关键字表示这个属性只对下游使用者有效,库自身编译时不需要
步骤 2:安装库文件与头文件
include(GNUInstallDirs)
# 安装静态库
install(TARGETS MyMath
EXPORT MyMathTargets
DESTINATION ${CMAKE_INSTALL_LIBDIR} # 默认为lib
)
# 安装头文件
install(DIRECTORY include/
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/math # 安装到/usr/local/include/math
FILES_MATCHING PATTERN "*.h"
)
关键解读:
- EXPORT MyMathTargets将MyMath目标添加到名为MyMathTargets的导出集合中
- GNUInstallDirs模块定义了各个平台下的标准安装目录,遵循 GNU 规范
步骤 3:导出目标到构建树
# 导出目标集合到构建树,供其他项目直接从构建目录使用
export(EXPORT MyMathTargets
FILE ${CMAKE_CURRENT_BINARY_DIR}/MyMathTargets.cmake
)
这一步会在构建目录生成MyMathTargets.cmake文件,包含了从构建树使用这个库的所有信息。
步骤 4:导出目标到安装树
# 导出目标集合到安装树,供安装后使用
install(EXPORT MyMathTargets
FILE MyMathTargets.cmake
NAMESPACE MyMath:: # 添加命名空间,避免命名冲突
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyMath # 安装到/usr/local/lib/cmake/MyMath
)
这一步会在安装时生成MyMathTargets.cmake文件,包含了从安装树使用这个库的所有信息。
步骤 5:生成并安装 Config.cmake
find_package需要找到一个名为MyMathConfig.cmake的文件才能工作。我们使用configure_package_config_file来生成这个文件:
首先创建一个模板文件Config.cmake.in:
@PACKAGE_INIT@
include(${CMAKE_CURRENT_LIST_DIR}/MyMathTargets.cmake)
然后在 CMakeLists.txt 中添加:
include(CMakePackageConfigHelpers)
# 生成Config.cmake文件
configure_package_config_file(
${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/MyMathConfig.cmake
INSTALL_DESTINATION "lib/cmake/MyMath"
)
# 安装Config.cmake文件
install(FILES
${CMAKE_CURRENT_BINARY_DIR}/MyMathConfig.cmake
DESTINATION "lib/cmake/MyMath"
)
3.3 库查找完整流程:find_package 的工作原理
当你在另一个项目中使用find_package(MyMath CONFIG REQUIRED)时,CMake 会执行以下步骤:
add_executable(main main.cpp)
target_link_libraries(main PRIVATE MyMath::MyMath)
3.4 export 与 install (EXPORT) 的核心区别
这两个命令是最容易混淆的,它们的核心区别如下:
| 触发时机 | 配置 / 生成阶段立即写文件 | 只有执行cmake –install时才写文件 |
| 写入位置 | 由FILE参数指定,通常在构建目录 | 安装到系统标准路径,如/usr/local/lib/cmake/ |
| 内容差异 | 针对构建树,路径指向 build 目录 | 针对安装树,路径指向安装目录 |
| 典型用途 | 开发阶段,其他项目直接从构建目录使用 | 发布阶段,安装后供其他项目使用 |
四、总结与核心考点提炼
本文我们深入学习了 CMake 从目标属性设置到库发布与查找的完整流程,核心要点总结如下:
结尾:
🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点:
👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长
❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量
⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用
💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑
🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解
技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标!
结语:CMake 是 C/C++ 开发者必须掌握的工具,但很多人只停留在 “能用” 的阶段,没有深入理解其背后的设计思想。现代 CMake 基于目标的设计思想,让工程管理变得更加优雅和可维护。学习 CMake 最好的方式是实践,建议你按照本文的步骤,亲手创建一个库并发布,然后在另一个项目中使用它。只有在实际项目中不断踩坑,才能真正掌握 CMake 的精髓。如果你觉得本文对你有帮助,欢迎点赞收藏,也可以在评论区留下你的问题和经验,我们一起交流学习。
✨把这些内容吃透超牛的!放松下吧✨
ʕ˘ᴥ˘ʔ
づきらど



