CMake
CMake 是一个用于管理源代码构建的工具。最初,CMake 被设计为各种 Makefile 方言的生成器,如今 CMake 生成现代构建系统,如 Ninja,以及用于 Visual Studio 和 Xcode 等 IDE 的项目文件。
1. 简介
CMake 是一个开源的、跨平台的配置程序,用于构建、测试和打包软件,有时被称为“元”构建系统,简单来说,它就是一个用于管理源代码构建的工具。它使用简单的独立配置文件来控制软件的编译过程,根据项目、环境和用户提供的配置信息生成一个构建系统。注意 CMake 仅被设计用于与本地构建环境协同工作,而不负责运行生成软件构建的命令。
2. CMake 优势
2.1 为何选择 CMake?
CMake 是一个面向软件项目的开源构建系统生成器,开发人员可使用简单、可移植的文本文件格式来指定构建参数。之后,CMake 会使用该文件来生成面向本地构建工具(包括 Make/NMake 和 Ninja)的项目文件。 CMake 以简洁的方式处理软件构建的难点,如跨平台构建、系统自省和用户自定义构建,这使用户能够轻松地为复杂的硬件和软件系统定制构建。 CMake 的优势包括:
- 一套适用于所有平台的构建配置文件。
- 能够测试机器字节顺序和其他特定硬件特征。
- 能够自动搜索软件构建可能需要的程序、库和头文件。
- 能够在源代码树外部的目录树中构建。这允许开发者删除整个构建目录,而不必担心删除源文件。能够在配置时选择可选组件。例如,VTK 的一些库是可选的,而且 CMake 为用户提供了一种简单的方法来选择要构建的库。
- 能够轻松地在静态和共享构建之间切换。
- 可以使用系统相关的信息配置文件,例如数据文件的位置和其他信息。 CMake 可以创建包含信息的标头文件,例如数据文件的路径和其他信息,形式为 #define 宏。系统特定标志也可以放在已配置的标头文件中
2.2 为何不使用 Autotools?
Autotools 是一组用于为软件包创建 GNU 构建系统的工具,主要包括 Autoconf、Automake 和 Libtool 等,其中 Autoconf 主要处理 configure 相关的事情,而 Automake 则负责 Makefile 相关的工作。因为它源自 GNU 项目,所以被称为 GNU 构建系统。与 CMake 相比:
- 项目类型上:Autotools 在 GNU 项目上使用居多,CMake 在中间件及应用层 C++ 项目上使用居多,有的项目同时提供 Autotools 和 CMake 构建方式。
- 工作流程上:Autotools 是一个松散耦合的工具链:Autoconf 根据 configure.ac 生成 configure 脚本,Automake 根据 Makefile.am 生成 Makefile.in 模板,然后 configure 脚本在用户机器上运行,生成最终的 Makefile。Autoconf 与 Automake 组合在一起提供了与 CMake 相同的部分功能。CMake 是一个元构建系统,它读取 CMakeLists.txt,然后生成指定平台的原生构建文件,例如 Unix Makefiles、Ninja、Visual Studio 项目文件。
- 跨平台支持:Autotools 对 Unix-like 系统支持的很好,但对 Windows 的支持较差。CMake 原生支持 Windows,可以生成 Visual Studio 项目文件,直接使用 MSVC 编译器,无需额外的 Unix 兼容层,这使得它成为跨平台(尤其是同时覆盖 Unix 和 Windows)项目的首选。
- 学习成本上:对于 Autotools 开发者需要同时掌握 Autoconf 的 M4 宏语法和 Automake 的 Makefile 语法,往往晦涩难懂,有时需要深入理解 shell、M4、sed 等多种工具。CMake 的 CMakeLists.txt 语法是自定义的,一致性更好,只需学习一种语法,通常比 Autotools 更易理解。
3. 基本的 CMake 用法
CMake 以一个或多个 CMakeLists 文件作为输入,并生成项目文件或 Makefile,以供各种本地开发工具使用。典型的 CMake 工作流程如下:
- 在一个或多个 CMakeLists 文件中定义项目构建任务
- 配置并生成项目
- 使用喜欢的本地开发工具构建项目
3.1 创建 CMakeLists 文件
CMakeLists 文件(实际上是 CMakeLists.txt,但通常省略扩展名)使用 cmake-language 编写,表现为一系列注释、命令和变量组成项目描述的纯文本文件。首先,让我们考虑最简单的 CMakeLists 文件。要从一个源文件编译一个可执行文件,CMakeLists 文件将包含三行:
1
2
3
cmake_minimum_required(VERSION 3.20)
project(Hello)
add_executable(Hello Hello.c)
CMakeLists 文件的第一行应始终是 cmake_minimum_required,指定项目要求的 CMake 最小版本。任何 CMakeLists 文件的下一行应该是 project 命令,此命令设置项目的名称。最后,使用 add_executable 命令将给定的源文件添加向项目可执行文件。在此示例中,源目录中有两个文件:CMakeLists.txt 和 Hello.c。
3.2 配置和生成
创建 CMakeLists 文件后,CMake 会处理文本文件并在缓存文件中创建条目。接下来,CMake 使用缓存条目在用户所需的构建系统(例如 Makefile 或 Visual Studio 解决方案)中生成项目。
3.2.1 从命令行运行 CMake
从命令行,可以使用 cmake 可执行文件生成项目构建系统。要使用 cmake 构建项目,首先创建并切换到您希望放置二进制文件的目录。运行 cmake 并指定源码树的路径,并使用 -D 标志传入任何选项。
- cmake -S dir
- 指定项目根目录,CMake 将在此找到要构建的项目。它包含根 CMakeLists.txt 文件。如果未指定,则默认为当前工作目录。
- cmake -B dir
- 指定构建目录,CMake 将在此输出生成的构建系统的文件,以及在运行构建系统时产生的缓存文件。如果未指定,则默认为当前工作目录。
- cmake –build dir
- 在指定的构建目录中运行构建系统。
- 向 CMake 指定生成器
- CMake 支持将多个构建系统作为配置过程的输出,这些输出后端被称为“生成器”。CMake 生成器负责为原生构建系统编写输入文件。必须为构建树选择 CMake 生成器中的一个来决定使用哪个原生构建系统。使用 -G 选项来为新的构建树指定生成器。
- 向 CMake 指定编译器
- 在某些系统上,您可能有多个编译器可供选择,或者您的编译器可能位于非标准位置。在这些情况下,您需要向 CMake 指定所需编译器的位置。有三种方法可以指定:生成器可以指定编译器;可以设置环境变量;或者可以设置缓存条目。可以使用在运行 CMake 之前设置的环境变量来抢占这些列表。CC 环境变量指定 C 编译器,而 CXX 指定 C++ 编译器。例如,您可以使用 -DCMAKE_CXX_COMPILER=cl 在命令行上直接指定编译器。编译器和链接器的标志也可以通过设置环境变量来更改,设置 LDFLAGS 将初始化链接标志的缓存值,而 CXXFLAGS 和 CFLAGS 将分别初始化 CMAKE_CXX_FLAGS 和 CMAKE_C_FLAGS。
- 指定构建配置
- 构建配置允许以调试、优化等不同的方式构建项目。默认情况下,CMake 支持 Debug、Release、MinSizeRel 和 RelWithDebInfo 配置。Debug 是启用基本的调试标志。Release 是启用基本的优化标志。MinSizeRel 具有生成最小目标代码的标志,但不一定是速度最快的代码。RelWithDebInfo 构建具有调试信息的优化版本。
构建类型 编译选项 适用场景 Debug -g 开发调试阶段,需要断点和变量查看 Release -O2 -DNDEBUG 生产发布,追求运行速度 RelWithDebInfo -O2 -g 需要性能分析或调试优化的发布版本 MinSizeRel -Os 嵌入式或对体积敏感的场景
对于基于 Makefile 的生成器,在运行 CMake 时只能激活一个配置,并使用 CMAKE_BUILD_TYPE 变量指定。如果变量为空,则不会向构建添加任何标志。如果变量设置为配置的名称,则相应的变量和规则(例如 CMAKE_CXX_FLAGS_
1
2
3
4
5
# With source code in the directory MyProject
# to build MyProject-debug create that directory, cd into it and
cmake ../MyProject -DCMAKE_BUILD_TYPE=Debug
# the same idea is used for the release tree MyProject-release
cmake ../MyProject -DCMAKE_BUILD_TYPE=Release
3.2.2 从 GUI 运行 CMake
更习惯 GUI 界面的用户可以使用 cmake-gui 工具来调用 CMake 并生成构建系统。请查看 CMake GUI 工具交互指南。
3.3 构建您的项目
运行 CMake 后,您的项目将准备好构建。如果您的目标生成器基于 Makefile,则可以通过将目录更改为您的二进制树并键入 make(或 gmake 或 nmake,具体取决于情况)来构建项目。对于简单的项目,安装和运行 CMake 就这么简单。
4. CMake 语言基础
CMake 提供了一种图灵完备的领域特定语言来描述软件的构建过程。这门语言正式称为“CMake 语言”,或者更通俗地说,称为 CMakeLang。CMakeLang 中唯一的基础类型是字符串和列表。CMake 中的每个对象都是字符串,而列表本身也是包含分号作为分隔符的字符串。任何看起来操作非字符串(如布尔值、数字、JSON 对象等)的命令,实际上都是在解析一个字符串,执行一些内部转换逻辑(使用 CMakeLang 以外的语言),然后为任何潜在的输出转换回字符串。
4.1 注释
注释以 # 开头,该字符不在 括号参数、引号参数 中,也未被不带引号参数中的反斜杠转义。有两种类型的注释:括号注释 和 行注释。
4.1.1 括号注释
一个紧跟 bracket_open 的 # 形成一个括号注释,包含整个括号的范围。例如:
1
2
3
#[[This is a bracket comment.
It runs until the close bracket.]]
message("First Argument\n" #[[Bracket Comment]] "Second Argument")
4.1.2 行注释
一个未紧跟 bracket_open 的 # 形成一个行注释,该注释一直持续到行尾。例如:
1
2
3
# This is a line comment.
message("First Argument\n" # This is a line comment :)
"Second Argument") # This is a line comment.
4.2 变量
变量是 CMake 语言中存储的基本单元。它们的值始终是字符串类型。变量名称区分大小写,并且可以包含几乎任何文本,但我们建议坚持使用仅包含字母数字字符加上 _ 和 - 的名称。
4.2.1 变量类型
变量可分为普通变量、环境变量和缓存变量。
4.2.1.1 普通变量
使用 set() 和 unset() 命令可显式设置或取消设置变量。
使用 set() 命令创建一个变量,也就是一个字符串的名称。并可以使用花括号扩展来访问变量的值。
1
2
set(var "World!")
message("Hello ${var}")
通过脚本模式去验证:
1
2
$ cmake -P CMakeLists.txt
Hello World!
提示:cmake -P 被称为“脚本模式”,它告诉 CMake 该文件不包含 project() 命令。我们不构建任何软件,而是仅将 CMake 用作命令解释器。
CMake 内置了五种类别的变量,分别是提供信息、改变行为、描述系统、控制构建和用于语言的变量。
| 改变行为的内置变量 | 作用 |
|---|---|
BUILD_SHARED_LIBS | 在调用 add_library() 时若未明确指定库类型,则默认使用 SHARED(动态)库而非 STATIC(静态)库 |
CMAKE_BUILD_TYPE | 指定构建类型,典型值包括 Debug, Release, RelWithDebInfo 和 MinSizeRel |
CMAKE_INSTALL_PREFIX | 指定安装目录 |
CMAKE_INCLUDE_PATH | 指定 find_file() 和 find_path() 命令的搜索路径列表 |
CMAKE_LIBRARY_PATH | 指定 find_library() 命令的搜索路径目录列表 |
CMAKE_PREFIX_PATH | 指定 find_package()、find_library() 等的搜索前缀列表 |
CMAKE_PROGRAM_PATH | 指定 find_program() 命令的搜索路径目录列表 |
CMAKE_MODULE_PATH | 指定由 include() 或 find_package() 命令加载 CMake 模块的搜索路径 |
CMAKE_SYSROOT | 指定编译器的 –sysroot 标志的根目录路径 |
CMAKE_FIND_ROOT_PATH | 指定备选的根目录查找路径列表 |
CMAKE_FIND_ROOT_PATH_MODE_INCLUDE | 控制查找文件和目录时, 是否搜索 CMAKE_FIND_ROOT_PATH 和 CMAKE_SYSROOT 中定义的路径 |
CMAKE_FIND_ROOT_PATH_MODE_LIBRARY | 控制查找库时, 是否搜索 CMAKE_FIND_ROOT_PATH 和 CMAKE_SYSROOT 中定义的路径 |
CMAKE_FIND_ROOT_PATH_MODE_PACKAGE | 控制查找包时, 是否搜索 CMAKE_FIND_ROOT_PATH 和 CMAKE_SYSROOT 中定义的路径 |
CMAKE_FIND_ROOT_PATH_MODE_PROGRAM | 控制查找程序时, 是否搜索 CMAKE_FIND_ROOT_PATH 和 CMAKE_SYSROOT 中定义的路径 |
CMAKE_FIND_USE_SYSTEM_ENVIRONMENT_PATH | 默认为 TRUE,决定是否搜索由标准系统环境变量提供的路径 |
| 描述系统的内置变量 | 作用 |
|---|---|
CMAKE_SYSTEM_NAME | 目标操作系统名称:Linux/Windows/Android |
CMAKE_SYSTEM_PROCESSOR | 目标 CPU 架构:x86_64/aarch64/loongarch64 |
CMAKE_HOST_SYSTEM_NAME | 本机主机操作系统名称,即 CMake 运行所在的操作系统的名称 |
CMAKE_HOST_SYSTEM_PROCESSOR | 本机主机 CPU 架构,即 CMake 运行所在的机器 CPU 名称 |
UNIX | 当目标系统为 UNIX 或类 UNIX 系统时,设置为 true |
WIN32 | 当目标系统为 Windows(包括 Win64)时,设置为 true |
LINUX | 当目标系统为 Linux 时,设置为 true |
ANDROID | 当目标系统为 Android 时,设置为 true |
| 控制构建的内置变量 | 作用 |
|---|---|
CMAKE_LINKER_TYPE | 指定链接步骤将使用的链接器 |
CMAKE_EXE_LINKER_FLAGS | 指定可执行文件的链接标志 |
CMAKE_DEBUG_POSTFIX | Debug 版本库文件名后缀 |
CMAKE_INCLUDE_CURRENT_DIR | 自动将当前的源码目录和构建目录添加到包含路径中 默认情况下, CMAKE_INCLUDE_CURRENT_DIR 为 OFF |
CMAKE_SHARED_LINKER_FLAGS | 指定动态库的链接标志 |
CMAKE_STATIC_LINKER_FLAGS | 指定静态库的链接标志 |
CMAKE_ARCHIVE_OUTPUT_DIRECTORY | 指定构建时所有 ARCHIVE 目标文件的存放位置 |
CMAKE_LIBRARY_OUTPUT_DIRECTORY | 指定构建时所有 LIBRARY 目标文件的存放位置 |
CMAKE_RUNTIME_OUTPUT_DIRECTORY | 指定构建时所有 RUNTIME 目标文件的存放位置 |
CMAKE_POSITION_INDEPENDENT_CODE | 设置非 SHARED 或 MODULE 库目标上的 POSITION_INDEPENDENT_CODE 属性无论如何设置此变量,SHARED 和 MODULE 库目标均默认启用 PIC |
| 用于语言的内置变量 | 作用 |
|---|---|
CMAKE_C_COMPILER | 指定 C 编译器的完整路径 |
CMAKE_C_COMPILER_ID | 指定 C 编译器标识字符串 |
CMAKE_C_STANDARD | 用于设置 C_STANDARD,指定构建目标时所请求的 C 标准特性 |
CMAKE_C_STANDARD_REQUIRED | 布尔值,描述 C_STANDARD 的值是否为强制要求 |
CMAKE_C_FLAGS | 指定 C 语言编译选项 |
CMAKE_C_FLAGS_DEBUG | C 语言在 Debug 配置下构建时使用的编译选项 |
CMAKE_C_FLAGS_RELEASE | C 语言在 Release 配置下构建时使用的编译选项 |
CMAKE_C_FLAGS_RELWITHDEBINFO | C 语言在 RelWithDebInfo 配置下构建时使用的编译选项 |
CMAKE_C_FLAGS_MINSIZEREL | C 语言在 MinSizeRel 配置下构建时使用的编译选项 |
CMAKE_CXX_COMPILER | CXX 编译器的完整路径 |
CMAKE_CXX_COMPILER_ID | 指定 CXX 编译器标识字符串 |
CMAKE_CXX_STANDARD | 用于设置 CXX_STANDARD,指定构建目标时所请求的 CXX 标准特性 |
CMAKE_CXX_STANDARD_REQUIRED | 布尔值,描述 CXX_STANDARD 的值是否为强制要求 |
CMAKE_CXX_FLAGS | 指定 CXX 语言编译选项 |
CMAKE_CXX_FLAGS_DEBUG | CXX 语言在 Debug 配置下构建时使用的编译选项 |
CMAKE_CXX_FLAGS_RELEASE | CXX 语言在 Release 配置下构建时使用的编译选项 |
CMAKE_CXX_FLAGS_RELWITHDEBINFO | CXX 语言在 RelWithDebInfo 配置下构建时使用的编译选项 |
CMAKE_CXX_FLAGS_MINSIZEREL | CXX 语言在 MinSizeRel 配置下构建时使用的编译选项 |
4.2.1.2 环境变量
根据用途可分为改变行为、控制构建和针对编程语言三类环境变量,使用 cmake -E environment 命令行工具可显示所有当前环境变量。
改变行为的环境变量
环境变量 作用 CMAKE_INCLUDE_PATH供 find_file() 和 find_path() 命令搜索。 CMAKE_LIBRARY_PATH供 find_library() 命令在其中进行搜索。 CMAKE_PROGRAM_PATH供 find_program() 命令搜索。 这些路径类型环境变量可以被设置为单个目录,或在 UNIX 上用 : 分隔,在 Windows 上用 ; 分隔的目录列表。
控制构建的环境变量
环境变量 作用 说明 CMAKE_BUILD_TYPE首次构建时为 CMAKE_BUILD_TYPE 变量提供默认值 CMAKE_GENERATOR未使用 -G指定生成器时,作为 CMake 默认生成器传入无效值则使用 CMake 内置默认,无论如何最终生效值存入 CMAKE_GENERATOR 变量 CMAKE_INSTALL_PREFIX覆盖 CMake 默认安装前缀 指定目录值必须是绝对路径 CMAKE_TOOLCHAIN_FILE首次构建时指定默认工具链文件 DESTDIR在 UNIX 系统上重定向安装路径 例如:make DESTDIR=/package/stage install VERBOSE开启 CMake 与底层构建工具的详细日志输出 CMAKE_NO_VERBOSE禁用来自 CMake 的详细输出 针对编程语言的环境变量
环境变量 作用 说明 CC指定 C 语言编译器 仅在首次配置生效,之后将存入 CMAKE_C_COMPILER 缓存;对于任何配置运行(包括首次),如果定义了 CMAKE_C_COMPILER 变量则该环境变量将被忽略 CFLAGS添加编译 C 文件时使用的编译选项 仅在首次配置生效;对于任何配置运行(包括首次),如果定义了 CMAKE_C_FLAGS 变量,则该环境变量将被忽略。 CXX指定 CXX 语言编译器 仅在首次配置生效,之后将存入 CMAKE_CXX_COMPILER 缓存;对于任何配置运行(包括首次),如果定义了 CMAKE_CXX_COMPILER 变量则该环境变量将被忽略 CXXFLAGS添加编译 C 文件时使用的编译选项 仅在首次配置生效;对于任何配置运行(包括首次),如果定义了 CMAKE_CXX_FLAGS 变量,则该环境变量将被忽略。
4.2.1.3 缓存变量
由 -D 标志和 option() 命令创建的是缓存(cache)变量。通常保存在 CMakeCache.txt 文件中。缓存变量是全局可见的变量,且具有“粘性”,一旦设置后就很难改变。如果一个缓存变量被设置了一次,它将一直保留,直到被另一个 -D 标志覆盖。set() 也可以用来操作缓存变量,但不会改变已经创建的变量。
由于 -D 标志在任何项目命令之前处理,因此它们在设置缓存变量的值时具有优先权。
虽然缓存变量通常无法更改,但它们可以被普通变量隐藏(shadowed)。我们可以通过使用 set() 设置一个与缓存变量同名的变量,然后使用 unset() 删除该普通变量来观察这一点。
4.2.2 变量引用
- 普通变量
- 引用形式为
${<variable>},并在 带引号参数 或 不带引号参数 中进行评估。变量引用将被指定变量或缓存条目的值替换,如果两者都未设置,则替换为空字符串。变量引用可以嵌套,并从内向外评估,例如:${outer_${inner_variable}_variable}。 - 环境变量
- 引用形式为
$ENV{<variable>}。 - 缓存变量
- 引用形式为
$CACHE{<variable>},它会被指定缓存条目的值替换,而无需检查同名的常规变量。如果缓存条目不存在,则替换为空字符串。
注意:
if()命令具有特殊条件语法,允许使用<variable>的短形式而不是${<variable>}进行变量引用。但是,环境变量始终需要引用为$ENV{<variable>}。
4.2.3 变量作用域
变量具有动态作用域。
- 块作用域
block()命令可以为变量绑定创建新的作用域。- 函数作用域
function()命令可以为变量绑定创建新的作用域。- 目录作用域
- 不在函数调用内部的 “设置” 或 “取消设置” 操作会绑定到当前目录作用域。源码树中的每个目录都有其自己的变量绑定。在处理目录的 CMakeLists.txt 文件之前,CMake 会复制父目录中当前定义的所有变量绑定(如果有),以初始化新的目录作用域。
- 持久化缓存
- CMake 存储了一组独立的“缓存”变量或“缓存条目”,其值在项目构建树的多次运行中保持不变。缓存条目具有独立的绑定作用域,仅通过显式请求进行修改,例如使用 set() 和 unset() 命令的 CACHE 选项。
提示:环境变量在 CMake 进程中具有全局作用域,它们永远不会被缓存。
4.3 列表
CMake 中的列表是一组用 ; 分隔的字符串。CMake 中没有数组,只有列表,它是 CMake 最基础的容器。要创建列表,可以使用 set() 命令。例如:
1
2
set(srcs a.c b.c c.c) # sets "srcs" to "a.c;b.c;c.c"
message(${srcs}) # the output is "a.c b.c c.c"
列表子命令分为三类,包括读取(GET、LENGTH、SUBLIST)、搜索(FIND)和修改(APPEND、PREPEND、INSERT、REMOVE_ITEM、REMOVE_AT、REMOVE_DUPLICATES、REVERSE、SORT、TRANSFORM)。
| 子命令 | 作用 | 示例 |
|---|---|---|
| GET | 返回列表中指定索引对应的元素列表 | list(GET <list> <element index> [<index> ...] <out-var>) |
| LENGTH | 获取列表元素个数 | list(LENGTH <list> <output variable>) |
| SUBLIST | 截取一段子列表 | list(SUBLIST <list> <begin> <length> <out-var>) |
| FIND | 查找元素,返回索引;找不到返回-1 | list(FIND <list> <value> <out-var>) |
| APPEND | 追加元素到列表尾部 | list(APPEND <list> [<element>...]) |
| PREPEND | 插入到列表头部 | list(PREPEND <list> [<element>...]) |
| INSERT | 在指定索引位置插入元素 | list(INSERT <list> <element_index> <element> [<element> ...]) |
| REMOVE_ITEM | 从列表中移除指定项目的所有实例 | list(REMOVE_ITEM <list> <value> [<value> ...]) |
| REMOVE_AT | 移除列表中给定索引处的项目 | list(REMOVE_AT <list> <index> [<index> ...]) |
| REMOVE_DUPLICATES | 移除列表中的重复项目 | list(REMOVE_DUPLICATES <list>) |
| REVERSE | 反转列表 | list(REVERSE <list>) |
| SORT | 按字母顺序原地排序列表 | list(SORT <list> [...]) |
| TRANSFORM | 将 <ACTION> 应用于所有元素 | list(TRANSFORM <list> <ACTION> [...]) |
4.3 命令
CMake 命令分为 脚本命令、项目命令 和 CTest 命令。
4.3.1 命令参数
命令调用中有三种类型的参数:括号参数、引号参数和无引号参数。
4.3.1.1 括号参数
将内容括在等长的左右“括号”之间。左括号的写法是 [ 后跟零个或多个 =,再后跟 [。对应的右括号写法是 ] 后跟相同数量的 =,再后跟 ]。括号不能嵌套。
括号参数的内容包含左右括号之间的所有文本,但紧跟在左括号后的换行符(如果有)会被忽略。不对括起来的内容进行任何求值(如 转义序列 或 变量引用)。括号参数始终作为一个单一参数传递给命令调用。
1
2
3
4
5
6
7
message([=[
This is the first line in a bracket argument with bracket length 1.
No \-escape sequences or ${variable} references are evaluated.
This is always one argument even though it contains a ; character.
The text does not end on a closing bracket of length 0 like ]].
It does end in a closing bracket of length 1.
]=])
4.3.1.2 带引号参数
引号参数将内容括在左右双引号字符之间。引号参数的内容包含左右引号之间的所有文本。转义序列 和 变量引用 都会被求值。引号参数始终作为一个单一参数传递给命令调用。
1
2
3
4
5
6
message("This is a quoted argument containing multiple lines.
This is always one argument even though it contains a ; character.
Both \\-escape sequences and ${variable} references are evaluated.
The text does not end on an escaped double-quote like \".
It does end in an unescaped double quote.
")
在任何以奇数个反斜杠结尾的行上,最后的 \ 会被视为续行符,并与紧随其后的换行符一起被忽略。例如:
1
2
3
4
5
message("\
This is the first line of a quoted argument. \
In fact it is the only line but since it is long \
the source code uses line continuation.\
")
4.3.1.3 不带引号参数
无引号参数不被任何引号语法括起来。它不能包含任何空格、(、)、#、" 或 \,除非这些字符被反斜杠转义。无引号参数的内容由连续的允许字符或转义字符块组成。转义序列 和 变量引用 都会被求值。生成的值会按照 列表 分割元素的方式进行拆分。每个非空元素都被作为一个参数传递给命令调用。因此,一个无引号参数可能作为零个或多个参数传递给命令调用。
1
2
3
4
5
6
7
8
foreach(arg
NoSpace
Escaped\ Space
This;Divides;Into;Five;Arguments
Escaped\;Semicolon
)
message("${arg}")
endforeach()
4.3.2 命令调用
命令调用是一个名称,后跟括号括起来的由空格分隔的参数。例如:
1
add_executable(hello world.c)
命令名称不区分大小写。
4.3.3 命令分类
4.3.3.1 脚本命令
可在脚本模式下执行的命令称为脚本命令,常用的脚本命令列举如下:
| 类型 | 作用 |
|---|---|
cmake_host_system_information | 查询各类主机系统信息 |
cmake_minimum_required | 设置项目的 CMake 最低要求版本 |
cmake_parse_arguments | 解析函数或宏的参数 |
cmake_path | 路径操作 |
cmake_pkg_config | 处理 pkg-config 格式的包文件 |
configure_file | 将文件复制到另一个位置并修改其内容 |
block / endblock | 在一个专用的变量和/或策略作用域内执行一组命令 |
break | 从外层的 foreach 或 while 循环中跳出 |
continue | 继续执行外层 foreach 或 while 循环的顶部 |
if / elseif / else / endif | 有条件地执行一组命令 |
execute_process | 执行一个或多个子进程 |
file | 文件操作 |
find_file | 查找指定文件的完整路径 |
find_library | 查找库 |
find_package | 查找包 |
find_path | 查找包含指定文件的目录 |
find_program | 查找程序 |
foreach / endforeach | 针对列表中的每一项执行一组命令 |
function / endfunction | 定义一个函数 |
get_directory_property | 获取目录作用域的属性 |
get_filename_component | 获取完整文件名的特定部分 |
get_property | 获取一个属性 |
include | 从文件或模块加载并运行 CMake 代码 |
list | 对分号分隔的列表进行操作 |
macro / endmacro | 定义一个宏 |
message | 记录并输出消息 |
option | 提供可以选择的布尔选项 |
return | 从文件、目录或函数中返回 |
separate_arguments | 将命令行参数解析为以分号分隔的列表 |
set / unset | 设置/取消变量、缓存变量或环境变量 |
set_directory_properties | 设置当前目录及其子目录的属性 |
set_property | 在给定的作用域内设置命名的属性 |
string | 字符串操作 |
variable_watch | 监视 CMake 变量的变化 |
while / endwhile | 在条件为真时执行一组命令 |
4.3.3.2 项目命令
只能在项目模式中使用,不能在脚本模式中使用的命令为项目命令,常用的项目命令列举如下:
| 类型 | 作用 |
|---|---|
add_compile_definitions | 向编译器添加预处理器定义 |
add_compile_options | 向编译器添加编译选项,这些选项不会用于链接阶段 |
add_custom_command | 添加自定义命令 |
add_custom_target | 添加自定义目标 |
add_definitions | 为源文件的编译添加 -D 定义标志 |
add_dependencies | 在顶层目标之间添加依赖关系 |
add_executable | 添加一个可执行文件 |
add_library | 添加一个库目标 |
add_link_options | 添加链接选项,不能用于为静态库目标添加选项,因为它们不使用链接器 |
add_subdirectory | 向构建中添加一个子目录 |
aux_source_directory | 收集指定目录中所有源文件的名称 |
get_source_file_property | 获取源文件的属性 |
get_target_property | 获取目标的属性 |
include_directories | 向构建中添加头文件搜索目录 |
install | 指定在安装时运行的规则 |
link_directories | 添加链接器查找库时所使用的目录 |
project | 设置项目名称 |
remove_definitions | 移除由 add_compile_definitions() 或 add_definitions() 添加的编译定义 |
set_source_files_properties | 设置源文件的属性 |
set_target_properties | 设置目标的属性 |
target_compile_definitions | 向目标添加编译定义 |
target_compile_features | 向目标添加所需的编译器特性 |
target_compile_options | 向目标添加编译选项 |
target_include_directories | 为目标添加包含目录 |
target_link_directories | 为目标添加链接目录 |
target_link_libraries | 为目标添加链接库 |
target_link_options | 为目标添加链接选项 |
target_precompile_headers | 添加一个需要预编译的头文件列表 |
target_sources | 向目标添加源文件 |
try_compile | 尝试构建一些代码 |
try_run | 尝试编译并运行一些代码 |
4.4 函数和宏
通过将相同或相似的功能进行抽象形成宏或函数,可避免在文件中出现大量重复相似的命令集,这可以借助 function() 和 macro() 来实现。
与许多语言一样,函数和宏的区别在于作用域。macro() 在语义上类似于文本替换,类似于 C/C++ 宏,因此宏产生的任何副作用在调用它的上下文中都是可见的。如果我们宏中创建或更改一个变量,调用者将会看到该更改。function() 会创建自己的变量作用域,因此副作用对调用者不可见。为了将更改传播给调用该函数的父级,我们必须使用 set(<var> <value> PARENT_SCOPE),它的工作方式与 set() 相同,但仅针对属于调用者上下文的变量。
4.5 控制结构
4.5.1 条件块
有条件地执行一组命令,由 if() 命令组织。
1
2
3
4
5
6
7
if(<condition>)
<commands>
elseif(<condition>) # optional block, can be repeated
<commands>
else() # optional block
<commands>
endif()
根据下文描述的 条件语法 评估 if 子句的 condition 参数。如果结果为真,则执行 if 块中的 commands。否则,将以相同方式处理可选的 elseif 块。最后,如果没有任何 condition 为真,则执行可选的 else 块中的 commands。
4.5.1.1 基础表达式
if(<constant>)- 如果常量为 1、ON、YES、TRUE、Y 或非零数字(包括浮点数),则为真。如果常量为 0、OFF、NO、FALSE、N、IGNORE、NOTFOUND、空字符串或以 -NOTFOUND 后缀结尾,则为假。布尔常量不区分大小写。如果参数不是这些特定常量之一,它将被视为变量或字符串(请参阅下文的 变量展开),并适用以下两种形式之一。
if(<variable>)- 如果给定的变量定义为非假常量的某个值,则为真。否则为假,包括变量未定义的情况。注意,宏参数不是变量。环境变量 也不能以这种方式测试,例如 if(ENV{some_var}) 的求值结果永远为假。
if(<string>)- 引号括起来的字符串总是被判定为假,除非字符串的值是真常量之一。
if()- 如果未提供参数,则为假。
4.5.1.2 逻辑运算符
if(NOT <condition>)- 如果条件不为真,则为真。
if(<cond1> AND <cond2>)- 如果两个条件都为真,则为真。
if(<cond1> OR <cond2>)- 如果两个条件中任一条件被视为真,则为真。
if((condition) AND (condition OR (condition)))- 圆括号内的条件首先被求值,然后剩余的条件按其他示例中的方式求值。如果存在嵌套的圆括号,最内层的圆括号会作为评估包含它们的条件的一部分进行求值。
4.5.1.3 存在性检查
if(COMMAND <command-name>)- 如果给定名称是可调用的命令、宏或函数,则为真。
if(TARGET <target-name>)- 如果给定名称是一个已存在的逻辑目标名称,且已通过调用 add_executable()、add_library() 或 add_custom_target() 命令(在任何目录中)创建,则为真。
if(TEST <test-name>)- 如果给定名称是由 add_test() 命令创建的现有测试名称,则为真。
if(DEFINED <name>|CACHE{<name>}|ENV{<name>})- 如果定义了具有给定
的变量、缓存变量或环境变量,则为真。变量的值无关紧要。注意以下注意事项: - 宏参数不是变量。
- 无法直接测试
是否为非缓存变量。如果缓存变量或非缓存变量 someName 存在,表达式 if(DEFINED someName) 将为真。相比之下,表达式 if(DEFINED CACHE{someName}) 仅在缓存变量 someName 存在时才为真。如果您需要了解非缓存变量是否存在,则必须测试这两个表达式:if(DEFINED someName AND NOT DEFINED CACHE{someName})。
if(<variable|string> IN_LIST <variable>)- 如果给定的元素包含在命名的列表变量中,则为真。
4.5.1.4 文件操作
if(EXISTS <path-to-file-or-directory>)- 如果命名文件或目录存在且可读,则为真。行为仅对明确的完整路径定义良好(路径开头的 ~/ 不会展开为主目录,并被视为相对路径)。解析符号链接,即如果命名文件或目录是符号链接,则在符号链接的目标存在时返回真。如果给定路径为空字符串,则为假。
if(IS_READABLE <path-to-file-or-directory>)- 如果命名文件或目录可读,则为真。行为仅对明确的完整路径定义良好。解析符号链接,即如果命名文件或目录是符号链接,则在符号链接的目标可读时返回真。如果给定路径为空字符串,则为假。
if(IS_WRITABLE <path-to-file-or-directory>)- 如果命名文件或目录可写,则为真。行为仅对明确的完整路径定义良好。解析符号链接,即如果命名文件或目录是符号链接,则在符号链接的目标可写时返回真。如果给定路径为空字符串,则为假。
if(IS_EXECUTABLE <path-to-file-or-directory>)- 如果命名文件或目录可执行,则为真。行为仅对明确的完整路径定义良好。解析符号链接,即如果命名文件或目录是符号链接,则在符号链接的目标可执行时返回真。如果给定路径为空字符串,则为假。
if(IS_DIRECTORY <path>)- 如果 path 是一个目录,则为真。行为仅对完整路径定义良好。如果给定路径为空字符串,则为假。
if(IS_SYMLINK <path>)- 如果给定路径是符号链接,则为真。行为仅对完整路径定义良好。
if(IS_ABSOLUTE <path>)- 如果给定路径是绝对路径,则为真。注意以下特殊情况:
- 空 path 会评估为假。
- 在 Windows 主机上,任何以驱动器号加冒号(例如 C:)、正斜杠或反斜杠开头的 path 都会评估为真。
- 在非 Windows 主机上,任何以波浪号(~)开头的 path 都会评估为真。
if(<file1> IS_NEWER_THAN <file2>)- 如果 file1 比 file2 新,或者两个文件中的一个不存在,则为真。行为仅对完整路径定义良好。如果文件时间戳完全相同,IS_NEWER_THAN 比较将返回真,以便在平局时发生任何相关的构建操作。这包括为 file1 和 file2 传递相同文件名的情形。
4.5.1.4 比较
if(<variable|string> MATCHES <regex>)- 如果给定的字符串或变量的值与给定的正则表达式匹配,则为真。
if(<variable|string> LESS/GREATER/EQUAL/LESS_EQUAL/GREATER_EQUAL <variable|string>)- 如果给定的字符串或变量的值被解析为实数(类似于 C 语言中的 double)且小于/大于/等于/小于等于/大于等于右侧的值,则为真。
if(<variable|string> STRLESS/STRGREATER/STREQUAL/STRLESS_EQUAL/STRGREATER_EQUAL <variable|string>)- 如果给定的字符串或变量的值在字典序上小于/大于/等于/小于等于/大于等于右侧的字符串或变量,则为真。
4.5.2 循环
CMake 提供两种循环结构:while() 以及更有用的 foreach()。
4.5.2.1 foreach
针对列表中的每个值评估一组命令。
1
2
3
foreach(<loop_var> <items>)
<commands>
endforeach()
其中 <items> 是由分号或空格分隔的项目列表。foreach 和对应的 endforeach 之间的所有命令都会被记录而不会立即执行。一旦评估到 endforeach,记录的命令列表将针对 <items> 中的每个项目执行一次。在每次迭代开始时,变量 <loop_var> 将被设置为当前项目的值。
提示:
<loop_var>的作用域仅限于循环内部。
foreach 在上述标准格式基础上存在多种变体,如:
foreach(<loop_var> RANGE <stop>)- 在此变体中,
foreach遍历从 0、1 开始直到(并包含)非负整数<stop>的数字。 foreach(<loop_var> RANGE <start> <stop> [<step>])- 在此变体中,
foreach以<step>为步长,遍历从<start>开始到最多<stop>的数字。如果未指定<step>,则步长为 1。三个参数 <start>、<stop>和<step>都必须是非负整数,且<stop>不得小于<start>,否则,将引发未定义行为。 foreach(<loop_var> IN [LISTS [<lists>]] [ITEMS [<items>]])- 在此变体中,
<lists>是由空格或分号分隔的列表变量列表。foreach命令遍历每个给定列表中的每一项。ITEMS关键字后面的<items>将按照foreach命令的第一种变体进行处理。格式LISTS A和ITEMS ${A}是等效的。如果没有提供<lists>或<items>,则循环体永远不会被执行(即被处理为空)。 foreach(<loop_var>... IN ZIP_LISTS <lists>)- 在此变体中,
<lists>是由空格或分号分隔的列表变量列表。foreach命令同时遍历每个列表,并按如下方式设置迭代变量:- 如果只给定一个 loop_var,则它会设置一系列 loop_var_N 变量为对应列表中的当前项;
- 如果传递了多个变量名,则它会将每个变量设置为对应列表中的当前项。迭代变量的数量必须与列表变量的数量相匹配。
如果没有提供
,则循环体永远不会被执行(即被处理为空)。
4.5.2.2 while
在条件为真时执行一组命令
1
2
3
while(<condition>)
<commands>
endwhile()
while 与匹配的 endwhile() 之间的所有命令都会被记录而不会立即执行。一旦 endwhile() 被求值,只要 <condition> 为真,记录的命令列表就会被执行。
<condition> 的语法与求值逻辑与 if() 命令中详细描述的内容一致。
4.5.2.3 跳出控制流
命令 break() 和 continue() 提供了跳出正常控制流的手段。break() 命令从外层的 foreach 或 while 循环中跳出。continue() 命令中止 foreach() 或 while() 循环当前迭代的剩余部分,并从下一次迭代的顶部开始。
5. CMake 构建系统
基于 CMake 的构建系统组织为一组高层级的逻辑目标。每个目标对应一个可执行文件或库,或者是一个包含自定义命令的自定义目标。目标之间的依赖关系在构建系统中进行表达,以确定构建顺序以及在发生变更时进行重新生成的规则。
5.1 二进制目标
使用 add_executable() 和 add_library() 命令定义可执行文件和库。生成的二进制文件具有目标平台适用的 PREFIX、SUFFIX 和扩展名。二进制目标之间的依赖关系使用 target_link_libraries() 命令表达。
1
2
3
add_library(archive archive.cpp zip.cpp lzma.cpp)
add_executable(zipapp zipapp.cpp)
target_link_libraries(zipapp archive)
archive 被定义为 STATIC 库——一个包含由 archive.cpp、zip.cpp 和 lzma.cpp 编译生成的对象文件的归档。 zipapp 被定义为通过编译并链接 zipapp.cpp 形成的可执行文件。链接 zipapp 可执行文件时,会链接入 archive 静态库。
5.1.1 可执行文件
可执行文件是通过链接对象文件在一起创建的二进制文件,其中一个包含程序入口点,例如 main。通过 add_executable() 命令定义一个可执行目标。
1
add_executable(mytool mytool.cpp)
CMake 生成构建规则,将源文件编译为对象文件,并将它们链接成可执行文件。
可执行文件的链接依赖项可以使用 target_link_libraries() 命令指定。链接器从编译自可执行文件自身源文件的对象文件开始,然后通过搜索链接的库来解析剩余的符号依赖项。
5.1.2 静态库
静态库是对象文件的归档。它们由归档工具而非链接器产生。可执行文件、共享库 和 模块库 可以链接到静态库作为依赖项。链接器根据需要从静态库中选择对象文件子集以解析符号,并将它们链接到消耗它们的二进制文件中。每个链接到静态库的二进制文件都会获得一份符号副本,且静态库本身在运行时不需要。
add_library() 命令在调用时使用 STATIC 库类型定义静态库目标
1
add_library(archive STATIC archive.cpp zip.cpp lzma.cpp)
或者当 BUILD_SHARED_LIBS 变量为 false 时,不指定类型。
1
add_library(archive archive.cpp zip.cpp lzma.cpp)
CMake 生成构建规则,将源文件编译为对象文件,并将其归档到静态库中。
静态库的链接依赖项可以使用 target_link_libraries() 命令指定。由于静态库是归档而非链接的二进制文件,来自其链接依赖项的对象文件不会包含在库本身中(指定为直接链接依赖项的 对象库 除外)。相反,CMake 会记录静态库的链接依赖项,以便在链接消耗它们的二进制文件时进行传递性使用。
5.1.3 共享库
共享库是通过链接对象文件在一起创建的二进制文件。可执行文件、其他共享库和 模块库 可以链接到共享库作为依赖项。链接器在消耗它们的二进制文件中记录对共享库的引用。在运行时,动态加载器在磁盘上搜索引用的共享库并加载它们的符号。
add_library() 命令在调用时使用 SHARED 库类型定义共享库目标
1
add_library(archive SHARED archive.cpp zip.cpp lzma.cpp)
或者当 BUILD_SHARED_LIBS 变量为 true 时,不指定类型。
1
add_library(archive archive.cpp zip.cpp lzma.cpp)
CMake 生成构建规则,将源文件编译为对象文件,并将它们链接成共享库。
共享库的链接依赖项可以使用 target_link_libraries() 命令指定。链接器从编译自共享库自身源文件的对象文件开始,然后通过搜索链接的库来解析剩余的符号依赖项。
5.1.4 模块库
模块库是通过链接对象文件在一起创建的二进制文件。与 共享库 不同,模块库不能被其他二进制文件作为依赖项链接 —— 不要将它们列在 target_link_libraries() 命令的右侧。相反,模块库是应用程序可以在运行时按需动态加载的插件,例如通过 dlopen 加载。
add_library() 命令在调用时使用 MODULE 库类型定义模块库目标。
1
add_library(archivePlugin MODULE 7z.cpp)
CMake 生成构建规则,将源文件编译为对象文件,并将它们链接成模块库。
模块库的链接依赖项可以使用 target_link_libraries() 命令指定。链接器从编译自模块库自身源文件的对象文件开始,然后通过搜索链接的库来解析剩余的符号依赖项。
5.1.5 对象库
对象库是编译源文件产生的对象文件的集合,不进行任何归档或链接。这些对象文件可以在链接 可执行文件、共享库 和 模块库 时使用,或者在归档 静态库 时使用。
add_library() 命令在调用时使用 OBJECT 库类型定义对象库目标。
1
add_library(archiveObjs OBJECT archive.cpp zip.cpp lzma.cpp)
CMake 生成构建规则,将源文件编译为对象文件。
其他目标可以使用 生成器表达式 语法 $<TARGET_OBJECTS:name> 将这些对象文件指定为源输入。
1
2
3
add_library(archiveExtras STATIC $<TARGET_OBJECTS:archiveObjs> extras.cpp)
add_executable(test_exe $<TARGET_OBJECTS:archiveObjs> test.cpp)
消耗这些对象的目标在链接(或归档)时,既使用它们自身的源文件,也使用命名对象库中的对象文件。
或者,也可以将对象库指定为其他目标的链接依赖项。
1
2
3
4
5
add_library(archiveExtras STATIC extras.cpp)
target_link_libraries(archiveExtras PUBLIC archiveObjs)
add_executable(test_exe test.cpp)
target_link_libraries(test_exe archiveObjs)
消耗这些对象的目标在链接(或归档)时,既使用它们自身的源文件,也使用由 target_link_libraries() 指定为直接链接依赖项的对象库中的对象文件。
5.2 构建规范与使用要求
目标的构建遵循其自身的 构建规范,并结合从其链接依赖项传递而来的 使用要求。两者都可以使用特定于目标的 命令 来指定。
5.2.1 目标命令
特定于目标的命令会填充 二进制目标 的 构建规范,以及 二进制目标、接口库 和 导入目标 的 使用要求。
| 类型 | 填充构建规范属性 | 填充使用要求属性 |
|---|---|---|
target_compile_definitions() | COMPILE_DEFINITIONS | INTERFACE_COMPILE_DEFINITIONS |
target_compile_options() | COMPILE_OPTIONS | INTERFACE_COMPILE_OPTIONS |
target_compile_features() | COMPILE_FEATURES | INTERFACE_COMPILE_FEATURES |
target_include_directories() | INCLUDE_DIRECTORIES | INTERFACE_INCLUDE_DIRECTORIES |
target_sources() | SOURCES | INTERFACE_SOURCES |
target_precompile_headers() | PRECOMPILE_HEADERS | INTERFACE_PRECOMPILE_HEADERS |
target_link_libraries() | LINK_LIBRARIES | INTERFACE_LINK_LIBRARIES |
target_link_directories() | LINK_DIRECTORIES | INTERFACE_LINK_DIRECTORIES |
target_link_options() | LINK_OPTIONS | INTERFACE_LINK_OPTIONS |
调用必须指定作用域关键字,每个关键字影响其后参数的可见性。作用域包括:
| 类型 | 作用 |
|---|---|
| PUBLIC | 同时填充 构建 属性和 使用 目标的属性。 |
| PRIVATE | 仅填充 构建 目标的属性。 |
| INTERFACE | 仅填充 使用 目标的属性。 |
5.2.2 目标构建规范
二进制目标 的构建规范由目标属性表示。对于以下每一项 编译 和 链接 属性,目标的编译和链接既受其自身值的影响,也受从链接依赖项的传递闭包中收集的、以 INTERFACE_ 为前缀的对应 使用要求 属性的影响。
5.2.2.1 目标编译属性
这些表示编译目标的 构建规范。
| 类型 | 作用 |
|---|---|
COMPILE_DEFINITIONS | 用于编译目标中源文件的编译定义列表。 它们以未指定的顺序通过 -D 标志或等效标志传递给编译器。 |
COMPILE_OPTIONS | 用于编译目标中源文件的编译选项列表。 它们按出现顺序作为标志传递给编译器。 |
COMPILE_FEATURES | 编译目标中源文件所需的 编译特性 列表。 通常,这些确保目标的源文件使用足够的语言标准级别进行编译。 |
INCLUDE_DIRECTORIES | 用于编译目标中源文件的包含目录列表。 它们按出现顺序通过 -I 或 -isystem 标志或等效标志传递给编译器。 |
SOURCES | 与目标关联的源文件列表。 包括在通过 add_executable()、add_library() 或 add_custom_target() 命令创建目标时指定的源文件。 |
PRECOMPILE_HEADERS | 编译目标中源文件时要预编译并包含的头文件列表。 |
5.2.2.2 目标链接属性
这些表示链接目标的 构建规范。如果目标是可执行文件、共享库或模块库,则
| 类型 | 作用 |
|---|---|
LINK_LIBRARIES | 此项为链接该目标所需的链接库列表。 静态库 和 共享库 的条目通过其链接产物的路径,或使用 -l 标志或等效标志传递给链接器。 |
LINK_DIRECTORIES | 此项为链接该目标所需的链接目录列表。 目录通过 -L 标志或等效标志传递给链接器。 |
LINK_OPTIONS | 此项为链接该目标所需的链接选项列表。 选项按出现顺序作为标志传递给链接器。 |
LINK_DEPENDS | 此项为链接该目标所依赖的文件列表。 |
5.2.3 目标使用要求
目标的 使用要求 是传播给消费者的设置,消费者通过 target_link_libraries() 链接到该目标,以便正确与其编译和链接。它们由传递性 编译 和 链接 属性表示。
请注意,使用要求的设计目的并非仅为了方便让下游使用特定的 COMPILE_OPTIONS、COMPILE_DEFINITIONS 等。属性的内容必须是 要求,而不仅仅是建议。
目标的使用要求可以传递性地传播给依赖者。 target_link_libraries() 命令具有 PRIVATE、INTERFACE 和 PUBLIC 关键字来控制传播。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
add_library(archive archive.cpp)
target_compile_definitions(archive INTERFACE USING_ARCHIVE_LIB)
add_library(serialization serialization.cpp)
target_compile_definitions(serialization INTERFACE USING_SERIALIZATION_LIB)
add_library(archiveExtras extras.cpp)
target_link_libraries(archiveExtras PUBLIC archive)
target_link_libraries(archiveExtras PRIVATE serialization)
# archiveExtras is compiled with -DUSING_ARCHIVE_LIB
# and -DUSING_SERIALIZATION_LIB
add_executable(consumer consumer.cpp)
# consumer is compiled with -DUSING_ARCHIVE_LIB
target_link_libraries(consumer archiveExtras)
因为 archive 是 archiveExtras 的 PUBLIC 依赖项,所以它的使用要求也传播到了 consumer。
因为 serialization 是 archiveExtras 的 PRIVATE 依赖项,所以它的使用要求不会传播到 consumer。
通常,如果一个依赖项仅由库的实现使用,而不用于头文件,则应在 target_link_libraries() 的使用中使用 PRIVATE 关键字指定。如果一个依赖项此外还用于库的头文件中(例如用于类继承),则应将其指定为 PUBLIC 依赖项。不被库的实现使用,仅由其头文件使用的依赖项应指定为 INTERFACE 依赖项。 target_link_libraries() 命令可以使用每个关键字的多次调用进行调用。
1
2
3
4
target_link_libraries(archiveExtras
PUBLIC archive
PRIVATE serialization
)
使用要求通过读取来自依赖项的目标属性的 INTERFACE_ 变体,并将这些值追加到操作数的非 INTERFACE_ 变体中来传播。例如,读取依赖项的 INTERFACE_INCLUDE_DIRECTORIES 并将其追加到操作数的 INCLUDE_DIRECTORIES 中。在顺序相关且被维护的情况下,如果 target_link_libraries() 调用产生的顺序不允许正确编译,则使用适当的命令直接设置属性可能会更新该顺序。
例如,如果目标的链接库必须按 lib1 lib2 lib3 的顺序指定,但包含目录必须按 lib3 lib1 lib2 的顺序指定。
1
2
3
target_link_libraries(myExe lib1 lib2 lib3)
target_include_directories(myExe
PRIVATE $<TARGET_PROPERTY:lib3,INTERFACE_INCLUDE_DIRECTORIES>)
5.2.3.1 传递编译属性
这些表示用于编译消费者的 使用要求。
| 类型 | 作用 |
|---|---|
INTERFACE_COMPILE_DEFINITIONS | 用于编译目标消费者中源文件的编译定义列表。 通常这些由目标的头文件使用。 |
INTERFACE_COMPILE_OPTIONS | 用于编译目标消费者中源文件的编译选项列表。 |
INTERFACE_COMPILE_FEATURES | 用于编译目标消费者中源文件所需的 编译特性 列表。 |
INTERFACE_INCLUDE_DIRECTORIES | 用于编译目标消费者中源文件的包含目录列表。 通常这些是目标头文件的位置。 |
INTERFACE_SYSTEM_INCLUDE_DIRECTORIES | 当指定为包含目录时, 在编译目标消费者中的源文件时应被视为“系统”包含目录的目录列表。 |
INTERFACE_SOURCES | 要与目标消费者关联的源文件列表。 |
INTERFACE_PRECOMPILE_HEADERS | 编译目标消费者中的源文件时要预编译并包含的头文件列表。 |
5.2.3.2 传递链接属性
这些表示用于链接消费者的 使用要求。
| 类型 | 作用 |
|---|---|
INTERFACE_LINK_LIBRARIES | 用于链接目标消费者(如果是可执行文件、共享库或模块库)的链接库列表。 |
INTERFACE_LINK_DIRECTORIES | 用于链接目标消费者(如果是可执行文件、共享库或模块库)的链接目录列表。 |
INTERFACE_LINK_OPTIONS | 用于链接目标消费者(如果是可执行文件、共享库或模块库)的链接选项列表。 |
INTERFACE_LINK_DEPENDS | 链接目标消费者(如果是可执行文件、共享库或模块库)所依赖的文件列表。 |
6. CMake 模块
CMake 利用代码重用技术创建可重用的模块,称之 cmake-modules。模块的位置可以使用模块文件的完整路径指定,也可以让 CMake 自己查找模块。CMake 将在 CMAKE_MODULE_PATH 指定的目录中查找模块;如果无法在该目录中找到模块,它将在 Modules 安装子目录中查找。模块可以细分为几个主要类别:
6.1 查找模块
以下列举了 CMake 提供的常用内置查找模块,更多的请查阅 cmake-modules:
以 FindZLIB 模块为例,查找 zlib 并将其链接到项目目标,使用方式如下:
1
2
find_package(ZLIB)
target_link_libraries(project_target PRIVATE ZLIB::ZLIB)
6.2 实用程序模块
以下列举了 CMake 提供的常用内置实用程序模块,更多的请查阅 cmake-modules:
| 模块名称 | 说明 |
|---|---|
CheckCompilerFlag | 提供检查编译器是否支持给定标志的命令 |
CheckSourceCompiles | 提供检查给定语言的源代码能否被构建的命令 |
CheckSourceRuns | 提供检查源代码是否可以构建并运行的命令 |
CheckCCompilerFlag | 提供命令用于检查 C 编译器是否支持给定的标志 |
CheckCSourceCompiles | 提供检查 C 源代码是否能够构建的命令 |
CheckCSourceRuns | 提供检查 C 源代码是否可以构建并运行的命令 |
CheckCXXCompilerFlag | 提供检查 C++ 编译器是否支持给定标志的命令 |
CheckCXXSourceCompiles | 提供检查 C++ 源代码是否可以构建的命令 |
CheckCXXSourceRuns | 提供检查 C++ 源代码是否可以被构建并运行 |
CheckCXXSymbolExists | 提供检查 C++ 符号是否存在的命令 |
CheckFunctionExists | 提供检查 C 函数是否存在的命令 |
CheckIncludeFile | 提供检查 C 头文件的命令 |
CheckIncludeFileCXX | 提供检查 C++ 头文件的命令 |
CheckIncludeFiles | 提供检查一个或多个 C/C++ 头文件的命令 |
CheckIPOSupported | 提供检查编译器是否支持过程间优化(IPO/LTO)的命令 |
CheckLibraryExists | 提供检查 C 语言库是否存在的命令 |
CheckLinkerFlag | 提供检查编译器是否支持给定链接器标志的命令 |
CheckPIESupported | 提供命令用于检查链接器是否支持可执行文件的位置无关代码 (PIE) |
CheckStructHasMember | 提供检查结构体或类是否包含指定成员变量的命令 |
CheckSymbolExists | 提供检查 C 语言符号是否存在的命令 |
CheckTypeSize | 提供检查 C/C++ 类型或表达式大小的命令 |
CheckVariableExists | 提供检查 C 语言变量是否存在的命令 |
CMakePrintHelpers | 提供打印属性和变量值的命令 |
CMakePrintSystemInformation | 包含此模块将会自动打印系统信息 |
CMakePackageConfigHelpers | 提供命令用于创建可被其他项目包含以查找和使用包的配置文件 |
CMakeDependentOption | 提供用于定义布尔选项的命令 |
CPack | 该模块为二进制安装程序和源码包配置生成器 |
CTest | 在项目的顶层 CMakeLists.txt 文件中包含此模块,可启用 CTest 测试 |
ExternalData | 提供用于管理存储在源树之外的数据文件的命令 |
ExternalProject | 提供在 CMake 构建阶段下载并构建外部项目的命令 |
FeatureSummary | 提供用于生成已启用/已禁用特性摘要的命令 |
FetchContent | 提供在配置时或作为调用脚本的一部分来填充内容的命令 |
FindPackageMessage | 提供用于打印查找结果消息的命令 |
GenerateExportHeader | 提供用于生成头文件的命令 |
GoogleTest | 提供用于辅助使用 Google Test 框架的命令 |
ProcessorCount | 提供用于确定处理器/核心数量的命令 |
以 CheckIncludeFile 检查 C 头文件模块为例,查找 C 头文件
1
2
3
include(CheckIncludeFile)
check_include_file(unistd.h HAVE_UNISTD_H)
6.3 自定义模块
自定义模块是一个以 .cmake 为扩展名的文件,其中定义了可被 include 加载的 CMake 函数和宏。其创建步骤是:
- 创建自定义模块存放文件夹
- 在自定义模块文件夹下创建模块文件,编写模块内容,实现模块功能
- 扩展 CMake 模块搜索路径,在 CMakeLists.txt 中通过 CMAKE_MODULE_PATH 注册该目录
- 加载模块并使用模块函数,使用 include() 引入模块并调用其中的函数
创建自定义模块并命名为 CustomModule.cmake,编写模块内容:
1
2
3
function(custom_module_function)
message(STATUS "This is a custom function!")
endfunction()
假设模块文件创建在 ${CMAKE_SOURCE_DIR}/cmake 文件夹中,在 CMakeLists.txt 中加载该自定义模块:
1
2
3
4
5
6
7
8
9
# 将 cmake/ 目录添加到模块搜索路径
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
# 加载自定义模块
include(CustomModule)
# 调用模块中的自定义函数
custom_module_function()
CMAKE_MODULE_PATH 是 CMake 查找模块文件时的搜索路径列表。CMake 会先在内置模块路径中查找,找不到时再到 CMAKE_MODULE_PATH 指定的路径中查找。
7. CMake 包
8. CMake 属性
9. CMake 自定义命令
通常,软件项目的构建过程不仅包括编译库和可执行文件。在许多情况下,在构建过程期间或之后可能需要额外的任务。常见的示例包括:移动生成的可执行文件;对可执行文件进行后处理等。CMake 支持使用 add_custom_command 和 add_custom_target 命令来执行这些其他任务。本章将描述如何使用自定义命令和目标来执行 CMake 本身不支持的复杂任务。
9.1 添加自定义命令
要添加一个自定义命令,请使用 add_custom_command 命令。现在自定义一个在可执行文件构建后对其进行复制的命令:
1
2
3
4
5
6
7
8
9
10
# first define the executable target as usual
add_executable(Foo bar.c)
# then add the custom command to copy it
add_custom_command(
TARGET Foo
POST_BUILD
COMMAND ${CMAKE_COMMAND}
ARGS -E copy $<TARGET_FILE:Foo> /testing_department/files
)
这将在构建后把生成的 Foo 可执行文件从其构建位置复制到 /testing_department/files 目录。
9.2 添加自定义目标
通常情况下,CMake 目标是指可执行文件和库。在此之外,CMake 还支持一个更通用的目标概念,称为自定义目标,可以在你想要目标概念但不想要最终产品是库或可执行文件时使用,即定义一个“动作”目标。要添加一个自定义目标,请使用 add_custom_target 命令。以下示例展示了如何使用 add_custom_target 创建一个目标来运行 Python 脚本:
1
2
3
4
5
# 创建一个自定义目标,运行 Python 脚本
add_custom_target(run_script
COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/scripts/setup.py
COMMENT "Running setup script..."
)
运行 make run_script 即可触发该目标,执行脚本。如果希望自定义目标成为默认构建的一部分,可以添加 ALL 参数:
1
2
3
4
add_custom_target(clean_all ALL
COMMAND ${CMAKE_COMMAND} -E remove_directory ${CMAKE_BINARY_DIR}/output
COMMENT "Cleaning all generated files..."
)
直接运行 make 时,该目标会自动执行。如果自定义目标依赖其他内容,可使用 DEPENDS 参数指定:
1
2
3
4
5
6
add_custom_command(
OUTPUT printout
COMMAND ${CMAKE_COMMAND} -E echo "Compile finish"
)
add_custom_target(finish ALL DEPENDS printout)
