
本文基于实战场景,从最简单的可执行文件构建出发,逐步拆解 CMake 核心指令与变量,让你彻底理解「从源码到可执行程序」的完整流程。
📌 target_include_directories() ------ CMake 头文件管理的核心 API
target_include_directories 是 CMake 中最基础、最重要的头文件管理 API。
它的核心作用是:为指定的目标(Target)添加头文件搜索路径(Include Path) 。
在官方文档中,它的基本形式如下:
bash
target_include_directories(<target>
[SYSTEM] [BEFORE]
<INTERFACE|PUBLIC|PRIVATE> [items1...]
[<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])
<target>:目标名,可以是add_executable创建的可执行目标,也可以是add_library创建的库目标。INTERFACE/PUBLIC/PRIVATE:属性传递的核心关键字,用于控制头文件路径是否传递给依赖者。[items...]:具体的头文件路径(可以是绝对路径,也可以是相对路径,通常相对于当前源码目录)。
这个函数并不是凭空生成 -I 参数,而是在操作目标的属性(Property)。
关于属性上一篇我们应该是有比较清晰的认知了,下面走一遍流程:
1. 写入属性
当我们调用:
bash
target_include_directories(MyMath PUBLIC include)
CMake 内部会自动将 include 这个路径,写入到目标 MyMath 的属性列表中。对应底层属性:
INTERFACE_INCLUDE_DIRECTORIES:存储通过PUBLIC或INTERFACE设置的路径。BUILD_INTERFACE:存储在构建树内的路径(如${CMAKE_CURRENT_BINARY_DIR})。
2. 传递机制(触发点)
属性传递发生在建立依赖的那一刻。当在可执行目标中链接库时:
bash
target_link_libraries(main PRIVATE MyMath)
CMake 会自动检测 MyMath 的 INTERFACE_INCLUDE_DIRECTORIES 属性,并将其传递 给 main 目标。
3. 最终生成
在构建阶段,CMake 会遍历所有目标的属性,生成对应的编译器参数。例如,main 目标拿到了 MyMath 的头文件路径后,gcc 编译时会自动添加:
-I /path/to/my_lib/include
实战案例:jsoncpp 中的落地用法
下面根据具体的 jsoncpp 案例,我们看这个函数在真实项目中是如何配合导入目标(Imported Target) 和 属性传递 工作的。
场景:引入系统安装好的 jsoncpp
假设 jsoncpp 已经安装到了 /usr/local,我们需要在项目中使用它。
1. 找到并创建导入目标(Imported Target) 首先,我们用 find_package 找到它,本质上是在内部创建了一个导入目标 JsonCpp::JsonCpp。
2. 配置接口属性(关键步骤) 在 jsoncpp 的配置脚本中,它内部是这样写的:
bash
# 创建一个接口库目标
add_library(JsonCpp::JsonCpp INTERFACE IMPORTED)
# 设置接口属性:告诉使用者头文件在哪里
set_target_properties(JsonCpp::JsonCpp PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "/usr/local/include"
)
注:target_include_directories(JsonCpp::JsonCpp INTERFACE "/usr/local/include") 效果是一样的。
3. 链接与属性传递
我们在自己的项目中进行:
bash
target_link_libraries(my_app PRIVATE JsonCpp::JsonCpp)
发生了什么?
- 因为
JsonCpp::JsonCpp是INTERFACE库,它本身不编译。 - 但它设置了
INTERFACE_INCLUDE_DIRECTORIES。 - 当
my_app链接它时,属性传递机制 自动把/usr/local/include传给my_app。 - 编译器就能找到
json/json.h了。
这个函数彻底替代了传统的 include_directories(全局变量),实现了模块化、作用域清晰 的现代构建规范。
我们后续编写如果可以,我们是更推荐现代写法,也就是 TARGET* 这一类的设置目标属性,追求最小范围原则!
理解了 target_include_directories 如何管理头文件路径和属性传递后,我们发现链接库 的行为是整个机制的触发器。下面,我们将深入讲解 target_link_libraries(),看它是如何建立依赖关系、如何触发属性传递、以及如何将静态库 / 动态库 / 导入库真正连接到我们的项目中的。
📌 target_link_libraries() ------ CMake 依赖管理与属性传递的核心引擎
在上一节我们深入讲解了 target_include_directories() 如何管理头文件路径,而真正触发属性传递 、建立依赖关系 的核心 API,正是 target_link_libraries()。它不仅是 "链接库" 的命令,更是现代 CMake 构建系统中依赖传播与配置传递的中枢。
target_link_libraries() 的核心作用有两个:
- 设置目标的依赖库列表 :告诉 CMake 当前目标需要链接哪些库,最终在编译器参数中以
-l形式体现。 - 建立依赖传播链路 :让被依赖目标的
INTERFACE/PUBLIC属性(如头文件路径、链接选项、宏定义等),沿着依赖链自动传递给当前目标。
它的基本形式如下:
bash
target_link_libraries(<target>
<INTERFACE|PUBLIC|PRIVATE> [item...]
[<INTERFACE|PUBLIC|PRIVATE> [item...] ...]
)
<target>:要配置的目标(可执行文件或库)。<INTERFACE|PUBLIC|PRIVATE>:控制依赖与属性的传递范围,和target_include_directories()语义完全一致。[item...]:依赖项,可以是库目标名、导入目标名(如CURL::libcurl)或系统库名(如pthread)。
底层原理:
target_link_libraries()本质是在操作目标的LINK_LIBRARIES和INTERFACE_LINK_LIBRARIES两个向量,把依赖库名和属性写入其中。
我们下面看看完整流程:从库发布到项目使用(以 CURL 为例)
我们来拆解 target_link_libraries() 触发属性传递的完整生命周期:
第一步:库发布者 ------ 定义导入目标与传播属性
库作者在发布时,会通过 Config.cmake 脚本创建导入目标(Imported Target) ,并设置 INTERFACE 属性,告诉使用者 "你用我时必须满足这些要求":
bash
# 创建导入目标:CURL::libcurl_static
add_library(CURL::libcurl_static STATIC IMPORTED)
# 设置接口属性:告诉使用者需要链接的依赖、头文件路径、宏定义
set_target_properties(CURL::libcurl_static PROPERTIES
INTERFACE_COMPILE_DEFINITIONS "CURL_STATICLIB"
INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include"
INTERFACE_LINK_LIBRARIES "\$<LINK_ONLY:OpenSSL::Crypto>;\$<LINK_ONLY:ZLIB::ZLIB>;..."
)
INTERFACE_LINK_LIBRARIES:声明 "用我必须同时链接 OpenSSL、ZLIB 等库"。INTERFACE_INCLUDE_DIRECTORIES:声明 "用我必须把我的头文件路径加进去"。
第二步:库使用者 ------ 建立依赖链路
在我们自己项目中,通过 find_package(CURL) 找到导入目标后,用 target_link_libraries() 建立依赖:
bash
add_executable(main main.cpp)
target_link_libraries(main PRIVATE CURL::libcurl_static)
这一行就是属性传递的触发点 :CMake 检测到 main 依赖 CURL::libcurl_static,开始递归收集所有 INTERFACE 属性。
第三步:CMake 计算完整依赖与属性
CMake 会遍历整个依赖链,递归收集所有 INTERFACE 属性:
- 从
CURL::libcurl_static拿到INTERFACE_INCLUDE_DIRECTORIES和INTERFACE_LINK_LIBRARIES。 - 继续从
OpenSSL::Crypto、ZLIB::ZLIB等依赖中,收集它们的INTERFACE属性。 - 把所有属性合并到
main目标的属性列表中。
第四步:生成最终编译 / 链接参数
CMake 将 main 目标的所有属性,翻译成 gcc 命令行参数:
- 头文件路径:
-I/usr/local/include - 链接库:
-L/usr/local/lib -lcurl -lcrypto -lz - 宏定义:
-DCURL_STATICLIB
下面我们实战对比:PRIVATE vs PUBLIC 的行为差异
我们用一个简单例子直观感受传递效果:
场景 1:库 add 用 PRIVATE 链接 pthread
bash
add_library(add STATIC add.cpp)
target_link_libraries(add PRIVATE pthread)
add_executable(main main.cpp)
target_link_libraries(main PRIVATE add)
add自己链接pthread,但INTERFACE_LINK_LIBRARIES为空。main链接add时,不会自动链接pthread。
场景 2:库 add 用 PUBLIC 链接 pthread
bash
add_library(add STATIC add.cpp)
target_link_libraries(add PUBLIC pthread)
add_executable(main main.cpp)
target_link_libraries(main PRIVATE add)
add自己链接pthread,同时把pthread写入INTERFACE_LINK_LIBRARIES。main链接add时,自动继承pthread依赖 ,CMake 会在main的链接参数里加上-lpthread。
📌 CMake 从 Install 到 Export:库的安装与标准化导出
到这里,我们已经成功创建并配置好了库目标(Target),也能够正常编译生成库文件与可执行程序。
所谓库目标 ,就是我们通过 add_library 定义的编译单元,它包含了库文件、头文件路径、编译选项、依赖关系等所有信息,是 CMake 管理项目的核心单元。
接下来,我们需要学习如何将这些文件 安装(Install)到系统目录中,方便其他项目调用。
一、基础 Install:最朴素的文件安装
按照最直观的理解,install 的作用似乎很简单:将编译好的头文件、库文件、可执行文件复制到系统指定目录 ,比如 /usr/local/include、/usr/local/lib 等位置。这是最基础、最朴素的安装方式,也是我们最先想到的做法。
典型的基础 install 写法如下:
bash
# 安装库文件与可执行文件
install(TARGETS MyLib
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin
)
# 安装头文件
install(DIRECTORY include/
DESTINATION include
)
- install:CMake 安装指令,负责将编译结果复制到指定安装目录。
- TARGETS :表示要安装的是目标(Target) ,也就是我们用
add_library创建的库。 - MyLib:我们定义的库目标名称。
- ARCHIVE DESTINATION lib :静态库 (
.a/.lib)安装到lib目录。 - LIBRARY DESTINATION lib :动态库 (
.so/.dylib)安装到lib目录。 - RUNTIME DESTINATION bin :可执行程序 (如果有)安装到
bin目录。 - DIRECTORY :表示安装的是整个目录,而不是单个文件。
- include/ :源项目中存放公共头文件的目录(末尾
/表示只复制目录内容)。 - DESTINATION include :将头文件安装到系统目录的
include文件夹。
我们首先使用 install(TARGETS) 指令安装项目构建出的库文件与可执行文件,通过 ARCHIVE、LIBRARY、RUNTIME 三个关键字分别指定静态库、动态库、可执行程序的安装位置,将它们统一安装到系统路径下的 lib 目录与 bin 目录。随后,使用 install(DIRECTORY) 指令将项目中的头文件目录 include 整体复制并安装到系统路径的 include 目录中,完成头文件的部署。通过这两步基础安装操作,我们将库的二进制文件和头文件分别放置到系统标准目录,实现了最基础的文件级安装。
上面这段 install 代码所做的事情,就是把文件安装到安装树 中。
安装树 = 安装到系统里的完整目录结构 包括:
- lib
- bin
- include
- (未来还有 cmake 配置目录)
它和我们编译时的构建树(build 目录)是完全分开的。
CMake 的默认安装前缀就是:
bash
CMAKE_INSTALL_PREFIX = /usr/local
我们可以执自行更改的!
执行 make install 后,文件确实会被复制到对应的系统目录。看起来,我们已经完成了 "安装" 这件事。
二、普通 Install 的不足:看似完成,实则不够用
但是,仅仅这样安装,并不足以让库真正被 CMake 项目标准化使用。
这种方式存在明显的短板:
- 只搬运了文件,没有给 CMake 提供任何 "识别信息"
- 其他项目无法自动找到库的位置
- 无法自动处理库的依赖关系
- 无法使用现代 CMake 标准的
find_package方式 - 用户必须手动填写头文件路径、库路径,繁琐且易出错
换句话说:**你把库装进了系统,但 CMake 不认识它。**这就是普通 install 最大的局限。
如果只做了基础 install,没有任何导出配置,用户必须在自己的 CMake 里手写以下所有内容:
bash
# 手动找头文件路径
include_directories(/usr/local/include)
# 手动指定库文件路径
link_directories(/usr/local/lib)
# 手动链接库名字
target_link_libraries(myapp MyLib)
虽然 /usr/local/lib 和 /usr/local/include 属于系统默认搜索路径,使得库文件与头文件可以被编译器间接找到,但这并不意味着 CMake 能够自动识别这个库。普通的 install 操作仅仅完成了文件的拷贝,没有为库提供任何 CMake 可识别的配置信息,因此 find_package 无法找到该库,CMake 也无法自动获取其版本、依赖、编译选项等关键信息。用户依然需要手动填写链接参数,这既不规范也不优雅,更不符合现代 CMake 的设计标准。要让库真正成为 CMake 可识别的标准包,就必须通过 export(导出目标) 生成配置文件,让 CMake 真正 "认识" 这个库。
三、Export 登场:让 CMake 认识你的库
为了解决上面的问题,我们需要引入一个关键概念:导出目标(Export Targets)。
Export 的核心作用只有一句话:把库目标的完整信息(位置、头文件路径、依赖、编译选项)保存成 CMake 可以读取的配置文件。
所以库目标 是 CMake 在构建过程中内部维护的逻辑对象,仅存在于构建系统中,不对应独立的配置文件;而导出目标 则是将库目标的完整信息生成并保存为 .cmake 格式的配置文件,二者最核心的差别,就是这套用于被外部项目识别的 CMake 配置文件 ------ 没有它,库目标只能在当前项目使用,有了它,才能成为可被 find_package 识别和引用的标准库。
这些配置文件会告诉外部项目:
- 库在哪里
- 头文件在哪里
- 需要链接哪些依赖
- 如何正确使用这个库
具体是哪 3 个 .cmake 文件?
XXXTargets.cmake(最核心)
XXXConfig.cmake(入口文件)
XXXConfigVersion.cmake(版本文件)
具体来说就是:
-
MyLibTargets.cmake: 记录库在哪里、头文件路径、依赖、编译选项。→ 由 export 和 install(EXPORT) 生成。
-
MyLibConfig.cmake: CMake 找库时第一个看的入口文件 。→ 告诉 CMake:去加载 MyLibTargets.cmake。
-
MyLibConfigVersion.cmake: 记录库的版本号,用于版本匹配检查。→ 告诉 CMake 当前库是 1.0 / 2.0 等。
库目标:只有 .a/.so/.h 文件,没有 cmake 配置。 导出目标:比库目标多了上面 3 个 .cmake 文件。
export 是 CMake 用来把库目标信息生成 .cmake 配置文件 的指令。它生成的文件可以让别的项目直接使用你的库,不需要安装,不需要 find_package,不需要写路径。
一句话:export = 把库目标变成可共享的 cmake 配置文件
bash
export(EXPORT <export-set>
[NAMESPACE <namespace>]
[FILE <filename>]
[EXPORT_PACKAGE_DEPENDENCIES]
)
EXPORT <export-set>含义:指定从哪个 "导出集合" 里取库目标(这是必须写的,不能省略!)
那什么是导出集合(export set)?
导出集合 = 一个存放库目标的 "名单 / 盒子", 它是在 install(TARGETS) 时创建的:
bash
install(TARGETS MyLib
EXPORT MyLibTargets # <-- 这里创建导出集合,名字叫 MyLibTargets
)
所以:
EXPORT MyLibTargets意思:从 MyLibTargets 这个导出集合里取出库目标
注意:export 不能直接操作库,只能从导出集合拿库!
NAMESPACE <namespace>含义:给导出的库加一个前缀(命名空间) (可选,但强烈推荐写,防止库名冲突)
bash
NAMESPACE MyLib::
效果:别人使用时不再写 MyLib,而是写 MyLib::MyLib,更标准、更安全。
FILE <filename>含义:指定生成的 .cmake 文件保存路径(必须写)
通常写在构建目录(build 文件夹):
bash
FILE ${PROJECT_BINARY_DIR}/MyLibTargets.cmake
生成路径:
bash
你的项目/build/MyLibTargets.cmake
EXPORT_PACKAGE_DEPENDENCIES含义:自动导出库的依赖(可选)
如果你的库依赖 pthread、OpenCV 等,加这个选项,使用方不需要再手动链接依赖,cmake 自动帮你处理 。
我们看看具体实例:
bash
export(
EXPORT MyLibTargets # 从导出集合取目标
NAMESPACE MyLib:: # 命名空间
FILE ${PROJECT_BINARY_DIR}/MyLibTargets.cmake # 生成文件路径
EXPORT_PACKAGE_DEPENDENCIES # 自动导出依赖
)
所以 export 指令到底做了什么?
它只做一件事:把 "导出集合" 里的库目标信息,写入 .cmake 文件,让外部项目可以直接使用。
生成的 MyLibTargets.cmake 里记录了:
- 库路径
- 头文件路径
- 编译选项
- 依赖库
- 版本信息
export 依赖导出集合,没有导出集合,export 无法使用
流程:
- install(TARGETS ... EXPORT 集合名) 创建导出集合
- export(EXPORT 集合名) 从集合生成 cmake 文件
通过 export 指令,我们可以在构建目录中生成 XXXTargets.cmake,让本地项目可以直接引用,无需安装。(啥意思?看下面!)
假设我们的项目目录:
这是我们 export 出来的
bash
MyLib/
├── build/ <-- 构建目录
│ ├── libMyLib.so <-- 编译出来的库
│ └── MyLibTargets.cmake <-- ✅ export 生成在这里!
├── include/
└── src/
我们在 CMake 里写:
bash
export(EXPORT MyLibTargets
FILE ${PROJECT_BINARY_DIR}/MyLibTargets.cmake
)
这句话只在 build 目录生成文件 → 不会安装 → 不去 /usr/local → 和 install 毫无关系
另一个项目想使用它,直接写:
bash
include(/home/xxx/MyLib/build/MyLibTargets.cmake)
target_link_libraries(app MyLib::MyLib)
✅ 能用! ✅ 没安装! ✅ 没去 /usr/local!
这就是:export = 不安装,本地直接用
当我们执行:
bash
make install
这些文件才会去到:
bash
/usr/local/include
/usr/local/lib
/usr/local/bin
但是我们只 install,不 install (EXPORT),那么 /usr/local 下面没有 cmake 文件夹
bash
/usr/local/
├── include/
├── lib/
│ └── libMyLib.so <-- 只有库,没有cmake文件
└── bin/
❌ 所以别人写:
bash
find_package(MyLib)
找不到!因为 没有 MyLibConfig.cmake
那我们如果进行了 install EXPORT 呢?
bash
install(EXPORT MyLibTargets
DESTINATION lib/cmake/MyLib
)
再 make install
现在 /usr/local 变成:
bash
/usr/local/
├── bin/ # 只有可执行文件!比如 mytool、myapp
├── include/ # 头文件
└── lib/ # 静态库 + 动态库 都在这里!
├── libMyLib.a # 静态库 ✔
├── libMyLib.so # 动态库 ✔
└── lib/cmake/MyLib/
├── MyLibTargets.cmake
├── MyLibConfig.cmake
└── MyLibConfigVersion.cmake
✅ 别人现在可以:
find_package(MyLib)
target_link_libraries(app MyLib::MyLib)
四、Install (EXPORT):真正完整、规范的库发布
install 指令真正强大的地方,也正在于它对 EXPORT 的支持。它不仅能安装文件,还能将导出的配置文件一起安装,让库变成一个标准可被发现的 CMake 包。
install (TARGETS) 【安装库目标】
bash
install(TARGETS <target1> <target2> ... # 【必填】指定要安装的库目标/可执行目标(可写多个)
[EXPORT <export-set>] # 【关键】创建导出集合,把目标登记进去,供export/install(EXPORT)使用
[ARCHIVE DESTINATION <dir>] # 安装静态库(.a/.lib)到指定目录,默认前缀/usr/local/lib
[LIBRARY DESTINATION <dir>] # 安装动态库(.so/.dylib)到指定目录,默认前缀/usr/local/lib
[RUNTIME DESTINATION <dir>] # 安装可执行文件到指定目录,默认前缀/usr/local/bin
[PUBLIC_HEADER DESTINATION <dir>] # 安装对外公开的头文件(.h),默认前缀/usr/local/include
[PRIVATE_HEADER DESTINATION <dir>] # 安装内部私有头文件,一般不对外暴露
[CONFIGURATIONS [Debug|Release|...]] # 指定仅在Debug/Release模式下执行安装
[PERMISSIONS permissions...] # 设置安装后文件的权限(如只读、可执行)
)
install (EXPORT) 【安装 cmake 配置文件】
bash
install(EXPORT <export-set> # 【必填】指定要安装的【导出集合】(必须和之前install(TARGETS)定义的名字一致)
FILE <filename>.cmake # 【必填】指定生成的配置文件名(通常为 库名Targets.cmake)
NAMESPACE <namespace>:: # 【可选】给库目标加命名空间前缀(如MyLib::),防止命名冲突
DESTINATION <dir> # 【必填】指定.cmake配置文件的安装路径(标准路径:lib/cmake/库名)
CONFIGURATIONS [Debug|Release...] # 【可选】指定仅对Debug/Release版本执行安装
)
完整流程分为三步:
- 将目标关联到导出集合(桥梁)
bash
install(TARGETS MyLib
EXPORT MyLibTargets
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin
PUBLIC_HEADER DESTINATION include
)
- 导出配置文件到构建目录(开发用)
bash
export(EXPORT MyLibTargets
NAMESPACE MyLib::
FILE ${PROJECT_BINARY_DIR}/MyLibTargets.cmake
)
- 安装导出配置到系统(发布用)
bash
install(EXPORT MyLibTargets
FILE MyLibTargets.cmake
NAMESPACE MyLib::
DESTINATION lib/cmake/MyLib
)
安装完成后,在系统目录中会出现:
bash
lib/cmake/MyLib/
├── MyLibTargets.cmake
├── MyLibConfig.cmake
└── MyLibConfigVersion.cmake
这时,其他项目只需要一行:
bash
find_package(MyLib REQUIRED)
target_link_libraries(app PRIVATE MyLib::MyLib)
CMake 就会自动找到并配置我们的库。
这就是现代 CMake 库开发从安装到导出的完整、规范流程。