在这里插入图片描述

📃个人主页:island1314

⛺️ 欢迎关注:👍点赞 👂🏽留言 😍收藏 💞 💞 💞

  • 生活总是不会一帆风顺,前进的道路也不会永远一马平川,如何面对挫折影响人生走向 – 《人民日报》


上一篇文章 我们用一个简单的hello_world程序演示了如何使用 cmake来构建一个二进制程序。现在继续学习如何使用 cmake 生成和使用一个项目内部的静态库。

在最佳工程实践里,工程规模大一点的工程中,我们往往会把一些比较独立的功能(比如如网络、数据库、算法,或者一些基础组件,redis-client,mysql-client,isoncpp,libcurl)封装为独立的库,由不同的团队来维护,在公司或者开源社区共享,达到复用目的

一、静态库(编译-链接-引用)

下面把 加法 和 减法函数 2个函数 封装成一个 MyMath 的数学静态库,并在 main 二进制里引用我们静态库里的加法和减法函数,来演示下如何使用 CMake 来管理这一场景。

目录结构

lighthouse@VM-8-10-ubuntu:learn5$ tree
.
├── app
│   ├── CMakeLists.txt
│   └── main.cpp
├── build
├── CMakeLists.txt
└── my_lib
    ├── CMakeLists.txt
    ├── include
    │   └── math.h
    └── src
        ├── add.cpp
        └── sub.cpp

5 directories, 7 files

顶层 CMakeLists.txt

cmake_minimum_required(VERSION 3.18)
project(TestMyMath LANGUAGES CXX)

# 添加目录层级
add_subdirectory(my_lib)
add_subdirectory(app)

my_lib/CMakeLists.txt

# 1. 收集库的源代码
file(GLOB SRC_LISTS "src/*.cpp")
# 2. 添加构建目标
add_library(MyMath STATIC ${SRC_LISTS})
# 3. 设置库的使用要求
target_include_directories(MyMath PUBLIC ${CMAKE_CURRENT_LIST_DIR}/include)
# 4. 修改默认的输出路径
set_target_properties(MyMath PROPERTIES ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)

app/CMakeLists.txt

# 1. 收集库的源代码
file(GLOB SRC_LISTS "*.cpp")
# 2. 添加构建目标
add_executable(main ${SRC_LISTS})
# 3. 添加依赖库列表
target_link_libraries(main PRIVATE MyMath)
# 4. 设置输出路径
set_target_properties(main PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)

add.cpp

#include "math.h"
int add(int a, int b){
    return a + b;
}

sub.cpp

#include "math.h"
int add(int a, int b){
    return a - b;
}

math.h

int add(int a, int b);
int sub(int a, int b);

main.cpp

#include <iostream>
#include "math.h"

int main(){
    std::cout << "3 + 4 = " << add(3, 4) << std::endl;
    std::cout << "3 - 4 = " << sub(3, 4) << std::endl;
}

注意:上面的为什么 “math.h” 和 “main.cpp” 并不处于同一目录下,但是为什么可以呢?

因为 #include "math.h" 的查找规则并不是“只能当前目录”,而是:

  1. 先在当前源文件所在目录找;
  2. 如果没找到,再到所有通过 target_include_directories() 给这个目标声明的目录里去找;
  3. 最后才会像 #include <...> 那样去系统默认包含路径。

在我们的工程里:

  • main.cpp 确实和 math.h 不在同一目录;
  • 但可执行目标 main 通过target_link_libraries(main PRIVATE MyMath)把 MyMath 目标链接了进来;
  • MyMath 目标又通过
    target_include_directories(MyMath PUBLIC ${CMAKE_CURRENT_LIST_DIR}/include)
    把 my_lib/include 目录公开出去;
  • 于是 main 自动继承了这个 PUBLIC 包含路径,编译器在命令行里会得到
    -I/home/…/learn5/my_lib/include
    这样头文件就能被找到,跟它们是不是在同一个目录毫无关系。

所以“可以运行” 并不是巧合,而是 CMake 的目标级包含路径传播机制在起作用。

运行结果如下:

lighthouse@VM-8-10-ubuntu:build$ cmake ..
lighthouse@VM-8-10-ubuntu:build$ cmake --build .

lighthouse@VM-8-10-ubuntu:build$ ./bin/main
3 + 4 = 7
3 - 4 = -1

1. CMake 3 大核心

下面是 CMake 的 3 大核心: 目标 — 属性 — API

这三者是 CMake 工作的骨架:

  1. 首先通过 add_... 命令创建目标。
  2. 然后使用 set_target_properties() 等命令为这些目标配置属性,来控制它们的构建行为。
  3. 最后使用 target_... 系列的 API 来建立目标之间的关系(比如链接、头文件依赖),实现模块化构建。
1.1 Target(目标)

目标 是 CMake 的核心。它代表了你需要构建的一切,无论是可执行文件、库(静态或动态)、还是自定义的构建步骤。CMake 的哲学是“先定义目标,再操作目标”。

Target
│ 
├─ 类型 
│ ├─ EXECUTABLE
│ ├─ STATIC / SHARED / MODULE 
│ ├─ OBJECT / INTERFACE 
│ └─ IMPORTED / ALIAS 
│ 
├─ 属性(≈键值表) 
│ ├─ Build impl ─ SOURCES COMPILE_OPTIONS ... 
│ ├─ Usage reqs ─ INTERFACE_INCLUDE_DIRECTORIES ... 
│ ├─ Output/Install ─ VERSION SOVERSION OUTPUT_NAME ... 
│ └─ Meta ─ TYPE ALIASED_TARGET ... 
│ 
├─ API 
│ ├─ add_library / add_executable 
│ ├─ target_* (compile_options, link_libraries, include_dirs) 
│ ├─ set/get_target_properties 
│ └─ install(TARGETS) / export 
│ 
└─ 流程 
 配置期 → ⽬标注册 + 属性写⼊
 ⽣成期 → 属性转具化到 Make/Ninja
 构建期 → 编译 & 链接
 安装期 → 使⽤属性⽣成 cmake_install.cmake
目标类型 (Target Types)

CMake 提供了多种目标类型,每种类型都有特定的用途:

  • EXECUTABLE: 创建一个可执行程序。这是最基本的目标类型,也是项目的最终产品。
    • 示例: add_executable(my_app main.cpp)
  • LIBRARY: 创建库文件。它有三个子类型:
    • STATIC: 创建静态库(.a 或 .lib)。静态库将代码直接嵌入到最终的可执行文件中。
    • SHARED: 创建动态库(.so 或 .dll)。动态库在运行时才被加载,可以被多个程序共享。
    • MODULE: 一种特殊的动态库,用于运行时加载,例如作为插件。它不会被链接到其他目标。
  • OBJECT: 创建目标文件(.o 或 .obj),但不会将它们打包成库或可执行文件。这在某些特殊场景下非常有用,例如当多个目标需要共享相同的对象文件时。
  • INTERFACE: 接口库。它不生成任何文件,只用于传递构建信息,如头文件路径或编译选项。这对于纯头文件库或仅包含接口的库非常有用。
  • IMPORTED: 导入目标。它表示一个在 CMake 项目外部构建的目标。可以通过它来引用系统库或第三方库,而无需在你的项目中重新编译它们。
  • ALIAS: 别名目标。它为已有的目标提供一个新名称,便于在 target_link_libraries 等命令中引用。

2. 目标生命周期(Lifecycle of Targets)

一个目标从定义到最终生成和安装,经历了四个主要阶段:

  1. 配置期(Configure Time): 当运行 cmake 命令时,CMake 读取 CMakeLists.txt 文件,注册所有目标,并解析它们的属性。此时,CMake 只是在内存中建立一个目标对象模型。
  2. 生成期(Generate Time): 配置完成后,CMake 根据你的构建工具(如 Makefile 或 Ninja)生成具体的构建脚本。这个过程会将目标属性具化为 Makefile 或 build.ninja 文件中的具体指令。
  3. 构建期(Build Time): 当运行 cmake --build . 或直接调用 make 等命令时,构建工具读取生成的脚本,并执行实际的编译和链接操作,生成最终的可执行文件和库。
  4. 安装期(Install Time): 当运行 cmake --install . 时,CMake 使用 install() 命令定义的属性,将构建好的目标文件复制到指定的安装目录。
1.2 Property(属性)

属性 是附加在目标、目录或全局上的键值对。它们用来修改目标的行为或存储信息。可以通过设置属性来控制编译选项、链接库、输出路径等。

类比作用域典型读/写命令常用属性示例
全局属性(Global)整个 CMake 运行生命周期get_property(GLOBAL)
set_property(GLOBAL)
CMAKE_ROLE
目录属性(Directory)当前源码目录及其子目录get_property(DIRECTORY)
set_property(DIRECTORY)
INCLUDE_DIRECTORIES
目标属性(Target)单个构建目标(库、可执行、接口库…)get_target_property()
set_target_properties()
LINK_LIBRARIES
INCLUDE_DIRECTORIES
源文件属性(Source File)单个源码/资源文件get_property(SOURCE)
set_property(SOURCE)
WORKING_DIRECTORY
测试属性(Test)由 add_test()定义的单个测试get_property(TEST)
set_property(TEST)
WORKING_DIRECTORY
安装文件属性(Installed File)install()生成的安装清单条目get_property(INSTALLED_FILE)
set_property(INSTALLED_FILE)
RPATH
属性的作用域关键字

这是 CMake 属性系统中一个非常强大的概念,尤其是在处理库的依赖时。target_* 命令中的 PUBLIC、PRIVATE 和 INTERFACE 关键字,就是用来控制属性的传递性。

  • PRIVATE: 属性只对当前目标可见,不会传递给链接到它的其他目标。
  • PUBLIC: 属性既对当前目标可见,也会传递给链接到它的其他目标。
  • INTERFACE: 属性只传递给链接到它的其他目标,对当前目标不可见。这常用于头文件库。

想象一下你有两个目标:my_app(可执行文件)和 my_lib(一个库),并且 my_app 依赖于 my_lib。

关键字对当前目标的构建影响是否传播对下游目标(如 my_app)的影响类比理解
PRIVATE生效否不生效内部工作:my_lib 需要某些头文件或编译选项来构建自身,但这些信息对 my_app 来说是无关的。就像你公司的财务数据,只对公司内部有用,不会公开给客户。
PUBLIC生效是生效公开 API:my_lib 的头文件既是它自身构建的一部分,也是 my_app 使用它时所必需的。就像公司的公开 API,既用于内部开发,也提供给外部开发者使用。
INTERFACE不生效是生效纯接口:my_lib 可能是一个只包含头文件的库(例如 boost::headers),它本身不需要编译任何东西,但所有链接到它的目标都必须知道它的头文件路径。就像一个提供的 API 文档,它本身不是产品,但客户必须阅读它才能正确使用你的产品。
目标属性 (Target Properties)

每个目标都有一系列属性,这些属性就像一个键值对表,存储着目标的各种元数据和构建信息。可以使用 set_target_properties() 和 get_target_property() 来操作它们。

  • 构建实现属性(Build Implementation Properties):
    • SOURCES: 目标的源文件列表。
    • COMPILE_DEFINITIONS: 编译时定义的宏。
    • COMPILE_OPTIONS: 传递给编译器的额外选项。
  • 使用要求属性(Usage Requirements Properties):
    • INTERFACE_INCLUDE_DIRECTORIES: 供链接到该目标的其他目标使用的头文件目录。这是实现 PUBLIC 和 INTERFACE 行为的基础。
    • INTERFACE_LINK_LIBRARIES: 供链接到该目标的其他目标使用的库列表。
  • 输出/安装属性(Output/Install Properties):
    • RUNTIME_OUTPUT_DIRECTORY: 可执行文件的输出路径。
    • ARCHIVE_OUTPUT_DIRECTORY: 静态库的输出路径。
    • LIBRARY_OUTPUT_DIRECTORY: 动态库的输出路径。
    • SOVERSION: 动态库的 ABI 版本号。
  • 元数据属性(Meta Properties):
    • TYPE: 目标的类型(如 EXECUTABLE、STATIC_LIBRARY 等)。
    • ALIASED_TARGET: 如果目标是别名,此属性指向被别名的原始目标。
1.3 操作目标和属性的核心 API

CMake 提供了丰富的命令来操作目标和它们的属性,而且操作目标和属性的核心 API 主要集中在 target_ 系列命令上

类别典型命令主要作用涉及核心属性
通用读/写读口target compile_options
target _precompile_headers
tinclude_sdirectoriestarget_sources
任意目标属性的设置、追加、查询(最底层API)
控制源文
任何prop_tgt
编译阶段相关target link libraries
target_link_options
target link_directories
控制源文件编译:宏定义、编译选项、语言特
性、预编译头、包含目录、源文件列表等
COMPILE_DEFINITIONS、 COMPILE_OPTIONS、COMPILE_FEATURES、 PRECOMPILE_HEADERS、 INCLUDE_DIRECTORIES、 SOURCES等
链接 & 输出的阶段相关target_link_libraries
target_link_options
target_link_directories
配置目标被链接时的库、选项及搜索路径COMPILE FEATURES
PRECOMPIL_HEADERS
INCLUDE DIRECTORIES、SOURCES 等
安装 & 打包阶段相关install(TARGETS…)
install(EXPORT…)
生成安装规则与包,控制目标在安装树中的布局及其运行时行为RUNTIME OUTPUT DIRECTORY.
LIBRARY OUTPUT DIRECTOR
ARCHIVE OUTPUT DIRECTOR
EXPORT NAME、
INSTALL RPATH
目标操作 API (Target Manipulation APIs)

CMake 提供了大量 API 来操作目标及其属性,其中最核心的命令以 target_ 开头。

  • add_library() / add_executable(): 创建新目标。
  • target_include_directories(): 最常用。为目标指定头文件目录,并控制其可见性(PUBLIC, PRIVATE, INTERFACE)。
  • target_link_libraries(): 最常用。指定目标需要链接的库或目标。它会自动处理依赖传递,确保链接顺序正确。
  • target_compile_definitions(): 为目标添加编译时的宏定义。
  • target_compile_options(): 为目标添加编译选项。
  • install(TARGETS ...): 定义如何安装目标文件。

2. add_library

2.1 基本概述

作用:用于创建库目标(Library Target)。一个库是一组编译好的源代码,可以被其他目标链接和重用

基本形式:

add_library(<name> [STATIC | SHARED | MODULE] [EXCLUDE_FROM_ALL]
            <source1> [<source2> ...])

参数解释:

  • <name>: 库目标的名称。这个名称在整个 CMake 项目中必须是唯一的。CMake 会根据这个名称和库类型(静态/动态)自动生成对应的库文件名(例如 lib<name>.a 或 <name>.dll)。

    [STATIC | SHARED | MODULE]: 可选的库类型关键字,用来指定要创建哪种类型的库。

    [EXCLUDE_FROM_ALL]: 可选关键字,表示这个库不会被默认的 all 构建目标所构建。当你不希望它被自动构建时非常有用。

    <source1> [<source2> ...]: 构成库的源文件列表。

默认情况下,将在与调用命令的源树目录相对应的构建树目录中创建库文件。

  • 可以使用 ARCHIVE_OUTPUT_DIRECTORY、LIBRARY_OUTPUT_DIRECTORY 和 RUNTIME_OUTPUT_DIRECTORY 目标属性修改默认的输出路径。
2.2 库类型

add_library 支持多种库类型,每种类型都有不同的用途和特性。

a. STATIC (静态库)

  • 作用: 将所有目标文件的代码打包成一个单独的文件(在 Linux 上是 .a,在 Windows 上是 .lib)。
  • 特性: 静态库在链接阶段会被完整地复制到最终的可执行文件中。这使得可执行文件是独立的,不需要依赖外部库文件。
  • 优点: 易于分发,因为所有依赖都打包在可执行文件内部。
  • 缺点: 无法在多个程序间共享代码,会增加可执行文件的大小。

b. SHARED (动态库)

  • 作用: 创建一个动态链接库(在 Linux 上是 .so,在 Windows 上是 .dll)。
  • 特性: 动态库不会被复制到可执行文件中。可执行文件只包含指向动态库的引用,在程序运行时才加载库。
  • 优点: 节省磁盘空间和内存,允许多个程序共享同一份库文件。易于更新,只需替换库文件即可,无需重新编译可执行文件。
  • 缺点: 分发时必须同时提供库文件,否则程序无法运行。

c. MODULE (模块库)

  • 作用: 创建一种特殊的动态库,用于在运行时通过 dlopen(Linux)或 LoadLibrary(Windows)等函数动态加载。
  • 特性: 模块库通常不用于静态链接。它们主要用作插件或可扩展模块。
  • 区别: 模块库和动态库的主要区别在于它们的用途和链接方式。动态库主要用于链接阶段,而模块库主要用于运行时加载。

除了上述三种标准类型,CMake 还提供了两种特殊的库目标。

a. OBJECT (对象库)

  • 作用: 将源代码编译成目标文件(.o),但不会将它们打包成库。
  • 特性: 对象库用于多个目标共享相同的源代码。你可以创建一个对象库,然后将其中的目标文件链接到多个其他目标中。
  • 优点: 避免重复编译,提高大型项目的编译速度。

b. INTERFCE (接口库)

  • 作用: 创建一个不包含任何源文件、也不生成任何库文件的目标。
  • 特性: 接口库主要用于传递 接口属性,如头文件路径(INTERFACE_INCLUDE_DIRECTORIES)和编译定义(INTERFACE_COMPILE_DEFINITIONS)。
  • 优点: 非常适合用于纯头文件库或仅用于管理依赖的场景。你可以用它来表示一个外部依赖,并向其他目标提供所需的编译信息。

示例

# 创建一个静态库
add_library(my_static_lib STATIC src/static_util.cpp)

# 创建一个动态库
add_library(my_shared_lib SHARED src/shared_util.cpp)

# 创建一个对象库
add_library(my_object_lib OBJECT src/shared_code.cpp)

# 创建一个接口库,用于包含公共头文件
add_library(my_headers INTERFACE)
target_include_directories(my_headers INTERFACE include)

# 链接到库
add_executable(my_app main.cpp)

# 链接静态库、动态库和对象库
target_link_libraries(my_app PRIVATE my_static_lib my_shared_lib my_object_lib)

# 链接接口库,获取头文件路径
target_link_libraries(my_app PRIVATE my_headers)

3. tartget_include_directories

3.1 基本概述

作用:设置目标在开发和发布阶段的 头文件搜索目录 ,可以 传递性 给下游的使用方(比如:最上面那举得样例,跨目录的头文件)

  • 告诉编译器去哪里寻找源文件中包含的头文件
  • 这个命令解决了 C++ 项目中最常见的问题:编译器找不到 #include 中的头文件

基本形式:

target_include_directories(<target>
    [SYSTEM] # path 告诉编译器“这些是系统头⽂件”,GCC/Clang 会⽤ -isystem ⽽⾮ -I,从⽽ 抑制第三⽅头引出的警告。 
    [BEFORE] # 把路径插到已有列表最前⾯ 
    <INTERFACE|PUBLIC|PRIVATE> path1 [path2 ...]
    [<INTERFACE|PUBLIC|PRIVATE> pathN ...] …
)

参数解释:

  • <target>:要添加头文件路径的目标名称(例如,一个库或可执行文件)。
  • <INTERFACE|PUBLIC|PRIVATE>:属性的作用域关键字,它们定义了头文件路径的 可见性(visibility) 和 传递性(transitivity)。
  • path:头文件搜索路径,如果是相对路径则相对于当前的 CMAKE_CURRENT_SOURCE_DIR

注意:通过 target_include_directories 添加的路径 最终是通过 gcc 的 -I 或 /I 选项 传递给编译器的

3.2 对比 include_directories

最佳实践推荐使用 target_include_directories,而不要用 include_directories ,后者或导致搜索路径被误添加到其他目标

include_directories() 的问题:它会将其指定的头文件搜索路径 添加到当前 CMakeLists.txt 文件及其所有子目录中的所有目标上。

举个例子:想象一下你的项目有两个目录:my_lib 和 my_app,它们都通过 add_subdirectory() 添加到顶层 CMakeLists.txt 中。

my_lib/CMakeLists.txt:

include_directories(include)  # 问题所在
add_library(my_lib ...)

在上面的代码中,include_directories(include) 不仅为 my_lib 增加了搜索路径,还会自动将这个路径应用到所有后续的 add_subdirectory() 中定义的目标。这意味着,如果我们在 my_app 中定义了一个目标,它也会无意中继承 my_lib 的头文件路径。

这种行为带来了两个主要问题:

  1. 污染(Pollution):我们可能会无意中将一个库的内部头文件路径暴露给其他不相关的目标。这会导致编译器意外地找到不该找到的头文件,造成构建错误或难以调试的问题。
  2. 隐藏依赖(Hidden Dependencies):如果 my_app 意外地依赖了 my_lib 的头文件路径,但我们没有明确地使用 target_link_libraries 来建立依赖关系,那么当 my_lib 被移除或重构时,my_app 的构建就会失败,因为它的隐含依赖被打破了。

而 target_include_directories() 将头文件搜索路径与特定的目标绑定。它提供了精确的控制,并且通过 PUBLIC、PRIVATE 和 INTERFACE 关键字明确地管理依赖的传递。

  • 精确性:明确地告诉 CMake “这个路径只属于这个目标”,避免了不必要的路径污染。
  • 依赖管理:通过 PUBLIC 和 INTERFACE 关键字,将头文件依赖关系与链接依赖关系(target_link_libraries)结合起来。当一个目标链接到另一个目标时,它会自动获得所需的头文件路径。这使得依赖关系清晰可见,易于维护。
  • 可维护性:如果 my_lib 改变了它的内部结构,只要不影响它的 PUBLIC 头文件,my_app 就不需要做任何修改。而如果 my_lib 被移除,my_app 会在链接阶段立即报错,而不是在编译时因为找不到头文件而产生难以理解的错误。

4. target_link_libraries

4.1 基本概述

作用:告诉链接器去哪里找到所需的库文件,设置目标(target)的链接依赖库列表

  • 这个命令不仅处理库的链接,还负责管理它们之间的依赖关系
  • 相当于使用通用的set属性设置函数设置了 LINK_LIBRARIES 或者 INTERFACE_LINK _LIBRARIES 这个属性

基本形式:

target_link_libraries(<target>
    [<PRIVATE|PUBLIC|INTERFACE>]
    <item1> [<item2>...])

set_target_properties(<target> PROPERTIES 
 LINK_LIBRARIES|INTERFACE_LINK_LIBRARIES <item>...
)

参数解释:

  • <target>: 需要链接库的目标名称,如可执行文件或库。
  • <PRIVATE|PUBLIC|INTERFACE>:属性的作用域关键字
    • PRIVATE 关键字:相当于使用 set_target_properties 设置了 LINK_LIBRARIES 属性,设置的库列表只会写进目标的 LINK_LIBRARIES 列表里。
    • INTERFACE 关键字:相当于使用 set_target_properties 设置了 INTERFACE_LINK_LIBRARIES,只会写进目标的 INTERFACE LINK_LIBARIERS 列表里。
    • PUBLIC 关键字:设置的列表会同时写进 LINK_LIBRARIES 和 INTERFACELINK_LIBRARIES 里,并且INTERFACE_LINK_LIBRARIES 列表出现的库会被传播给这个目标的使用方。
  • <item>: 要链接的库。它可以是 CMake 定义的目标名(如 MyMath),也可以是外部库的文件路径。
4.2 示例

下面我们新建 test_target_link_libraries 这个工程,通过设置属性观察属性的传递过程,并观察编译器和链接器的编译链接参数。

目录结构

lighthouse@VM-8-10-ubuntu:test_target_link_libraries$ tree
.
├── build
├── CMakeLists.txt
├── main.cpp
└── src
    ├── add.cpp
    └── CMakeLists.txt

2 directories, 4 files

CMakeLists.txt

cmake_minimum_required(VERSION 3.18)
project(TestLinkLibrary LANGUAGES CXX)

add_subdirectory(src)
add_executable(main main.cpp)
# 建立链接关系,让 main 成为 add 的使用者
target_link_libraries(main PRIVATE add) 

src/CMakeLists.txt

add_library(add STATIC add.cpp)

# 头文件包含路径
target_include_directories(add PRIVATE "/usr/local/include/private")
target_include_directories(add PUBLIC "/usr/local/include/public")
target_include_directories(add INTERFACE "/usr/local/include/interface")

# 库文件搜索路径
target_link_directories(add PRIVATE "/usr/local/lib/private")
target_link_directories(add PUBLIC "/usr/local/lib/public")
target_link_directories(add INTERFACE "/usr/local/lib/interface")

# 依赖库列表
target_link_libraries(add INTERFACE "pthread")

add.cpp

// 空⽂件, 不真的使⽤add的逻辑,就是⽣成⼀个空的elf格式为库⽂件

main.cpp

int main(){
    return 0;
}

运行 CMake,并编译链接

lighthouse@VM-8-10-ubuntu:build$ cmake ../

lighthouse@VM-8-10-ubuntu:build$ cmake --build . -v
/usr/bin/cmake -S/home/lighthouse/code/cmake/test_target_link_libraries -B/home/lighthouse/code/cmake/test_target_link_libraries/build --check-build-system CMakeFiles/Makefile.cmake 0
Re-run cmake file: Makefile older than: ../CMakeLists.txt
-- Configuring done
-- Generating done
-- Build files have been written to: /home/lighthouse/code/cmake/test_target_link_libraries/build
/usr/bin/cmake -E cmake_progress_start /home/lighthouse/code/cmake/test_target_link_libraries/build/CMakeFiles /home/lighthouse/code/cmake/test_target_link_libraries/build//CMakeFiles/progress.marks
/usr/bin/gmake  -f CMakeFiles/Makefile2 all
gmake[1]: Entering directory '/home/lighthouse/code/cmake/test_target_link_libraries/build'
/usr/bin/gmake  -f src/CMakeFiles/add.dir/build.make src/CMakeFiles/add.dir/depend
gmake[2]: Entering directory '/home/lighthouse/code/cmake/test_target_link_libraries/build'
cd /home/lighthouse/code/cmake/test_target_link_libraries/build && /usr/bin/cmake -E cmake_depends "Unix Makefiles" /home/lighthouse/code/cmake/test_target_link_libraries /home/lighthouse/code/cmake/test_target_link_libraries/src /home/lighthouse/code/cmake/test_target_link_libraries/build /home/lighthouse/code/cmake/test_target_link_libraries/build/src /home/lighthouse/code/cmake/test_target_link_libraries/build/src/CMakeFiles/add.dir/DependInfo.cmake --color=
gmake[2]: Leaving directory '/home/lighthouse/code/cmake/test_target_link_libraries/build'
/usr/bin/gmake  -f src/CMakeFiles/add.dir/build.make src/CMakeFiles/add.dir/build
gmake[2]: Entering directory '/home/lighthouse/code/cmake/test_target_link_libraries/build'
[ 25%] Building CXX object src/CMakeFiles/add.dir/add.cpp.o
cd /home/lighthouse/code/cmake/test_target_link_libraries/build/src && /usr/bin/c++  -I/usr/local/include/private -I/usr/local/include/public  -MD -MT src/CMakeFiles/add.dir/add.cpp.o -MF CMakeFiles/add.dir/add.cpp.o.d -o CMakeFiles/add.dir/add.cpp.o -c /home/lighthouse/code/cmake/test_target_link_libraries/src/add.cpp
[ 50%] Linking CXX static library libadd.a
cd /home/lighthouse/code/cmake/test_target_link_libraries/build/src && /usr/bin/cmake -P CMakeFiles/add.dir/cmake_clean_target.cmake
cd /home/lighthouse/code/cmake/test_target_link_libraries/build/src && /usr/bin/cmake -E cmake_link_script CMakeFiles/add.dir/link.txt --verbose=1
/usr/bin/ar qc libadd.a CMakeFiles/add.dir/add.cpp.o
/usr/bin/ranlib libadd.a
gmake[2]: Leaving directory '/home/lighthouse/code/cmake/test_target_link_libraries/build'
[ 50%] Built target add
/usr/bin/gmake  -f CMakeFiles/main.dir/build.make CMakeFiles/main.dir/depend
gmake[2]: Entering directory '/home/lighthouse/code/cmake/test_target_link_libraries/build'
cd /home/lighthouse/code/cmake/test_target_link_libraries/build && /usr/bin/cmake -E cmake_depends "Unix Makefiles" /home/lighthouse/code/cmake/test_target_link_libraries /home/lighthouse/code/cmake/test_target_link_libraries /home/lighthouse/code/cmake/test_target_link_libraries/build /home/lighthouse/code/cmake/test_target_link_libraries/build /home/lighthouse/code/cmake/test_target_link_libraries/build/CMakeFiles/main.dir/DependInfo.cmake --color=
gmake[2]: Leaving directory '/home/lighthouse/code/cmake/test_target_link_libraries/build'
/usr/bin/gmake  -f CMakeFiles/main.dir/build.make CMakeFiles/main.dir/build
gmake[2]: Entering directory '/home/lighthouse/code/cmake/test_target_link_libraries/build'
[ 75%] Building CXX object CMakeFiles/main.dir/main.cpp.o
/usr/bin/c++  -I/usr/local/include/public -I/usr/local/include/interface  -MD -MT CMakeFiles/main.dir/main.cpp.o -MF CMakeFiles/main.dir/main.cpp.o.d -o CMakeFiles/main.dir/main.cpp.o -c /home/lighthouse/code/cmake/test_target_link_libraries/main.cpp
[100%] Linking CXX executable main
/usr/bin/cmake -E cmake_link_script CMakeFiles/main.dir/link.txt --verbose=1
/usr/bin/c++ CMakeFiles/main.dir/main.cpp.o -o main   -L/usr/local/lib/public  -L/usr/local/lib/interface  -Wl,-rpath,/usr/local/lib/public:/usr/local/lib/interface src/libadd.a -lpthread 
gmake[2]: Leaving directory '/home/lighthouse/code/cmake/test_target_link_libraries/build'
[100%] Built target main
gmake[1]: Leaving directory '/home/lighthouse/code/cmake/test_target_link_libraries/build'
/usr/bin/cmake -E cmake_progress_start /home/lighthouse/code/cmake/test_target_link_libraries/build/CMakeFiles 0

这个例子最核心的要点是:没有链接,就没有属性传递。

我们在 src/CMakeLists.txt 文件在 add 库上设置了各种属性,包括:

  • PRIVATE 头文件路径:/usr/local/include/private
  • PUBLIC 头文件路径:/usr/local/include/public
  • INTERFACE 头文件路径:/usr/local/include/interface
  • INTERFACE 链接库:pthread

并且顶层 CMakeLists.txt 中做了三件事:

add_subdirectory(src)
add_executable(main main.cpp)
# 建立链接关系,让 main 成为 add 的使用者
target_link_libraries(main PRIVATE add)
  • 创建了 main 可执行文件,并且使用 target_link_libraries 命令将其与 add 库链接起来。
  • 并且这是一个 PRIVATE 依赖,意味着 add 是 main 的内部实现细节,不会传递给 main 的使用者

CMake 的属性传递机制就像一个“契约”。只有当一个目标(main)明确声明它要依赖另一个目标(add)时,这个契约才生效,add 的 PUBLIC 和 INTERFACE 属性才会传递给 main。

现在,我们来分析 cmake --build . -v 的输出,看看属性是如何传递的

1. 构建 add 目标

add 库的编译命令和之前一样,因为它自身需要的属性没有变化。

/usr/bin/c++ -I/usr/local/include/private -I/usr/local/include/public ... -c /home/lighthouse/.../add.cpp
  • 头文件路径:add 的编译命令中包含了它自己的 PRIVATE (/usr/local/include/private) 和 PUBLIC (/usr/local/include/public) 路径。这是因为这两个属性都影响目标自身的构建。
  • INTERFACE 路径:INTERFACE 路径没有出现,因为它只传递给依赖者,不用于目标自身的编译。

2. 构建 main 目标

这部分才是关键,它展示了 main 如何继承 add 的属性。

编译 main.cpp:包含了从 add 传递过来的头文件路径。

/usr/bin/c++ -I/usr/local/include/public -I/usr/local/include/interface ... -c /home/lighthouse/.../main.cpp
  • PUBLIC 路径:-I/usr/local/include/public 出现了。这是因为 add 库的 PUBLIC 属性会传递给所有链接它的目标,main 自动继承了。
  • INTERFACE 路径:-I/usr/local/include/interface 也出现了。INTERFACE 属性同样会传递给依赖者,main 也正确地继承了。
  • PRIVATE 路径:/usr/local/include/private 没有出现,因为它被明确标记为 PRIVATE,不会传递给 main。

链接 main:链接命令现在包含了 add 库的所有链接依赖。

/usr/bin/c++ ... -o main -L/usr/local/lib/public -L/usr/local/lib/interface ... src/libadd.a -lpthread
  • 库搜索路径:-L/usr/local/lib/public 和 -L/usr/local/lib/interface 出现了。这是因为在 src/CMakeLists.txt 中设置的 PUBLIC 和 INTERFACE 链接目录也通过依赖关系传递给了 main。
  • 链接库:
    • src/libadd.a:这是 add 库本身的文件路径。因为 main 明确链接了 add,所以它被链接进去。
    • -lpthread:pthread 也出现了。这是 target_link_libraries(add INTERFACE "pthread") 的作用,INTERFACE 依赖被正确地传递给了 main。

5. set_target_properties/get_target_properties

5.1 set_target_properties

作用:为目标设置指定的属性值。这是修改目标默认行为的主要方式。

语法

set_target_properties(<target1> [<target2> ...]
                      PROPERTIES <prop1> <value1> [<prop2> <value2> ...])
  • <target1> [<target2> ...]: 一个或多个目标的名字。
  • PROPERTIES: 这是必需的关键字,用来分隔目标和属性列表。
  • <prop1> <value1> [...]: 键值对列表,其中 <prop1> 是属性名,<value1> 是它的值。

常见用法

你可以用它来设置各种属性,比如:

  • 修改输出文件名或目录:

    # 将 my_app 可执行文件重命名为 awesome_app
    set_target_properties(my_app PROPERTIES OUTPUT_NAME "awesome_app")
    
    # 将 my_lib 静态库的输出目录设置为 lib
    set_target_properties(my_lib PROPERTIES ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
    
  • 控制运行时库路径 (RPath): 在 Linux 上,当你的可执行文件依赖动态库时,RPATH 属性可以帮助运行时找到这些库。

    set_target_properties(my_app PROPERTIES
                          BUILD_WITH_INSTALL_RPATH true
                          INSTALL_RPATH_USE_LINK_PATH true)
    
  • 设置 C++ 标准和编译标志: 虽然更推荐使用 target_compile_features,但你仍然可以用它来设置 CXX_STANDARD 等属性。

    # 设置 C++17 标准,并要求编译器必须支持
    set_target_properties(my_app PROPERTIES
                          CXX_STANDARD 17
                          CXX_STANDARD_REQUIRED true)
    
  • 添加版本信息: 为动态库添加版本号,这在发布共享库时非常重要。

    set_target_properties(my_shared_lib PROPERTIES
                          VERSION "1.2.3"
                          SOVERSION "1")
    
5.2 get_target_property

作用:获取一个目标的指定属性值,并将其存储在一个变量中。这在编写更复杂的、需要根据目标属性做不同行为的 CMake 脚本时非常有用。

语法

get_target_property(<variable> <target> <property>)
  • <variable>: 一个变量名,用于存储获取到的属性值。
  • <target>: 你要查询的目标名字。
  • <property>: 你要获取的属性名。

常见用法

  • 获取目标类型:

    get_target_property(my_app_type my_app TYPE)
    if(my_app_type STREQUAL "EXECUTABLE")
        message("my_app is an executable.")
    endif()
    
  • 获取输出目录:

    get_target_property(lib_output_dir my_lib ARCHIVE_OUTPUT_DIRECTORY)
    message("The output directory for my_lib is: ${lib_output_dir}")
    
  • 检查属性是否存在: 如果属性不存在,get_target_property 会返回 NOTFOUND。

    get_target_property(version_info my_app VERSION)
    if(NOT version_info)
        message("my_app does not have a VERSION property set.")
    endif()
    
5.3 常见属性
属性名字含义gcc 选项
INCLUDE_DIRECTORIES构建规范:自己编译时包含的目录-l
INTERFACE_INCLUDE_DIRECTORIES使用要求:下游适用房需要包含的目录-l
LINK_LIBRARIES构建规范:自己链接时需要链接的库列表-l
INTERFACE_LINK_LIBRARIES使用要求:自己链接时需要链接的库列表-l
LIBRARY_OUTPUT_DIRECTORY库的输出路径
LIBRARY_OUTPUT_NAME库文件的输出名
BUILD_RPATH构建目录中的运行时库文件搜索路径-Wl, -rpath
INSTALL_RPATH安装目录中的运行时库文件搜索路径-Wl, -rpath
5.4 示例

下面我们将使用 test_set_target_properties 工程来验证库属性的设置和更新

目录结构

lighthouse@VM-8-10-ubuntu:test_set_target_properties$ tree
.
├── build
├── CMakeLists.txt
├── main.cpp
└── src
    ├── add.cpp
    └── CMakeLists.txt

2 directories, 4 files

CMakeLists.txt

cmake_minimum_required(VERSION 3.18)
project(TestLinkLibrary LANGUAGES CXX)

add_subdirectory(src)
add_executable(main main.cpp)
# 建立链接关系,让 main 成为 add 的使用者
target_link_libraries(main PRIVATE add) 

src/CMakeLists.txt

add_library(add STATIC add.cpp)

set_target_properties(add PROPERTIES
    # 1 编译类参数 
    COMPILE_OPTIONS "-g"
    COMPILE_OPTIONS "-O3"
    COMPILE_OPTIONS "-fPIC"

    INCLUDE_DIRECTORIES "/public"
    INTERFACE_INCLUDE_DIRECTORIES "/interface"
    # 2 链接类参数 
    LINK_DIRECTORIES "/public"
    INTERFACE_LINK_DIRECTORIES "/interface"
    LINK_LIBRARIES "curl"
    INTERFACE_LINK_LIBRARIES "jsoncpp"
    # 3 输出类参数 
    RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
    ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib"
    LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib"

    # 4 安装类参数 
    BUILD_RPATH "${CMAKE_BINARY_DIR}/lib"
    INSTALL_RPATH "lib" #//usr/local/lib
    OUTPUT_NAME "add"
    VERSION "1.2.3"
    SOVERSION "20"
)

add.cpp

// 空⽂件, 不真的使⽤add的逻辑,就是⽣成⼀个空的elf格式为库⽂件

main.cpp

int main(){
    return 0;
}

运行 CMake,并编译链接

cmake ../
cmake --build . -v

image-20250922163344489

6. add_subdirectory

6.1 基本概述

作用:将一个子目录添加到 构建树 中,并将该子目录中的 CMakeLists.txt 文件添加到处理队列,此时 CMake 会自动进入源码树子目录,执行位于子目录里的 CMakeLists.txt 文件

  • 相当于将一个复杂的项目分解成多个子项目,每个子项目有自己的 CMakeLists.txt 文件

基本形式:

add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])

参数解释:

  • source_dir:包含子项目 CMakeLists.txt 的源代码目录路径。
  • binary_dir:可选参数,指定该子项目构建文件的输出目录。如果省略,CMake 会自动创建一个与 source_dir 同名的子目录。
  • EXCLUDE_FROM_ALL:可选关键字,表示该子目录中的所有目标不会被默认的 all 构建目标所构建。当你不希望某个子项目被自动构建时非常有用,例如只用于测试或例子。

add_subdirectory 如何工作?

当调用 add_subdirectory() 时,CMake 会执行以下几个关键步骤:

  1. 切换目录: CMake 的当前工作目录(CMAKE_CURRENT_SOURCE_DIR 和 CMAKE_CURRENT_BINARY_DIR)会切换到指定的子目录。
  2. 处理 CMakeLists.txt:CMake 会在子目录中查找并处理其 CMakeLists.txt 文件。这个文件中可以定义新的目标、设置新的变量、添加新的子目录等等。
  3. 作用域:子目录中的 CMakeLists.txt 拥有自己的变量作用域。在子目录中定义的变量默认是局部的,只在该子目录中可见。但是,父目录中的变量对子目录是可见的。
  4. 目标可见性:在子目录中定义的所有目标(例如,通过 add_library 和 add_executable 创建的目标)对父目录都是可见的。这是 target_link_libraries 命令能够跨目录工作的基础。

关键行为

  1. 处理顺序:CMake 会立即处理 source_dir 中的 CMakeLists.txt,当前文件的处理会暂停,直到子目录处理完毕,在继续处理当前文件 add_subdirectory 之后的命令。

  2. 路径解析

    • source_dir 相对路径:相对于当前 CMakeLists.txt 文件所在目录。
    • binary_dir 相对路径:相对于当前构建目录(不指定,则使用 source_dir)
  3. 变量作用域

    • 子目录中定义的变量默认是局部的,不会影响父目录
    • 可通过 set(xxx PARENT_SCOPE) 将变量传递到父目录
    • 缓存变量是全局的,子目录里设置的缓存变量,父目录也可以获取到。
  4. CMake 在当前上下文执行完 add_subdirectory 命令,进入到子目录之后,在开始执行 CMakeLists.txt 时会修改的内置变量:

    变量名进入子目录是否变化说明
    CMAKE_CURRENT_SOURCE_DIR✅变为子目录的源代码树的目录
    CMAKE_CURRENT_BINARY_DIR✅变为子目录的构建树的目录
    CMAKE_CURRENT_LIST_FILE✅变为子目录的 CMakeLists.txt 文件全路径
    CMAKE_CURRENT_LIST_DIR✅变为子目录的 CMakeLists.txt 文件目录
6.2 对比 include
  1. include(<file>):把这段脚本当作 当前目录 的一部分执行
  2. add_subdirectory(<dir>):进行一个新目录作用域,递归执行其中的 CMakeLists.txt,并为它指定独立的构建输出目录

示例

目录结构

lighthouse@VM-8-10-ubuntu:test_add_sub_dir$ tree
.
├── CMakeLists.txt
└── sub
    └── CMakeLists.txt

1 directory, 2 files

CMakeLists.txt 代码如下:

cmake_minimum_required(VERSION 3.18)
project(TestAddSubDir)
message(STATUS "from top-level CMakeLists.txt")

# 当前正在执行的源代码目录 -- 即CMakeLists.txt所在的目录 
message(STATUS "CMAKE_CURRENT_SOURCE_DIR:" ${CMAKE_CURRENT_SOURCE_DIR})
# 当前正在执行构建树目录
message(STATUS "CMAKE_CURRENT_BINARY_DIR:" ${CMAKE_CURRENT_BINARY_DIR}) 
# 当前正在执行的cmake 脚本的 完整名称 
message(STATUS "CMAKE_CURRENT_LIST_FILE:" ${CMAKE_CURRENT_LIST_FILE})
# 当前正在执行的 cmake 脚本的全目录 
message(STATUS "CMAKE_CURRENT_LIST_DIR:" ${CMAKE_CURRENT_LIST_DIR})
# 包含子目录cmake脚本 
add_subdirectory(sub)
# include(sub/CMakeLists.txt)

sub/CMakeLists.txt

message(STATUS "from sub/CMakeLists.txt")

message(STATUS "CMAKE_CURRENT_SOURCE_DIR:" ${CMAKE_CURRENT_SOURCE_DIR})
message(STATUS "CMAKE_CURRENT_BINARY_DIR:" ${CMAKE_CURRENT_BINARY_DIR}) 
message(STATUS "CMAKE_CURRENT_LIST_FILE:" ${CMAKE_CURRENT_LIST_FILE})
message(STATUS "CMAKE_CURRENT_LIST_DIR:" ${CMAKE_CURRENT_LIST_DIR})

CMake 执行

lighthouse@VM-8-10-ubuntu:build$ cmake ../

-- from top-level CMakeLists.txt
-- CMAKE_CURRENT_SOURCE_DIR:/home/lighthouse/code/cmake/test_add_sub_dir
-- CMAKE_CURRENT_BINARY_DIR:/home/lighthouse/code/cmake/test_add_sub_dir/build
-- CMAKE_CURRENT_LIST_FILE:/home/lighthouse/code/cmake/test_add_sub_dir/CMakeLists.txt
-- CMAKE_CURRENT_LIST_DIR:/home/lighthouse/code/cmake/test_add_sub_dir

-- from sub/CMakeLists.txt
-- CMAKE_CURRENT_SOURCE_DIR:/home/lighthouse/code/cmake/test_add_sub_dir/sub
-- CMAKE_CURRENT_BINARY_DIR:/home/lighthouse/code/cmake/test_add_sub_dir/build/sub
-- CMAKE_CURRENT_LIST_FILE:/home/lighthouse/code/cmake/test_add_sub_dir/sub/CMakeLists.txt
-- CMAKE_CURRENT_LIST_DIR:/home/lighthouse/code/cmake/test_add_sub_dir/sub
  1. 可以看到 add_subdirectory 之后,src 里的变量自动添加了 app 子目录。
  2. 子目录 cmake 文件处理完成之后,才会回到main里继续执行,可以C语言 函数调用流程类似。

然后把顶层的 CMakeLists.txt 里的 add_subdirectory 换成 include ,结果又会怎样呢

-- from top-level CMakeLists.txt
-- CMAKE_CURRENT_SOURCE_DIR:/home/lighthouse/code/cmake/test_add_sub_dir
-- CMAKE_CURRENT_BINARY_DIR:/home/lighthouse/code/cmake/test_add_sub_dir/build
-- CMAKE_CURRENT_LIST_FILE:/home/lighthouse/code/cmake/test_add_sub_dir/CMakeLists.txt
-- CMAKE_CURRENT_LIST_DIR:/home/lighthouse/code/cmake/test_add_sub_dir

-- from sub/CMakeLists.txt
-- CMAKE_CURRENT_SOURCE_DIR:/home/lighthouse/code/cmake/test_add_sub_dir
-- CMAKE_CURRENT_BINARY_DIR:/home/lighthouse/code/cmake/test_add_sub_dir/build
-- CMAKE_CURRENT_LIST_FILE:/home/lighthouse/code/cmake/test_add_sub_dir/sub/CMakeLists.txt
-- CMAKE_CURRENT_LIST_DIR:/home/lighthouse/code/cmake/test_add_sub_dir/sub

对比两个的输出,我们可以发现:

image-20250922170117336

当前执行的 CMake 脚本位置,可以只用以这个位置为启动的 相对位置 来定位其他文件

7. file

作用:查看目录下的所有文件,如果匹配规则,则添加文件名字到文件列表中,是否需要递归,需要显示指定。

函数原型

file(GLOB|GLOB_RECURSE <variable> [LIST_DIRECTORIES true|false] 
	[RELATIVE <path>] [CONFIGURE_DEPENDS] <globbing-expressions>...
)

参数解释

  • GLOB:匹配当前目录下的文件(不递归子目录)
  • GLOB_RECURSE:递归匹配当前目录及其子目录下的文件
  • <out-var>:收集到的文件列表变量
  • <globbing-expr>:通配符表达式(如: .cpp、src/**/*.h)

注意事项

  • 尽量避免 GLOB: 尽管 file(GLOB) 很方便,但它有一个缺点:如果新增或删除了文件,CMake 不会自动重新运行配置。因此,如果你在用 GLOB 获取源文件列表后添加了新文件,需要我们手动触发 CMake 重新配置。对于源文件,更推荐手动列出或使用 add_custom_command 和 add_custom_target 来处理。
  • 谨慎使用 REMOVE_RECURSE: 这个命令非常危险,因为它会递归删除整个目录,因此使用前请确保操作的是正确的目录。

8. CMake 内部静态库的生成和定位流程

在cmake里当你添加一个目标比如静态库后,cmake会在 配置阶段 自动跟踪静态库的输出位置然后在生成阶段,会把这个输出地址通过-L参数传递给链接器,链接器就可以找到项目内部的静态库,整个过程的产物就是可视化的makefile(一切尽在makefile里)

下面就详细展开这个步骤:

第一步:目标生成

  • 当写下 add_library(MyMath STATIC add.cpp sub.cpp) 这一行命令时,cmake内部会在全局的 Targets 容器中注册一个名为 MyMath 的 cmTarget 目标。

第二步:目标信息存储

  • 每个 cmTargetInternals 会存储目标的名称,类型,源文件列表,输出地址等属性。当你使用 LIBRARY_OUTPUT_DIRECTORY 等属性修改目标输出地址的时候,cmake也会更新最终的输出地址。

第三步:生成器阶段-定位静态库路径

  • CMake配置阶段结束后,进入生成阶段(cmLocalGenerator、cmGlobalGenerator),生成器会遍历所有目标(cmTarget),根据目标的属性(如 ARCHIVE_OUTPUT_DIRECTORY )和平台规则,推导出静态库的实际输出路径。比如:build/libmylib.a

第四步:链接命令的生成

  • CMake 的生成器(Generator,如 Unix Makefiles 或 Ninja)在生成构建文件(如 Makefile)时,会根据目标的依赖关系生成链接器命令(g++ main.o -o myexe build/libmylib.a),这样链接器(g++/ld)就能找到并链接 libmylib.a。这个过程不仅仅是简单地将库文件的路径拼接起来,它遵循特定的规则:
    • 库搜索路径(-L): CMake 会将库所在的目录作为链接器的搜索路径。例如,如果 MyMath 静态库被放置在 build/lib 目录下,CMake 会自动在链接 main 可执行文件时,将 -L../lib 添加到链接命令中。这让链接器知道去哪里寻找库文件。
    • 库名称(-l): 链接器命令不会直接使用库文件的完整路径(如 ../lib/libMyMath.a),而是使用 -l 选项指定库名(MyMath)。链接器会根据 -L 提供的路径列表,自动去查找名为 libMyMath.a 的文件。

上面都是理论过程,下面我们通过例子来验证下这个过程,过程中使用到了配置生成阶段的一个生成器表达式,用于获取目标的输出路径:$<TARGET FILE:MyMath>

生成器表达式(Generator Expression) 是CMake 中一种特殊的语法,用于在生成阶段(即CMake 生成构建系统文件时)动态计算和展开内容。以 $<..> 的形式出现,常用于target 属性、编译选项、链接参数、install规则计算。

我们使用静态库的编译链接和使用之前的 my_math 这个项目,只需要在 app/CMakeLists.txt 后面追加一个命令即可,具体过程如下:

# 5. 打印库文件相对 main 的相对路径
add_custom_command(
    TARGET main POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E echo
    "MyMath static lib: $<TARGET_FILE:MyMath>"
    COMMENT "获取静态库的输出路径"
)

CMake 执行如下:

lighthouse@VM-8-10-ubuntu:build$ cmake ..
lighthouse@VM-8-10-ubuntu:build$ cmake --build . 
[ 20%] Building CXX object my_lib/CMakeFiles/MyMath.dir/src/add.cpp.o
[ 40%] Building CXX object my_lib/CMakeFiles/MyMath.dir/src/sub.cpp.o
[ 60%] Linking CXX static library ../lib/libMyMath.a
[ 60%] Built target MyMath
[ 80%] Building CXX object app/CMakeFiles/main.dir/main.cpp.o
[100%] Linking CXX executable ../bin/main
获取静态库的输出路径
MyMath static lib: /home/lighthouse/code/cmake/mymath/build/lib/libMyMath.a
[100%] Built target main

查看 Makefile 文件:cmake 会根据目标的输出路径生成实际的链接规则

lighthouse@VM-8-10-ubuntu:build$ cat app/CMakeFiles/main.dir/link.txt
/usr/bin/c++ CMakeFiles/main.dir/main.cpp.o -o ../bin/main  ../lib/libMyMath.a 
遇到的一些问题
# 5. 打印库文件相对 main 的相对路径
add_custom_command(
    TARGET main POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E echo
    "$<PATH:RELATIVE_PATH,$<TARGET_FILE:MyMath>,${CMAKE_CURRENT_BINARY_DIR}>"
    COMMENT "获取静态库的输出路径"
)

add_custom_command(
	TARGET main POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E echo
    "MyMath static lib: $<TARGET_FILE:MyMath>"
    COMMENT "获取静态库的输出路径"
)
  1. $<PATH:RELATIVE_PATH, ...> 是用来计算相对路径的。它需要两个路径作为参数,然后给你它们之间的相对关系。例如,$<PATH:RELATIVE_PATH,/a/b/c,/a/d/e> 会计算出 ../../d/e。
  2. $<TARGET_FILE: ...> 是用来获取绝对路径的。它接收一个目标名,然后给你这个目标最终编译生成的可执行文件、静态库或动态库的完整绝对路径。例如,$<TARGET_FILE:main> 会给你 /path/to/build/bin/main。

但是前者会出现如下报错,后者不会(在 Cmake 构建的时候):

CMake Error at app/CMakeLists.txt:10 (add_custom_command):
  Error evaluating generator expression:

    $<PATH:RELATIVE_PATH,$<TARGET_FILE:MyMath>,/home/lighthouse/code/cmake/learn5/build/app>

  Expression did not evaluate to a known generator expression

生成器表达式:两阶段解析本质

这背后是 CMake 配置阶段(Configure) 和 生成阶段(Generate) 两阶段模型的根本体现

  1. 配置阶段(第一次解析): 当运行 cmake .. 时,CMake 会解析 CMakeLists.txt 中的所有命令。它会替换所有的 ${VAR} 变量,并对生成器表达式进行预处理。在这个阶段:
    • 像 $<$<CONFIG:Debug>: ...> 这种依赖于配置的表达式,可以立即求值并得到结果。
    • 像 $<TARGET_FILE:My Math> 这种依赖于构建结果的表达式,无法在此时求值。CMake 会将其视为一个特殊的“占位符”,原样保留,等待生成阶段再处理。
  2. 生成阶段(第二次解析): 当你运行 cmake --build . 时,构建系统(如 make)会执行 CMake 生成的构建文件。在执行过程中,这些构建文件会再次调用 CMake,此时,那些在配置阶段被保留的生成器表达式($<...>)会被正确地展开为具体的值。

为什么第一种写法会报错?

add_custom_command(
    TARGET main POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E echo
    "$<PATH:RELATIVE_PATH,$<TARGET_FILE:MyMath>,${CMAKE_CURRENT_BINARY_DIR}>"
)

这个错误发生的根本原因是:$<PATH:RELATIVE_PATH,...> 不是一个构建时才能展开的表达式。它在 CMake 内部被设计为在配置阶段就进行求值。由于它的第二个参数 $<TARGET_FILE:MyMath> 在配置阶段无法求值,导致整个表达式解析失败,所以 CMake 直接抛出错误并中断配置。

为什么第二种写法不会报错?

add_custom_command(
    TARGET main POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E echo
    "MyMath static lib: $<TARGET_FILE:MyMath>"
)

这种写法之所以成功,是因为 $<TARGET_FILE:MyMath> 是一个构建时表达式,它在配置阶段被安全地保留了下来。add_custom_command 知道如何处理这种“延迟求值”的表达式。在构建时,当 main 目标被构建完成后,$<TARGET_FILE:MyMath> 会被正确地替换为 libMyMath.a 的实际路径。

注意:虽然用前者计算相对路径在 CMake 的时候会报错,但是 cmake --build . 以及可执行文件运行都是没问题的

  • 因为 cmake 并没有因为这个特定的语法错误而完全停止生成所有构建文件。它生成了大部分有效的 Makefile,允许 cmake --build . 顺利完成编译和链接,而 add_custom_command 的部分则被跳过或处理失败了。

二、静态库(编译-链接-安装)

在上面开头我就用了一个简单的 MyMath 工程来演示了如何生成并链接到项目内部的静态库,现在来看看如何发布生成的静态库到 linux 下的标准路径,以供其他软件引用,从而达到代码复用的目的

目录结构

lighthouse@VM-8-10-ubuntu:install_static_mymath$ tree
.
├── build
├── CMakeLists.txt
└── my_lib
    ├── CMakeLists.txt
    ├── Config.cmake.in
    ├── include
    │   └── math.h
    └── src
        ├── add.cpp
        └── sub.cpp

4 directories, 6 files

顶层 CMakeLists.txt

cmake_minimum_required(VERSION 3.18)
project(TestMyMath LANGUAGES CXX)

# 添加目录层级
add_subdirectory(my_lib)

my_lib/CMakeLists.txt

# 1. 收集库的源代码
file(GLOB SRC_LISTS "src/*.cpp")
# 2. 添加构建目标
add_library(MyMath STATIC ${SRC_LISTS})
# 3 设置库的使⽤要求,也就是下游消费者必须包含的头⽂件搜索路径 
target_include_directories(MyMath INTERFACE
    "$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>" # include
)
# 4. 修改默认的输出路径
set_target_properties(MyMath PROPERTIES ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)

# 5. 安装静态库
include(GNUInstallDirs)
install(TARGETS MyMath
    EXPORT MyMathTargets
    DESTINATION ${CMAKE_INSTALL_LIBDID} #lib
)

# 6. 安装头文件
install(DIRECTORY include/
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/math #/usr/local/include/math/math.h
    FILES_MATCHING PATTERN "*.h"
)

# 7. 安装导出目标集合 到 构建树 
export(EXPORT MyMathTargets
    FILE ${CMAKE_CURRENT_BINARY_DIR}/MyMathTargets.cmake    
)
# 8. 安装导出目标集合 到 安装树 
install(EXPORT MyMathTargets
    FILE MyMathTargets.cmake
    NAMESPACE MyMath:: # MyMath::MyMath
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyMath # /usr/local/lib/cmake/MyMath/MyMathTargets.cmake
)

# 9. 生成 find_package 需要的配置文件
include(CMakePackageConfigHelpers)
configure_package_config_file(
    ${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in
    ${CMAKE_CURRENT_BINARY_DIR}/MyMathConfig.cmake
    INSTALL_DESTINATION "lib/cmake/MyMath"
)

# 10. 安装配置⽂件到cmake 标准的安装路径 
install(FILES 
    ${CMAKE_CURRENT_BINARY_DIR}/MyMathConfig.cmake
    DESTINATION "lib/cmake/MyMath"
)

部分解释说明

  • 设置库要求
    • INTERFACE:这个关键字表示 MyMath 库自身不使用这个路径,而是将其传递给所有依赖它的消费者。
    • $<INSTALL_INTERFACE:...>:这是一个 Generator Expression(生成器表达式)。它的作用是:
      • 在构建树中(即你当前开发的目录),这个表达式是空的,不会添加任何头文件路径。
      • 在安装树中(即你的库被安装到 /usr/local 之后),这个表达式会展开为 ${CMAKE_INSTALL_INCLUDEDIR},也就是 /usr/local/include。
    • 目的:这确保了当其他项目 find_package 找到你的库时,它们会自动获得正确的头文件搜索路径,而无需手动添加。
  • 设置安装库文件(第 4 5 点)
    • set_target_properties(...):将构建生成的 libMyMath.a 库文件放置在 build/lib 目录下,方便管理。
    • include(GNUInstallDirs):这是一个内置模块,它定义了标准的安装目录变量,如 ${CMAKE_INSTALL_LIBDIR}(通常是 lib)和 ${CMAKE_INSTALL_INCLUDEDIR}(通常是 include)。这让你的安装脚本具有更好的跨平台兼容性
    • install(TARGETS ...):这是安装目标的核心命令。它告诉 CMake,MyMath 这个目标将被安装到${CMAKE_INSTALL_LIBDIR} 目录下
    • EXPORT MyMathTargets:这个关键字是最重要的。它告诉 CMake,在安装时,将 MyMath 这个目标的所有属性(包括它的头文件、链接依赖等)打包到一个名为 MyMathTargets 的导出集合中。这个集合稍后会被写入一个 .cmake 文件,供 find_package 使用。
  • 安装头文件 install(DIRECTORY …):将 include/ 目录下的所有头文件(符合 *.h 模式)复制到安装目录的 include/math/ 路径下。这使得使用者可以通过 #include <math/math.h> 这样的路径来使用你的头文件
  • 导出目标(第 7 8 点)
    • export(...):这步是为在构建树中测试 find_package 功能而准备的。它将 MyMathTargets 集合写入 build/MyMathTargets.cmake。
    • install(EXPORT ...):这步是为正式安装而准备的。它告诉 CMake,在 cmake --install . 时,将 MyMathTargets 集合写入 MyMathTargets.cmake 文件,并安装到 ${CMAKE_INSTALL_LIBDIR}/cmake/MyMath 目录下。
    • NAMESPACE MyMath:::这会为目标添加命名空间,让使用者通过 MyMath::MyMath 来引用你的库,防止名字冲突。
  • 生成 find_package 配置文件(第 9 10 点)
    • Config.cmake.in:这是一个模板文件,包含 find_package 运行所需的逻辑。
    • configure_package_config_file(...):CMake 会读取 Config.cmake.in 模板,用 INSTALL_DESTINATION 变量的值替换其中的 @...@ 占位符,然后生成最终的 MyMathConfig.cmake 文件。
    • install(FILES ...):将生成的 MyMathConfig.cmake 文件安装到标准路径。

Config.cmake.in

@PACKAGE_INIT@
include(${CMAKE_CURRENT_LIST_DIR}/MyMathTargets.cmake)

其他代码不变,然后来执行 CMake,如下:

lighthouse@VM-8-10-ubuntu:build$ cmake ../
lighthouse@VM-8-10-ubuntu:build$ cmake --build .
[ 33%] Building CXX object my_lib/CMakeFiles/MyMath.dir/src/add.cpp.o
[ 66%] Building CXX object my_lib/CMakeFiles/MyMath.dir/src/sub.cpp.o
[100%] Linking CXX static library ../lib/libMyMath.a
[100%] Built target MyMath

lighthouse@VM-8-10-ubuntu:build$ sudo cmake --install .
-- Install configuration: ""
-- Installing: /usr/local/lib/libMyMath.a
-- Installing: /usr/local/include/math
-- Installing: /usr/local/include/math/math.h
-- Installing: /usr/local/lib/cmake/MyMath/MyMathTargets.cmake
-- Installing: /usr/local/lib/cmake/MyMath/MyMathTargets-noconfig.cmake
-- Installing: /usr/local/lib/cmake/MyMath/MyMathConfig.cmake

我们来看看其相关的一些重要命令

1. export

作用:将项目中的目标(如可执行文件、库)及其相关属性导出到一个 .cmake 文件中,以便在其他项目中使用

  • 这个文件包含了目标的所有元数据,例如:
    • 目标类型(可执行文件、静态库、动态库等)
    • 公共头文件路径
    • 公共编译定义
    • 公共链接依赖
    • …以及其他所有公共属性
  • 这个导出的 .cmake 文件通常被称为 “targets file” 或 “export file”。
  • 设计理念:将目标的依赖信息打包到目标本身。这意味着,当一个项目引用了你的库时,它不需要手动添加一大堆头文件路径、链接库和编译选项。所有这些信息都已经被你通过 export 命令打包到了 .cmake 文件中

函数原型

export(EXPORT <export_name> [NAMESPACE <namespace>]
       FILE <filename> [APPEND])

参数

  • <export_name>:导出的目标集合的名称,用于在其他项目中引用(这个名称只在 CMake 内部使用)
  • FILE <file_name>:指定生成的 .cmake 文件的路径和名称,通常为 <export_name>Config.cmake
  • NAMESPACE <namespace>:可选参数,为导出的目标添加命名空间,以防名称冲突。

export() 命令会生成一个 CMake 文件,其中包含已导出目标的信息(如位置、依赖项、编译选项),其他项目可以通过 find_package() 加载此配置文件,从而使用你项目中的目标。

通常和 install 配合使用,install定义需要导出的导出组,export 将导出组中的所有目标导出到配置文件。

install(EXPORT <export_name> [NAMESPACE <namespace>]
        [DESTINATION <dir>] [FILE <filename>]
        [OPTIONAL]
        [CONFIGURATIONS <config1> [<config2>...]]
        [INCLUDE_INSTALL_DIRS])

参数

  • DESTINATION <destination>:可选参数,指定导出文件的安装目录
  • CONFIGURATIONS <config1>:可选参数,指定要导出的配置(如:Debug,Release)
  • INCLUDE_INSTALL_DIRS:可选参数,导出的配置文件中包含安装路径

和 install(EXPORT) 命令的区别:

  • export(EXPORT):生成临时配置文件,在 构建阶段 将目标信息导出到构建目录,仅用于从构建目录导出目标
  • install(EXPORT):生成正式配置文件,并安装到系统,在 安装阶段 将目标信息导出到安装目录,然后从安装目录导出目标

2. configure_package_config_file

作用:根据 模板文件 生成 find_package 所需的 包配置文件,可以被其他项目使用来查找和配置你的包

函数原型

configure_package_config_file(
    <input_file>
    <output_file>
    INSTALL_DESTINATION <path>
    [NO_SET_AND_INCLUDEDIR]
    [NO_CHECK_REQUIRED_COMPONENTS_MACRO]
    [INSTALL_PREFIX <path>]
    [PATH_VARS <var1> <var2> ...]
)

参数

  • <input_file>: 模板文件,通常命名为 <PackageName>Config.cmake.in。
  • <output_file>: 生成的配置文件路径,通常命名为 <PackageName>Config.cmake。这个文件通常先生成在构建目录,然后通过 install() 安装到系统目录。
  • INSTALL_DESTINATION <path>(可选):指定了生成的配置文件在安装树中的相对路径。CMake 会用这个路径来计算 find_package 搜索到的实际安装路径。
  • NO_SET_AND_INCLUDEDIR(可选):如果指定,将不生成 set_and_check宏,用于设置变量并验证其路径是否存在
  • NO_CHECK_REQUIRED_COMPONENTS_MACRO(可选):如果指定,将不生成 check_required_components宏,用于检查包的所有必需组件是否都已找到并正确配置。

为什么需要它?

  • 当你将一个库(比如 MyMath)安装到系统中,并希望其他项目能通过 find_package(MyMath) 来使用它时,find_package 需要一个配置文件来知道去哪里找你的库、需要哪些头文件以及依赖哪些其他库。这个配置文件通常命名为 <PackageName>Config.cmake。
  • configure_package_config_file 的任务就是根据一个模板(*.in 文件)来生成这个最终的配置文件。

工作原理

configure_package_config_file 本质上是一个智能的 configure_file 命令,它专门用于处理包配置文件的模板。它会读取一个 *.cmake.in 模板文件,然后用一些特定的变量替换模板中的占位符,最终生成 *.cmake 文件。

它替换的占位符包括:

  • @PACKAGE_INIT@: 这是一个特殊的宏,它会展开为 CMake 内部的一些逻辑,用于设置 find_package 的变量,例如 _DIR 和 _ROOT,帮助 find_package 找到包的正确位置。
  • @<variable>@: 类似于 configure_file,它会用 CMake 变量的值替换模板中的 @variable@ 占位符。

3. write_basic_package_version_file

作用:生成一个包含版本信息和兼容性检查的包版本文件,使得其他项目能够轻松地使用你的包,并确保版本兼容性。

函数原型

write_basic_package_version_file(
    <filename>
    VERSION <major>.<minor>.<patch>
    [COMPATIBILITY <mode>]
)

参数

  • <filename>: 生成的包版本文件的路径和名称。这个文件通常位于构建目录,然后通过 install(FILES ...) 安装到正确的系统目录。
  • VERSION <major>.<minor>.<patch>: 你的库的版本号。这是最关键的参数。
  • COMPATIBILITY <mode>: 可选参数,用于指定版本兼容性模式。常见的模式有:
    • AnyNewerVersion: 任何比指定版本新的版本都兼容。这是最宽松的模式。
    • SameMajorVersion: 只有主版本号相同的版本才兼容。这是最常用的模式,遵循语义化版本(SemVer)规则。
    • SameMinorVersion: 只有主版本号和次版本号都相同的版本才兼容。
    • ExactVersion: 必须是完全相同的版本。

为什么需要它?

当其他开发者使用 find_package 查找你的库时,他们通常会指定一个版本号,例如 find_package(MyMath 1.2.0 REQUIRED)。

为了让 find_package 能够检查你的库是否满足这个版本要求,你需要提供一个版本文件。这个文件通常命名为 <PackageName>ConfigVersion.cmake,它包含了你的库的版本信息。write_basic_package_version_file 命令就是用来自动生成这个文件的。

工作原理

这个命令的工作非常简单直接:它根据你提供的版本信息,生成一个符合 CMake 规范的 .cmake 文件。

这个生成的文件通常会包含以下内容:

  • PACKAGE_VERSION:完整的版本字符串(例如 “1.2.3”)。
  • PACKAGE_VERSION_MAJOR:主版本号(例如 “1”)。
  • PACKAGE_VERSION_MINOR:次版本号(例如 “2”)。
  • PACKAGE_VERSION_PATCH:修订版本号(例如 “3”)。
  • PACKAGE_FIND_REQUIRED_VERSION:一个变量,用于检查 find_package 请求的版本是否满足。

三、静态库(查找-使用)

在上面我发布了一个数学静态库到系统的标准路径下,现在将使用 find_MyMath 工程演示下如何查找和引用发布的 MyMath 这个静态库。

目录结构

lighthouse@VM-8-10-ubuntu:find_MyMath$ tree
.
├── build
├── CMakeLists.txt
└── main.cpp

1 directory, 2 files

顶层 CMakeLists.txt

cmake_minimum_required(VERSION 3.18)
project(MyMathApp)

# 查找 MyMath 库
find_package(MyMath REQUIRED CONFIG)

add_executable(main main.cpp)
target_link_libraries(main PRIVATE MyMath::MyMath)

main.cpp

#include<iostream>
#include "math/math.h"

int main(){
    std::cout << "add(3,4) = " << add(3, 4) << std::endl;
    std::cout << "sub(3,4) = " << sub(3, 4) << std::endl;
}

执行 CMake,编译链接如下:

lighthouse@VM-8-10-ubuntu:build$ cmake ../
lighthouse@VM-8-10-ubuntu:build$ cmake --build .

lighthouse@VM-8-10-ubuntu:build$ ./main
add(3,4) = 7
sub(3,4) = -1

查看 main 的编译和链接过程

lighthouse@VM-8-10-ubuntu:build$ cmake --build . -v
/usr/bin/cmake -S/home/lighthouse/code/cmake/find_MyMath -B/home/lighthouse/code/cmake/find_MyMath/build --check-build-system CMakeFiles/Makefile.cmake 0
/usr/bin/cmake -E cmake_progress_start /home/lighthouse/code/cmake/find_MyMath/build/CMakeFiles /home/lighthouse/code/cmake/find_MyMath/build//CMakeFiles/progress.marks
/usr/bin/gmake  -f CMakeFiles/Makefile2 all
gmake[1]: Entering directory '/home/lighthouse/code/cmake/find_MyMath/build'
/usr/bin/gmake  -f CMakeFiles/main.dir/build.make CMakeFiles/main.dir/depend
gmake[2]: Entering directory '/home/lighthouse/code/cmake/find_MyMath/build'
cd /home/lighthouse/code/cmake/find_MyMath/build && /usr/bin/cmake -E cmake_depends "Unix Makefiles" /home/lighthouse/code/cmake/find_MyMath /home/lighthouse/code/cmake/find_MyMath /home/lighthouse/code/cmake/find_MyMath/build /home/lighthouse/code/cmake/find_MyMath/build /home/lighthouse/code/cmake/find_MyMath/build/CMakeFiles/main.dir/DependInfo.cmake --color=
gmake[2]: Leaving directory '/home/lighthouse/code/cmake/find_MyMath/build'
/usr/bin/gmake  -f CMakeFiles/main.dir/build.make CMakeFiles/main.dir/build
gmake[2]: Entering directory '/home/lighthouse/code/cmake/find_MyMath/build'
[ 50%] Building CXX object CMakeFiles/main.dir/main.cpp.o
/usr/bin/c++    -MD -MT CMakeFiles/main.dir/main.cpp.o -MF CMakeFiles/main.dir/main.cpp.o.d -o CMakeFiles/main.dir/main.cpp.o -c /home/lighthouse/code/cmake/find_MyMath/main.cpp
[100%] Linking CXX executable main
/usr/bin/cmake -E cmake_link_script CMakeFiles/main.dir/link.txt --verbose=1
/usr/bin/c++ CMakeFiles/main.dir/main.cpp.o -o main  /usr/local/lib/libMyMath.a 
gmake[2]: Leaving directory '/home/lighthouse/code/cmake/find_MyMath/build'
[100%] Built target main
gmake[1]: Leaving directory '/home/lighthouse/code/cmake/find_MyMath/build'
/usr/bin/cmake -E cmake_progress_start /home/lighthouse/code/cmake/find_MyMath/build/CMakeFiles 0
  • 配置阶段 (find_package): 当你运行 cmake .. 时,CMakeLists.txt 文件中的 find_package(MyMath REQUIRED) 命令开始执行。CMake 会在系统标准路径(例如 /usr/local/lib/cmake/MyMath)中搜索 MyMathConfig.cmake 这个配置文件。
  • 导入目标: MyMathConfig.cmake 文件被找到并执行。这个文件通常会包含一个 include(...) 命令,用于加载一个名为 MyMathTargets.cmake 的文件。这个文件是库的开发者在安装时通过 install(EXPORT ...) 命令生成的。它包含了关于 MyMath 目标的所有元数据,包括其在系统中的绝对路径:/usr/local/lib/libMyMath.a。当这个文件被加载后,CMake 会在你的项目中创建一个导入目标(IMPORTED target),通常命名为 MyMath::MyMath,并将库的绝对路径作为它的一个属性存储起来。
  • 构建阶段 (target_link_libraries): 当你执行 cmake --build . 时,CMake 会读取 target_link_libraries(main PRIVATE MyMath::MyMath) 这条命令。它会找到之前导入的 MyMath::MyMath 目标,并从其属性中获取库的绝对路径。
  • 链接器命令的生成: CMake 使用这个绝对路径来生成最终的链接器命令。从你的日志中可以看到,这条命令是: c++ CMakeFiles/main.dir/main.cpp.o -o main /usr/local/lib/libMyMath.a。
    • c++ ... -o main:这是标准的链接命令,将编译好的 main.cpp.o 目标文件链接成可执行文件 main。
    • /usr/local/lib/libMyMath.a:这是 CMake 自动解析出的静态库的完整路径。由于静态库在链接时就会被打包进可执行文件,所以这里直接使用了它的完整路径。这与动态库需要 -L 和 -l 选项的方式不同。

我们来看看其相关的一些重要命令

find_package

作用:在系统或项目中查找并加载一个外部软件包的配置,从而让自己的项目能够使用该包提供的库、头文件和工具

  • 语法:
    find_package(<PackageName> [version] [EXACT] [QUIET] [MODULE]
              [REQUIRED] [[COMPONENTS] [components...]]
              [OPTIONAL_COMPONENTS components...]
              [NO_POLICY_SCOPE])
    

关键参数

  • [version]:可选参数,用于指定所需的版本号。find_package 会使用 write_basic_package_version_file 生成的版本文件来检查版本是否兼容。
  • EXACT(可选):要求精确匹配指定版本
  • REQUIRED(可选):如果找不到包,CMake 就会报错并停止配置。
  • COMPONENTS <component>...:可选参数,用于指定包中需要使用的特定组件。
  • CONFIG/NO_MODULE:优先使用 Config 模式查找

【★,°:.☆( ̄▽ ̄)/$:.°★ 】那么本篇到此就结束啦,如果有不懂 和 发现问题的小伙伴可以在评论区说出来哦,同时我还会继续更新关于【CMake】的内容,请持续关注我 !!

在这里插入图片描述

更多推荐