文章

CMake

CMake 是一个用于管理源代码构建的工具。最初,CMake 被设计为各种 Makefile 方言的生成器,如今 CMake 生成现代构建系统,如 Ninja,以及用于 Visual Studio 和 Xcode 等 IDE 的项目文件。

CMake

1. 简介

CMake 是一个开源的、跨平台的配置程序,用于构建、测试和打包软件,有时被称为“元”构建系统,简单来说,它就是一个用于管理源代码构建的工具。它使用简单的独立配置文件来控制软件的编译过程,根据项目、环境和用户提供的配置信息生成一个构建系统。注意 CMake 仅被设计用于与本地构建环境协同工作,而不负责运行生成软件构建的命令。

Cmake Icon

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 工作流程如下:

  1. 在一个或多个 CMakeLists 文件中定义项目构建任务
  2. 配置并生成项目
  3. 使用喜欢的本地开发工具构建项目

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 选项来为新的构建树指定生成器。

Single Source Build

向 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_)将添加到编译行。要构建调试和发布树,用户应使用 CMake 的源外构建功能创建多个构建目录,并将 CMAKE_BUILD_TYPE 设置为每个构建所需的选项。例如:

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 内置了五种类别的变量,分别是提供信息、改变行为、描述系统、控制构建和用于语言的变量。

提供信息的内置变量作用
PROJECT_NAME当前目录作用域或其上层目录中,
最近一次调用 project() 命令时所给出的名称
PROJECT_VERSION当前目录作用域或其上层目录中,
最近一次调用 project() 命令时所提供的版本号
PROJECT_DESCRIPTION当前目录作用域或其上层目录中,
最近一次调用 project() 命令时所提供的描述
PROJECT_SOURCE_DIR当前目录作用域或其上层目录中,
最近一次调用 project() 命令时的源目录
PROJECT_BINARY_DIR当前目录作用域或其上层目录中,
最近一次调用 project() 命令时的二进制目录
CMAKE_PROJECT_NAME顶层 CMakeLists.txt 文件中通过 project() 命令指定的项目名称
CMAKE_PROJECT_VERSION顶层 CMakeLists.txt 文件中通过 project() 命令指定的项目版本
CMAKE_PROJECT_DESCRIPTION顶层 CMakeLists.txt 文件中通过 project() 命令指定的项目描述
CMAKE_SOURCE_DIR源码树顶层目录的完整路径
CMAKE_BINARY_DIR构建树顶层目录的完整路径
CMAKE_CURRENT_SOURCE_DIR当前正在处理的源目录的完整路径
CMAKE_CURRENT_BINARY_DIR当前正在处理的二进制目录的完整路径
CMAKE_CURRENT_LIST_DIR当 CMake 处理项目中的列表文件时,
该变量为正在处理的列表文件所在的完整目录
CMAKE_CURRENT_LIST_FILE当 CMake 处理项目中的列表文件时,
该变量为当前正在处理的列表文件,包含完整路径
CMAKE_GENERATOR当前使用的构建生成器
CMAKE_CROSSCOMPILING指示是否处于交叉编译模式,布尔值
CMAKE_VERSIONCMake 版本字符串
CMAKE_MAJOR_VERSIONCMAKE_VERSION 变量的版本号第一个组成部分
CMAKE_MINOR_VERSIONCMAKE_VERSION 变量的版本号第二个组成部分
CMAKE_COMMANDcmake 可执行程序完整路径
CMAKE_EXECUTABLE_SUFFIX添加修改可执行文件后缀
CMAKE_STATIC_LIBRARY_PREFIX静态库前缀
CMAKE_STATIC_LIBRARY_SUFFIX静态库后缀
CMAKE_SHARED_LIBRARY_PREFIX动态库前缀
CMAKE_SHARED_LIBRARY_SUFFIX动态库后缀
CMAKE_MINIMUM_REQUIRED_VERSIONCMake 被要求的最低版本
CMAKE_PARENT_LIST_FILE包含当前 CMake 文件的那个文件的完整路径
CMAKE_TOOLCHAIN_FILE工具链文件路径
改变行为的内置变量作用
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_POSTFIXDebug 版本库文件名后缀
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_DEBUGC 语言在 Debug 配置下构建时使用的编译选项
CMAKE_C_FLAGS_RELEASEC 语言在 Release 配置下构建时使用的编译选项
CMAKE_C_FLAGS_RELWITHDEBINFOC 语言在 RelWithDebInfo 配置下构建时使用的编译选项
CMAKE_C_FLAGS_MINSIZERELC 语言在 MinSizeRel 配置下构建时使用的编译选项
CMAKE_CXX_COMPILERCXX 编译器的完整路径
CMAKE_CXX_COMPILER_ID指定 CXX 编译器标识字符串
CMAKE_CXX_STANDARD用于设置 CXX_STANDARD,指定构建目标时所请求的 CXX 标准特性
CMAKE_CXX_STANDARD_REQUIRED布尔值,描述 CXX_STANDARD 的值是否为强制要求
CMAKE_CXX_FLAGS指定 CXX 语言编译选项
CMAKE_CXX_FLAGS_DEBUGCXX 语言在 Debug 配置下构建时使用的编译选项
CMAKE_CXX_FLAGS_RELEASECXX 语言在 Release 配置下构建时使用的编译选项
CMAKE_CXX_FLAGS_RELWITHDEBINFOCXX 语言在 RelWithDebInfo 配置下构建时使用的编译选项
CMAKE_CXX_FLAGS_MINSIZERELCXX 语言在 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查找元素,返回索引;找不到返回-1list(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_DEFINITIONSINTERFACE_COMPILE_DEFINITIONS
target_compile_options()COMPILE_OPTIONSINTERFACE_COMPILE_OPTIONS
target_compile_features()COMPILE_FEATURESINTERFACE_COMPILE_FEATURES
target_include_directories()INCLUDE_DIRECTORIESINTERFACE_INCLUDE_DIRECTORIES
target_sources()SOURCESINTERFACE_SOURCES
target_precompile_headers()PRECOMPILE_HEADERSINTERFACE_PRECOMPILE_HEADERS
target_link_libraries()LINK_LIBRARIESINTERFACE_LINK_LIBRARIES
target_link_directories()LINK_DIRECTORIESINTERFACE_LINK_DIRECTORIES
target_link_options()LINK_OPTIONSINTERFACE_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 头文件 是否存在,并将检查结果存储在 `HAVE_UNISTD_H` 缓存变量中。使用方式如下:

1
2
3
include(CheckIncludeFile)

check_include_file(unistd.h HAVE_UNISTD_H)

6.3 自定义模块

自定义模块是一个以 .cmake 为扩展名的文件,其中定义了可被 include 加载的 CMake 函数和宏。其创建步骤是:

  1. 创建自定义模块存放文件夹
  2. 在自定义模块文件夹下创建模块文件,编写模块内容,实现模块功能
  3. 扩展 CMake 模块搜索路径,在 CMakeLists.txt 中通过 CMAKE_MODULE_PATH 注册该目录
  4. 加载模块并使用模块函数,使用 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)

10. CMake 工具链

11. References

GNU Automake

Autotools Tutorial — Alexandre Duret-Lutz

CMake 参考文档英文版

CMake 参考文档中文版

精通 CMake

Modern CMake

本文由作者按照 CC BY 4.0 进行授权