现代C++ CMake 指南 – 1 CMake 入门第一步

本书涵盖的内容

第 1 章 CMake 入门 ,涵盖 CMake 的安装、命令行界面的使用,并介绍 CMake 项目所需的基本构建模块。(本篇)

第 2 章 ,CMake 语言 ,讲解 CMake 语言的核心概念,包括命令调用、参数、变量、控制结构和注释。

第 3 章 , 在主流 IDE 中使用 CMake,强调集成开发环境 (IDE)的重要性,指导您选择 IDE,并提供 Clion、Visual Studio Code 和 Visual Studio IDE 的设置说明。

第 4 章 , 创建首个 CMake 项目 ,将教您如何在顶层文件中配置基础 CMake 项目、组织文件树结构,并准备开发所需的工具链。

第 5 章 , 目标构建 ,探讨逻辑构建目标的概念,理解其属性和不同类型,并学习如何为 CMake 项目定义自定义命令。

第 6 章 , 生成器表达式 ,阐述生成器表达式的用途和语法,包括如何将其用于条件扩展、查询和转换。

第 7 章 , 使用 CMake 编译 C++源码 ,深入编译过程,配置预处理器和优化器,并探索减少构建时间和改进调试的技术。

第 8 章 , 链接可执行文件与库 ,理解链接机制、不同类型的库、单一定义规则、链接顺序,以及如何为项目测试做准备。

第 9 章 ,CMake 中的依赖管理 ,将教你管理第三方库,为缺乏 CMake 支持的库添加支持,以及从互联网获取外部依赖。

第 10 章 , 使用 C++20 模块 ,介绍 C++20 模块,展示如何在 CMake 中启用模块支持并相应配置工具链。

第 11 章 , 测试框架 ,将帮助你理解自动化测试的重要性,利用 CMake 内置的测试支持,并使用流行框架开始单元测试。

第 12 章 , 程序分析工具 ,将向你展示如何自动格式化源代码,并在构建时和运行时检测软件错误。

第 13 章 , 生成文档 ,介绍如何使用 Doxygen 从源代码自动创建文档,并通过添加样式来提升文档外观。

第 14 章 , 安装与打包 ,指导您如何为项目发布做准备(包括是否需要安装),创建可复用的软件包,并指定需要单独打包的组件。

第 15 章 , 创建专业级项目 ,将全书所学知识综合运用于开发一个完整、专业级的项目。

第 16 章 , 编写 CMake 预设 ,通过 CMake 预设文件将高级项目配置封装为工作流程,使项目设置和管理更加高效。


软件开发有种神奇的魔力。我们不仅是在创造一个能被赋予生命的运行机制,更常常是在构思解决方案功能背后的核心理念。

为实现创意落地,我们遵循以下循环流程:设计、编码与测试。我们构思变更,用编译器能理解的语言表述,并验证其是否符合预期。要从源代码打造出正确、高质量的软件,必须一丝不苟地执行那些重复且易错的任务:调用正确命令、检查语法、链接二进制文件、运行测试、报告问题等。

每次都要牢记每个步骤需要耗费巨大精力。相反,我们希望专注于实际编码工作,将其他事务交由自动化工具处理。理想情况下,这个过程只需在代码修改后点击一个按钮即可启动——它应该智能高效、可扩展,且在不同操作系统和环境中表现一致。该流程将获得多种集成开发环境 (IDE)的支持。更进一步,我们还能将其优化为持续集成 (CI)流水线,每当变更提交至共享仓库时自动构建和测试软件。

CMake 正是解决此类需求的答案;然而,要正确配置和使用它需要一些功夫。CMake 并非复杂性的根源,真正的复杂性来自我们正在处理的这个主题。别担心,我们将非常有条理地完成整个学习过程。不知不觉中,你就会成为软件构建的行家。

我知道你迫不及待想开始编写自己的 CMake 项目,这也正是本书大部分内容要做的。但由于你创建的项目主要面向用户(包括你自己),首先理解他们的视角非常重要。

那么,让我们就从这里开始:成为 CMake 高级用户 。我们将学习一些基础知识:这个工具是什么、它的工作原理以及如何安装。然后,我们将深入探讨命令行和操作模式。最后,我们会总结项目中不同文件的用途,并解释如何在不创建项目的情况下使用 CMake。

本章将涵盖以下主要内容:

理解基础知识
在不同平台上安装 CMake
掌握命令行操作
浏览项目文件
发现脚本与模块

构建本书提供的示例时,请始终执行所有推荐命令:

cmake -B <build tree> -S <source tree>
cmake --build <build tree>

请确保将占位符 <build tree> 和 <source tree> 替换为正确的路径。正如本章所述, 构建树是输出目录的路径,而源码树则是源代码所在的位置。

要构建 C++程序,您还需要一个适合您平台的编译器。如果您熟悉 Docker,可以使用在不同平台上安装 CMake 章节中介绍的全功能工具镜像。如果更倾向于手动设置 CMake,我们将在同一章节中解释安装过程。

理解基础知识

C++源代码的编译过程看似相当简单。让我们从经典的 Hello World 示例开始。

以下代码可在 ch01/01-hello/hello.cpp 中找到,C++语言中的 Hello World

#include <iostream>
int main() {
  std::cout << "Hello World!" << std::endl;
  return 0;
}

要生成可执行文件,我们当然需要一个 C++编译器。CMake 本身并不附带编译器,因此您需要自行选择并安装一个。常见的选择包括:

Microsoft Visual C++ 编译器

GNU 编译器集合

Clang/LLVM

大多数读者都熟悉编译器 ,因为学习 C++时它必不可少,因此我们不再赘述如何选择和安装。本书示例将使用 GNU GCC,因为它是一个成熟的开源软件编译器,可免费用于多种平台。

假设我们的编译器已经安装完毕,运行它的方式对于大多数厂商和系统都类似。我们应该以文件名作为参数来调用它:

$ g++ hello.cpp -o hello

我们的代码是正确的,因此编译器将静默生成一个机器可识别的可执行二进制文件。我们可以通过调用其名称来运行它:

$ ./hello
Hello World!

用一条命令构建程序足够简单;然而,随着项目规模增长,你会很快意识到将所有内容保存在单个文件中根本不可行。整洁代码实践建议源代码文件应保持小巧并组织在良好的结构中。手动编译每个文件可能是个繁琐且脆弱的过程。一定有更好的方法。

什么是 CMake?

假设我们通过编写一个脚本来实现自动化构建,该脚本会遍历项目目录并编译所有内容。为了避免不必要的重复编译,脚本将检测源代码自上次运行以来是否被修改过。现在,我们需要一种便捷的方式来管理传递给每个文件编译器的参数——最好是能基于可配置的规则来实现。此外,脚本还应知道如何将所有编译好的文件链接成单个二进制文件,或者更理想的是,构建完整的解决方案,这些方案可作为模块被复用并集成到更大的项目中。

软件构建是一个高度灵活的过程,可能涉及多个不同方面:

编译可执行文件和库文件
管理依赖关系
测试
安装
打包
生成文档
测试更多内容

要开发出一个真正模块化且功能强大的 C++构建工具,适用于各种用途,需要耗费很长时间。事实也确实如此。Kitware 公司的 Bill Hoffman 在 20 多年前实现了 CMake 的最初版本。正如你可能已经猜到的,它非常成功。如今,CMake 拥有众多功能,并得到社区的广泛支持。它正在积极开发中,已成为 C 和 C++程序员的行业标准。

自动化构建代码的问题比 CMake 古老得多,因此自然存在许多其他选择:GNU Make、Autotools、SCons、Ninja、Premake 等等。但为什么 CMake 能占据优势?

关于 CMake,我发现有几个方面(主观上认为)很重要:

它始终专注于支持现代编译器和工具链。
CMake 是真正跨平台的——支持为 Windows、Linux、macOS 和 Cygwin 构建项目。
它能生成主流 IDE 的项目文件:Microsoft Visual Studio、Xcode 和 Eclipse CDT。此外,它还为其他工具(如 CLion)提供了项目模型。
CMake 在恰到好处的抽象层级上运作——允许你将文件组织成可重用的目标和项目。
有大量项目使用 CMake 构建,并提供了便捷的方式将其集成到您的项目中。
CMake 将测试、打包和安装视为构建过程中不可或缺的部分。
为了保持 CMake 的精简,老旧无用的功能会被弃用。

CMake 提供了一致且高效的统一体验。无论您是在 IDE 中构建软件还是直接从命令行操作,真正重要的是它还能妥善处理构建后的各个阶段。

您的 CI/CD 流水线可以轻松使用相同的 CMake 配置,即使所有前置环境各不相同,也能通过单一标准构建项目。

它是如何工作的?

您可能认为 CMake 是一个工具,一端读取源代码,另一端生成二进制文件——虽然原则上这是正确的,但这并非全部真相。

CMake 本身无法构建任何内容——它依赖系统中的其他工具来执行实际的编译、链接等任务。您可以将其视为构建过程的协调者:它知道需要完成哪些步骤、最终目标是什么,以及如何找到合适的工具和资源来完成工作。

此过程分为三个阶段:

配置
生成
构建

让我们来详细探讨一下。

配置阶段

此阶段涉及读取存储在目录(称为源码树 )中的项目详情,并为生成阶段准备输出目录或构建树 

CMake 首先会检查项目是否之前配置过,并从 CMakeCache.txt 文件中读取缓存的配置变量。首次运行时,由于不存在缓存文件,它会创建一个空的构建树,并收集当前工作环境的所有详细信息:例如系统架构、可用的编译器、已安装的链接器和归档工具等。此外,它还会验证是否能正确编译一个简单的测试程序。

接下来,解析并执行 CMakeLists.txt 项目配置文件(没错,CMake 项目是用 CMake 的编码语言配置的)。该文件是 CMake 项目的最低配置(源文件可以稍后添加),它向 CMake 说明项目结构、目标及其依赖项(库和其他 CMake 包)。

在此过程中,CMake 会将收集到的信息存储在构建树中,包括系统详情、项目配置、日志和临时文件,这些信息将用于后续步骤。具体而言,会创建一个 CMakeCache.txt 文件来存储更稳定的信息(如编译器及其他工具的路径),当再次执行整个构建流程时,这能节省时间。

生成阶段

读取项目配置后,CMake 会为当前工作环境生成一个构建系统 。构建系统本质上是为其他构建工具量身定制的配置文件(例如为 GNU Make 或 Ninja 生成的 Makefile,或为 Visual Studio 生成的 IDE 项目文件)。在此阶段,CMake 仍可通过评估生成器表达式对构建配置进行最终调整。

生成阶段在配置阶段完成后自动执行。因此,本书及其他资料在提及构建系统的”配置”或”生成”时,有时会将这两个阶段互换使用。若要仅显式运行配置阶段,可使用 cmake-gui 工具。

构建阶段

为了生成项目中指定的最终产物(如可执行文件和库),CMake 需要运行相应的构建工具 。这可以通过直接调用、通过 IDE 或使用适当的 CMake 命令来完成。反过来,这些构建工具将执行步骤,使用编译器、链接器、静态和动态分析工具、测试框架、报告工具以及您能想到的任何其他工具来生成目标产物 

该解决方案的妙处在于能够通过单一配置(即相同的项目文件)为每个平台按需生成构建系统:

还记得我们在理解基础知识一节中用到的 hello.cpp 应用程序吗?使用 CMake 构建它非常简单。我们只需要在源代码目录下准备这样一个 CMakeLists.txt 文件。

cmake_minimum_required(VERSION 3.26)
project(Hello)
add_executable(Hello hello.cpp)

创建此文件后,在同一目录下执行以下命令:

cmake -B <build tree>
cmake --build <build tree>

请注意, <build tree> 是一个占位符,应替换为用于存放生成文件的临时目录路径。

以下是在 Docker 中运行的 Ubuntu 系统输出(Docker 是一种可在其他系统内运行的虚拟机;我们将在在不同平台安装 CMake 章节详细讨论)。第一条命令会生成一个构建系统 

~/examples/ch01/01-hello# cmake -B ~/build_tree
-- The C compiler identification is GNU 11.3.0
-- The CXX compiler identification is GNU 11.3.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done (1.0s)
-- Generating done (0.1s)
-- Build files have been written to: /root/build_tree

第二条命令实际上构建了项目:

~/examples/ch01/01-hello# cmake --build ~/build_tree
Scanning dependencies of target Hello
[ 50%] Building CXX object CMakeFiles/Hello.dir/hello.cpp.o
[100%] Linking CXX executable Hello
[100%] Built target Hello

剩下的就是运行编译好的程序了:

~/examples/ch01/01-hello# ~/build_tree/Hello
Hello World!

在此,我们生成了一个构建系统,它存储在构建树目录中。随后,我们执行了构建阶段并生成了最终可运行的二进制文件。

现在你已经了解了最终结果的样子,我敢肯定你心中充满了疑问:这个过程有哪些前提条件?这些命令是什么意思?为什么我们需要两个命令?如何编写自己的项目文件?别担心——这些问题都将在后续章节中得到解答。

本书将为你提供与当前 CMake 版本(撰写本书时为 3.26 版)相关的最重要信息。为了给你最佳建议,我刻意避开了所有已弃用和不再推荐的功能,并强烈建议至少使用 CMake 3.15 版本,这被视为现代 CMake。如需更多信息,你可以在线查阅最新完整文档:https://cmake.org/cmake/help/。

在不同平台上安装 CMake

CMake 是一款用 C++编写的跨平台开源软件。这意味着你当然可以自行编译它;但最可能的情况是你无需这么做,因为官方网站 Download CMake 上已提供了预编译的二进制文件可供下载。

基于 Unix 的系统可直接通过命令行提供即装即用的软件包。

请记住,CMake 本身不附带编译器。如果您的系统尚未安装编译器,使用 CMake 前需先配置好。确保将编译器可执行文件的路径添加到 PATH 环境变量中,以便 CMake 能够找到它们。

为避免在学习本书时遇到工具链和依赖问题,我建议采用第一种安装方式——Docker 进行实践。当然,在实际开发场景中,除非本就处于虚拟化环境,否则您会希望使用原生版本。

下面我们来了解 CMake 适用的几种不同环境。

Docker

Docker(Docker: Accelerated Container Application Development)是一款跨平台工具,提供操作系统级虚拟化功能,允许应用程序以定义明确的容器形式进行封装。这些自包含的软件包内含有运行所需的所有库文件、依赖项和工具。Docker 在相互隔离的轻量级环境中执行这些容器。

这一概念使得共享整个工具链变得极其便利,这些工具链针对特定流程已配置完备、开箱即用。当您无需纠结于细微的环境差异时,操作会变得异常简单,这一点怎么强调都不为过。

Docker 平台拥有一个公共容器镜像仓库 https://registry.hub.docker.com/,其中提供了数百万个即用型镜像。

为方便起见,我已发布两个 Docker 仓库:

swidzinski/cmake2:base : 一个基于 Ubuntu 的镜像,包含使用 CMake 构建所需的精选工具和依赖项
swidzinski/cmake2:examples : 基于前述工具链构建的镜像,包含本书所有项目及示例

第一种选择适合希望获得纯净镜像以便构建自己项目的读者,第二种选择则适合在阅读章节时动手实践示例。

您可以通过 Docker 官方文档的指引进行安装(请参考 docs.docker.com/get-docker)。然后在终端执行以下命令以下载镜像并启动容器:

$ docker pull swidzinski/cmake2:examples
$ docker run -it swidzinski/cmake2:examples
root@b55e271a85b2:root@b55e271a85b2:#

请注意,示例可在符合此格式的目录中找到:

devuser/examples/examples/ch<N>/<M>-<title>

此处, <N> 和 <M> 分别是补零的章节编号和示例编号(如 01 、 08 和 12 )。

Windows 系统

在 Windows 上安装非常简单——只需从官网下载 32 位或 64 位版本即可。您还可以选择便携式 ZIP 包或 Windows Installer 的 MSI 安装包,这将把 CMake 的 bin 目录添加到 PATH 环境变量中( 图 1.2),使您能在任意目录下使用而不会出现如下错误:

cmake 未被识别为内部或外部命令、可执行程序或批处理文件。

如果选择 ZIP 压缩包,您需要手动操作。MSI 安装程序则提供了便捷的图形用户界面:

正如我之前提到的,这是开源软件,因此您可以自行构建 CMake。但在 Windows 系统上,您需要先获取 CMake 的二进制版本。CMake 贡献者通常使用这种方式来生成新版本。

Windows 平台与其他平台并无不同,同样需要一个能够完成 CMake 启动的构建过程的构建工具。这里常用的选择是 Visual Studio IDE,它自带 C++编译器。社区版可从微软官网免费获取:Download Visual Studio Tools – Install Free for Windows, Mac, Linux。

Linux

在 Linux 上安装 CMake 的过程与其他流行软件包相同:通过命令行调用包管理器。软件仓库通常会提供较新版本的 CMake,但一般不是最新版本。如果您对此没有异议,并且使用的是 Debian 或 Ubuntu 等发行版,最简单的办法就是直接安装相应的软件包:

$ sudo apt-get install cmake

对于 Red Hat 发行版,请使用以下命令:

$ yum install cmake

请注意,安装软件包时,您的包管理器会从为操作系统配置的仓库中获取最新可用版本。多数情况下,软件包仓库提供的并非最新版本,而是经过时间验证能稳定运行的稳定版。请根据需求选择,但需注意旧版本可能不具备本书描述的所有功能。

要获取最新版本,请参考 CMake 官方网站的下载部分。如果您知道当前版本号,可以使用以下命令之一。

适用于 Linux x86_64 系统的命令是:

$ VER=3.26.0 && wget https://github.com/Kitware/CMake/releases/download/v$VER/cmake-$VER-linux-x86_64.sh && chmod +x cmake-$VER-linux-x86_64.sh && ./cmake-$VER-linux-x86_64.sh

适用于 Linux AArch64 系统的命令是:

$ VER=3.26.0 && wget https://github.com/Kitware/CMake/releases/download/v$VER/cmake-$VER-Linux-aarch64.sh && chmod +x cmake-$VER-Linux-aarch64.sh && ./cmake-$VER-Linux-aarch64.sh

或者,也可以查看从源代码构建部分,了解如何在您的平台上自行编译 CMake。

macOS

该平台同样受到 CMake 开发者的强力支持。最流行的安装方式是通过 MacPorts,使用以下命令:

$ sudo port install cmake

请注意,在撰写本文时,MacPorts 中提供的最新版本是 3.24.4。要获取最新版本,请安装 cmake-devel 软件包:

$ sudo port install cmake-devel

或者,您也可以使用 Homebrew 包管理器:

$ brew install cmake

macOS 包管理器会涵盖所有必要步骤,但需注意除非从源代码构建,否则可能无法获取最新版本。

从源代码构建

如果您使用的是其他平台,或者只是想体验尚未发布为正式版本(或未被您常用的软件包仓库收录)的最新构建版本,可以从官方网站下载源代码并自行编译:

$ wget https://github.com/Kitware/CMake/releases/download/v3.26.0/cmake-3.26.0.tar.gz
$ tar xzf cmake-3.26.0.tar.gz
$ cd cmake-3.26.0
$ ./bootstrap
$ make
$ make install

从源代码构建相对较慢且步骤较多。但若想自由选择任意版本的 CMake,这是唯一途径。当操作系统仓库中的软件包版本过旧时,这种方法尤其有用:系统版本越老,获得的更新就越少。

既然已经安装了 CMake,现在让我们学习如何使用它!

掌握命令行操作

本书大部分内容将教你如何为用户准备 CMake 项目。为了满足他们的需求,我们需要深入理解用户在不同场景下如何与 CMake 交互。这将使你能够测试项目文件并确保其正确运行。

CMake 是一套工具集,包含五个可执行文件:

cmake :主可执行文件,用于配置、生成和构建项目
ctest : 用于运行和报告测试结果的测试驱动程序
cpack : 用于生成安装程序和源码包的打包程序
cmake-gui : cmake 的图形界面封装程序
ccmake : cmake 的控制台图形界面封装程序

此外,CMake 背后的公司 Kitware 还提供了一款名为 CDash 的独立工具,用于对我们的项目构建健康状况进行高级监控。

CMake 命令行工具

cmake 是 CMake 套件的主程序,提供多种操作模式(有时也称为动作):

生成项目构建系统
构建项目
安装项目
运行脚本
运行命令行工具
运行工作流预设
获取帮助

让我们看看它们是如何工作的。

生成项目构建系统

构建项目的第一步是生成构建系统。以下是执行 CMake 生成项目构建系统操作的三种命令形式:

cmake [<options>] -S <source tree> -B <build tree>
cmake [<options>] <source tree>
cmake [<options>] <build tree>

我们将在后续章节讨论可用的 <options> 。现在,让我们重点讨论如何选择正确的命令形式。CMake 的一个重要特性是支持外部构建 ,或者说支持将构建产物存储在与源代码树不同的目录中。这是首选的做法,可以保持源代码目录不受任何构建相关文件的污染,并避免在版本控制系统 VCSs)中混入意外文件或忽略指令。

正因如此,第一种命令形式最为实用。它允许我们分别通过 -S 和 -B 来指定源码树的路径及生成的构建系统路径。

cmake -S ./project -B ./build

CMake 将从 ./project 目录读取项目文件,并在 ./build 目录中生成构建系统(如有需要会预先创建该目录)。

我们可以省略其中一个参数, cmake 会”猜测”我们打算使用当前目录。注意,若同时省略两个参数,将导致源码内构建 ,使构建产物与源代码混在一起,这是我们不希望看到的。

运行 CMake 时务必显式指定参数

不要使用 cmake <directory> 命令的第二或第三种形式,因为它们可能导致混乱的源码内构建 。在第 4 章设置首个 CMake 项目部分,我们将学习如何防止用户进行此类操作。

如语法片段所示,若 <directory> 中已存在先前构建,同一命令的行为会有所不同:它将使用缓存的源码路径并从此处重新构建。由于我们经常从终端命令历史中调用相同命令,此处可能会遇到问题;在使用此形式前,务必检查当前 shell 是否工作在正确的目录下。

示例

在当前目录下使用上一级目录的源代码生成构建树:

cmake -S ..

在 ./build 目录下使用当前目录的源代码生成构建树:

cmake -B build
选择生成器

如前所述,您可以在生成阶段指定一些选项。选择和配置生成器将决定在后续构建项目环节中使用我们系统中的哪种构建工具、构建文件的外观以及构建树的结构。

那么,你是否需要关心这个问题呢?幸运的是,答案通常是”不需要”。CMake 确实支持多种平台上的原生构建系统;不过,除非你同时安装了多个生成器,否则 CMake 会自动为你选择合适的生成器。你可以通过设置 CMAKE_GENERATOR 环境变量或直接在命令行中指定生成器来覆盖默认选择,例如:

cmake -G <generator name> -S <source tree> -B <build tree>

某些生成器(如 Visual Studio)支持更深入地指定工具集(编译器)和平台(编译器或 SDK)。此外,CMake 会扫描覆盖默认值的环境变量: CMAKE_GENERATOR_TOOLSET 和 CMAKE_GENERATOR_PLATFORM 。或者,这些值也可以直接在命令行中指定:

cmake -G <generator name>
      -T <toolset spec>
      -A <platform name>
      -S <source tree> -B <build tree>

Windows 用户通常希望为其首选 IDE 生成构建系统。在 Linux 和 macOS 上,使用 Unix Makefiles 或 Ninja 生成器非常常见。

要检查系统上可用的生成器,请使用以下命令:

cmake --help

在 help 打印输出的末尾,您将获得完整的生成器列表,如下所示(此示例基于 Windows 10 系统生成,部分输出内容已截断以提升可读性):

本平台提供以下生成器:

Visual Studio 17 2022       
Visual Studio 16 2019       
Visual Studio 15 2017 [arch]
Visual Studio 14 2015 [arch]
Visual Studio 12 2013 [arch]
Visual Studio 11 2012 [arch]
Visual Studio 9 2008 [arch] 
Borland Makefiles           
NMake Makefiles             
NMake Makefiles JOM         
MSYS Makefiles              
MinGW Makefiles             
Green Hills MULTI           
Unix Makefiles              
Ninja                       
Ninja Multi-Config          
Watcom WMake                
CodeBlocks - MinGW Makefiles
CodeBlocks - NMake Makefiles
CodeBlocks - NMake Makefiles JOM
CodeBlocks - Ninja          
CodeBlocks - Unix Makefiles 
CodeLite - MinGW Makefiles  
CodeLite - NMake Makefiles  
CodeLite - Ninja            
CodeLite - Unix Makefiles   
Eclipse CDT4 - NMake Makefiles
Eclipse CDT4 - MinGW Makefiles
Eclipse CDT4 - Ninja        
Eclipse CDT4 - Unix Makefiles
Kate - MinGW Makefiles      
Kate - NMake Makefiles      
Kate - Ninja                
Kate - Unix Makefiles       
Sublime Text 2 - MinGW Makefiles
Sublime Text 2 - NMake Makefiles
Sublime Text 2 - Ninja      
Sublime Text 2 - Unix Makefiles

如您所见,CMake 支持多种不同的生成器类型和集成开发环境。

管理项目缓存

CMake 在配置阶段会查询系统获取各类信息。由于这些操作可能耗时较长,收集到的信息会被缓存在构建树目录下的 CMakeCache.txt 文件中。提供了一些命令行选项,可让你更方便地管理缓存行为。

我们可用的第一个选项是能够预填充缓存信息 

cmake -C <initial cache script> -S <source tree> -B <build tree>

我们可以提供一个指向 CMake 列表文件的路径,该文件(仅)包含一系列 set() 命令,用于指定将用于初始化空构建树的变量。我们将在下一章讨论如何编写这些列表文件。

现有缓存变量的初始化和修改可以通过另一种方式完成(例如,当仅需设置少量变量时创建文件显得过于繁琐)。您可以直接在命令行中设置它们,如下所示:

cmake -D <var>[:<type>]=<value> -S <source tree> -B <build tree>

:<type> 部分是可选的(由 GUI 使用),它接受以下类型: BOOL 、 FILEPATH 、 PATH 、 STRING 或 INTERNAL 。如果省略类型,CMake 将检查变量是否存在于 CMakeCache.txt 文件中并使用其类型;否则,它将被设置为 UNINITIALIZED 。

我们经常通过命令行设置的一个特别重要的变量是构建类型 ( CMAKE_BUILD_TYPE )。大多数 CMake 项目会在许多场合使用它来决定诸如诊断信息的详细程度、调试信息的存在与否以及生成产物的优化级别等事项。

对于单配置生成器(如 GNU Make 和 Ninja),您应在配置阶段指定构建类型 ,并为每种配置类型生成独立的构建树。此处使用的值为 Debug 、 Release 、 MinSizeRel 或 RelWithDebInfo 。若缺少此信息,可能会对依赖该配置的项目产生未定义的影响。

这是一个示例:

cmake -S . -B ../build -D CMAKE_BUILD_TYPE=Release

请注意,多配置生成器是在构建阶段进行配置的。

出于诊断目的,我们还可以使用 -L 选项对 list cache 变量进行调试:

cmake -L -S <source tree> -B <build tree>

 有时,项目作者可能会提供带有变量的有见地的帮助信息——要打印它们,请添加 H 修饰符:

cmake -LH -S <source tree> -B <build tree>
cmake -LAH -S <source tree> -B <build tree>

值得注意的是,手动通过 -D 选项添加的自定义变量在此打印输出中不可见,除非你指定了受支持的类型之一。

移除一个或多个变量可通过以下选项实现:

cmake -U <globbing_expr> -S <source tree> -B <build tree>

此处的通配表达式支持 * (通配符)和 ? (任意字符)符号。使用时需谨慎,因为很容易误删超出预期的变量数量。

-U 和 -D 选项均可重复多次使用。

调试与追踪

cmake 命令可通过多种选项运行,让您深入了解内部机制。要获取有关变量、命令、宏及其他设置的一般信息,请运行以下命令:

cmake --system-information [file]

可选的文件参数允许你将输出存储到文件中。在构建树目录下运行该命令,将打印有关缓存变量的额外信息以及日志文件中的构建消息。

在我们的项目中,我们将使用 message() 命令来报告构建过程的详细信息。CMake 会根据当前日志级别(默认为 STATUS )过滤这些日志输出。以下行指定了我们感兴趣的日志级别:

cmake --log-level=<level>

此处, level 可以是以下任意一种: ERROR 、 WARNING 、 NOTICE 、 STATUS 、 VERBOSE 、 DEBUG 或 TRACE 。您可以在 CMAKE_MESSAGE_LOG_LEVEL 缓存变量中永久指定此设置。

另一个有趣的选项允许您显示日志上下文与每个 message() 调用。为了调试非常复杂的项目, CMAKE_MESSAGE_CONTEXT 变量可以像堆栈一样使用。每当代码进入一个有趣的上下文时,您可以对其进行描述性命名。通过这种方式,我们的消息将使用当前 CMAKE_MESSAGE_CONTEXT 变量进行装饰,如下所示:

[some.context.example] Debug message.

启用此类日志输出的选项如下:

cmake --log-context <source tree>

我们将在第 2 章 CMake 语言 》中更详细地讨论命名上下文和日志命令。

如果其他方法都失败,我们需要动用终极武器时,总有追踪模式可用。该模式会打印每个执行的命令及其文件名、调用行号以及传递的参数列表。你可以通过以下方式启用它:

cmake --trace

可以想象,由于输出内容非常冗长,不建议在日常使用中启用该模式。

配置预设项

用户可以通过许多选项从项目中生成构建树 。在处理构建树路径、生成器、缓存和环境变量时,很容易混淆或遗漏某些内容。开发者可以通过提供一个 CMakePresets.json 文件来简化用户与项目的交互,该文件可以指定一些默认值。

要列出所有可用的预设,请执行以下操作:

cmake --list-presets

您可以使用以下任一预设方案:

cmake --preset=<preset> -S <source> -B <build tree>

要了解更多信息,请参阅本章的项目文件导航部分以及第 16 章编写 CMake 预设 

清理构建目录

偶尔,我们可能需要删除生成的文件。这可能是由于构建之间环境发生了一些变化,或者只是为了确保我们从一个干净的状态开始工作。我们可以手动删除构建树目录,或者直接在命令行中添加 --fresh 参数:

cmake --fresh -S <source tree> -B <build tree>

CMake 随后将以系统无关的方式清除 CMakeCache.txt 和 CMakeFiles/ ,并从头开始生成构建系统。

构建项目

生成构建树后,我们就可以进行构建项目操作了。CMake 不仅知道如何为多种不同的构建器生成输入文件,还能根据项目需求提供适当参数并为我们运行这些构建器。

避免直接调用 MAKE

许多网络资料建议在生成阶段后直接通过 make 命令运行 GNU Make。由于 GNU Make 是 Linux 和 macOS 的默认生成器,这种建议可能有效。但请改用本节描述的方法,因为这是与生成器无关且官方支持所有平台的方案。这样您就无需担心应用程序每个用户的具体环境差异。

构建模式的语法为:

cmake --build <build tree> [<options>] [-- <build-tool-options>]

在大多数情况下,只需提供最基本的配置即可成功完成构建:

cmake --build <build tree>

唯一必需的参数是生成的构建树路径。该路径与生成阶段通过 -B 参数传递的路径相同。

CMake 允许您指定适用于所有构建器的关键构建参数。如需向选定的原生构建器提供特殊参数,请在命令末尾 -- 标记后传递这些参数:

cmake --build <build tree> -- <build tool options>

让我们看看还有哪些其他可用选项。

运行并行构建

默认情况下,许多构建工具会利用现代处理器的多核特性,通过并行编译来提升效率。构建系统能够识别项目依赖结构,因此可以同时对满足依赖关系的步骤进行处理,从而为用户节省时间。

若您希望在多核机器上加快构建速度(或强制单线程构建以进行调试),可能需要覆盖该设置。

只需通过以下任一选项指定作业数量:

cmake --build <build tree> --parallel [<number of jobs>]
cmake --build <build tree> -j [<number of jobs>]

另一种方法是使用 CMAKE_BUILD_PARALLEL_LEVEL 环境变量进行设置。命令行选项将覆盖此变量。

选择要构建和清理的目标

每个项目都由一个或多个部分组成,这些部分称为目标 (我们将在本书第二部分讨论)。通常,我们会希望构建所有可用目标;但有时,我们可能希望跳过某些目标,或显式构建那些在常规构建中被刻意排除的目标。具体操作如下:

cmake --build <build tree> --target <target1> --target <target2> …

我们可以通过重复 –target 参数来指定多个构建目标。此外,还可以使用简写版本 -t <target> 作为替代。

清理构建目录

一个通常不会构建的特殊目标叫做 clean 。构建它具有特殊效果,会清除构建目录中的所有工件,以便后续可以从头开始重新创建所有内容。你可以这样启动该过程:

cmake --build <build tree> -t clean

此外,如果希望先清理再执行常规构建,CMake 还提供了一个便捷别名:

cmake --build <build tree> --clean-first

此操作与清理构建树章节中提到的清理不同,因为它仅影响目标工件而不涉及其他内容(如缓存)。

为多配置生成器配置构建类型

那么,我们已经对生成器有了一些了解:它们形态各异、大小不一。其中一些生成器能够在单一构建树中同时支持构建 Debug 和 Release 两种构建类型。支持此功能的生成器包括 Ninja Multi-Config、Xcode 和 Visual Studio。其余所有生成器均为单配置生成器,它们需要为每种要构建的配置类型单独创建构建树。

选择 Debug 、 Release 、 MinSizeRel 或 RelWithDebInfo 并按如下方式指定:

cmake --build <build tree> --config <cfg>

否则,CMake 将默认使用 Debug 。

调试构建过程

当出现问题时,我们首先要做的是检查输出信息。然而,经验丰富的开发者都知道,始终打印所有细节会令人困惑,因此他们通常会默认隐藏这些信息。当我们需要深入了解时,可以通过让 CMake 输出详细日志来获取更详尽的信息:

cmake --build <build tree> --verbose
cmake --build <build tree> -v

通过设置 CMAKE_VERBOSE_MAKEFILE 缓存变量可以达到相同的效果。

安装项目

当构件构建完成后,用户可以将其安装到系统中。通常这意味着将文件复制到正确的目录、安装库文件或执行 CMake 脚本中的自定义安装逻辑。

安装模式的语法为:

cmake --install <build tree> [<options>]

与其他操作模式一样,CMake 需要指定生成构建树的路径:

cmake --install <build tree>

安装操作还提供了许多额外选项。让我们看看它们的功能。

选择安装目录

我们可以为安装路径添加自定义前缀(例如,当我们对某些目录的写入权限受限时)。添加了 /home/user 前缀的 /usr/local 路径会变成 /home/user/usr/local 。

此选项的签名如下:

cmake --install <build tree> --install-prefix <prefix>

如果您使用的是 CMake 3.21 或更早版本,则必须使用一个不太明确的选项:

cmake --install <build tree> --prefix <prefix>

请注意,这在 Windows 上不起作用,因为该平台上的路径通常以驱动器号开头。

多配置生成器的安装

与构建阶段类似,我们可以指定安装时使用的构建类型(更多详情请参阅项目构建章节)。可用类型包括 Debug 、 Release 、 MinSizeRel 和 RelWithDebInfo 。签名如下:

cmake --install <build tree> --config <cfg>
选择要安装的组件

作为开发者,您可能希望将项目拆分为可独立安装的组件。我们将在第 14 章安装与打包中详细讨论组件概念。现在,我们只需将其理解为不需要在每种情况下都使用的构件集合,例如 application 、 docs 和 extra-tools 。

要安装单个组件,请使用以下选项:

cmake --install <build tree> --component <component>
设置文件权限

如果在类 Unix 平台上执行安装,您可以使用以下选项指定安装目录的默认权限,格式为 u=rwx,g=rx,o=rx :

cmake --install <build tree>
      --default-directory-permissions <permissions>
调试安装过程

与构建阶段类似,我们也可以选择查看安装阶段的详细输出。为此,可使用以下任一方法:

cmake --install <build tree> --verbose
cmake --install <build tree> -v

若设置了 VERBOSE 环境变量,也能达到同样效果。

运行脚本

CMake 项目使用 CMake 自定义语言进行配置。这种语言跨平台且功能强大。既然它已经存在,何不将其用于其他任务?果然,CMake 可以运行独立脚本(更多内容见发现脚本和模块章节),如下所示:

cmake [{-D <var>=<value>}...] -P <cmake script file>
      [-- <unparsed options>...]

运行此类脚本不会执行任何配置生成阶段,也不会影响缓存。

有两种方法可以向此脚本传递值:

通过使用 -D 选项定义的变量
通过可在 -- 标记后传递的参数

CMake 会为传递给脚本的所有参数创建 CMAKE_ARGV<n> 变量(包括 -- 标记)。

运行命令行工具

在极少数情况下,我们可能需要以跨平台的方式运行单个命令——可能是复制文件或计算校验和。并非所有平台都完全相同,因此并非所有命令在每个系统中都可用(或者它们的命名方式不同)。

CMake 提供了一种模式,可以跨平台以相同方式执行大多数常见命令。其语法为:

cmake -E <command> [<options>]

由于该特定模式的使用相当有限,我们不会深入探讨。不过,如果您对细节感兴趣,建议调用 cmake -E 列出所有可用命令。若要简单了解支持的功能,CMake 3.26 支持以下命令: capabilities 、 cat 、 chdir 、 compare_files 、 copy 、 copy_directory 、 copy_directory_if_different 、 copy_if_different 、 echo 、 echo_append 、 env 、 environment 、 make_directory 、 md5sum 、 sha1sum 、 sha224sum 、 sha256sum 、 sha384sum 、 sha512sum 、 remove 、 remove_directory 、 rename 、 rm 、 sleep 、 tar 、 time 、 touch 、 touch_nocreate 、 create_symlink 、 create_hardlink 、 true 和 false 。

若您想使用的命令缺失或需要更复杂的行为,可考虑将其封装在脚本中并通过 -P 模式运行。

运行工作流预设

我们在工作原理章节中提到,使用 CMake 构建分为三个阶段:配置、生成和构建。此外,我们还可以运行自动化测试,甚至用 CMake 创建可再发行包。通常用户需要通过命令行调用相应的 cmake 操作来手动执行每个步骤。但高级项目可以指定工作流预设 ,将多个步骤捆绑成只需一条命令即可执行的单一操作。目前我们仅提及用户可通过运行以下命令获取可用预设列表:

cmake ––workflow --list-presets

他们可以通过以下方式执行预设的工作流程:

cmake --workflow --preset <name>

这将在第 16 章编写 CMake 预设中深入讲解。

获取帮助

CMake 提供可通过命令行访问的广泛帮助功能,这并不令人意外。帮助模式的语法如下:

cmake --help

这将打印出可供深入探讨的主题列表,并解释需要向命令添加哪些参数以获取更多帮助。

CTest 命令行工具

自动化测试对于生成和维护高质量代码至关重要。CMake 套件为此专门提供了一个名为 CTest 的命令行工具,旨在标准化测试运行和报告的方式。作为 CMake 用户,您无需了解测试特定项目的细节:使用什么框架或如何运行它。CTest 提供了一个便捷的接口来列出、筛选、随机排序、重试和限时运行测试。

要运行已构建项目的测试,我们只需在生成的构建树中调用 ctest :

$ ctest
Test project /tmp/build
Guessing configuration Debug
    Start 1: SystemInformationNew
1/1 Test #1: SystemInformationNew .........   Passed 3.19 sec
100% tests passed, 0 tests failed out of 1
Total Test time (real) =   3.24 sec

我们专门用一整章来讨论这个主题: 第 11 章  测试框架 

CPack 命令行工具

在我们构建并测试了出色的软件后,便准备将其分享给全世界。极少数高级用户可以直接使用源代码,但绝大多数用户出于便利和省时考虑,都会选择预编译的二进制文件。

CMake 在这方面也提供了完善支持——内置的 CPack 工具能创建多种平台的分发包:压缩归档文件、可执行安装程序、安装向导、NuGet 包、macOS 应用包、DMG 磁盘映像、RPM 包等等。

CPack 的工作方式与 CMake 高度相似:通过 CMake 语言进行配置,并提供多种打包生成器可供选择(注意不要与 CMake 构建系统生成器混淆)。我们将在第 14 章安装与打包中详细介绍,因为该工具主要面向成熟的 CMake 项目使用。

CMake GUI

CMake 为 Windows 平台提供了图形界面版本,用于配置已准备项目的构建过程。对于类 Unix 平台,则提供了基于 Qt 库构建的版本。Ubuntu 系统可通过 cmake-qt-gui 软件包获取该工具。

要访问 CMake GUI,请运行可执行文件: cmake-gui

图形用户界面(GUI)应用程序为您的用户提供了便利:对于那些不熟悉命令行操作、更倾向于使用图形界面的用户来说尤为实用。

改用命令行工具

我强烈建议最终用户使用 GUI 界面,但对于像您这样的程序员,我建议避免任何需要每次构建程序时都手动点击表单的阻塞性操作。这在成熟项目中尤其有利,因为整个构建过程可以完全无需用户交互就能执行完成。

CCMake 命令行工具

ccmake 可执行文件是类 Unix 平台上 CMake 的交互式文本用户界面(除非明确构建,否则在 Windows 上不可用)。我在此提及它是为了让您看到时知道它是什么( 图 1.4),但和 GUI 一样,开发者直接编辑 CMakeCache.txt 文件会获得更大收益。

 

解决了这个问题后,我们已经完成了 CMake 套件命令行的基础介绍。现在该来探索典型 CMake 项目的结构了。

项目目录与文件导航

CMake 项目由相当多的文件和目录构成。我们先大致了解每个组件的作用,以便开始动手调整。这些文件可分为几个主要类别:

当然,我们会准备项目文件,作为开发者,随着项目的发展不断修改它们。
CMake 会生成一些供自身使用的文件,尽管这些文件包含 CMake 语言命令,但它们并非供开发人员编辑。任何手动修改都会被 CMake 覆盖。
某些文件专为高级用户(非项目开发者)设计,用于根据个人需求自定义 CMake 构建项目的方式。
最后,还有一些临时文件在特定情况下能提供有价值的信息。

本节还会建议哪些文件可以放入你的版本控制系统 VCS)的忽略文件中。

源代码树

这是存放项目的目录(也称为项目根目录 ),包含所有 C++源代码和 CMake 项目文件。

以下是该目录最重要的要点:

它需要一个 CMakeLists.txt 配置文件。
该目录的路径由用户通过 cmake 命令的 -S 参数指定,当生成构建系统时。
避免在 CMake 代码中硬编码任何源代码树的绝对路径——您的软件用户会将项目存储在其他路径中。

建议在此目录下初始化一个仓库,可以使用类似 Git 这样的版本控制系统。

构建树

CMake 会在用户指定的路径下创建此目录。它将存储构建系统及构建过程中生成的所有内容:项目产物、临时配置、缓存、构建日志以及原生构建工具(如 GNU Make)的输出。该目录的替代名称包括构建根目录二进制树 

关键要点:

您的构建配置(构建系统)和构建产物将在此处创建(例如二进制文件、可执行文件和库,以及用于最终链接的目标文件和归档文件)。
CMake 建议将该目录置于源代码树目录之外(这种做法称为外部构建 )。这样可以避免污染我们的项目( 内部构建 )。
生成构建系统时,通过 -B 命令指定到 cmake 。
该目录并非生成文件的最终存放位置。建议项目包含一个安装阶段,将最终产物复制到系统指定位置,并移除所有用于构建的临时文件。

不要将该目录加入版本控制系统——每个用户都应自行选择构建目录。若有充分理由需要进行内部构建,请确保将该目录加入版本控制忽略文件(如 .gitignore )。

列出文件

包含 CMake 语言的文件被称为列表文件,可以通过调用 include() 和 find_package() 相互包含,或通过 add_subdirectory() 间接包含。CMake 并未强制规定这些文件的命名规则,但按照惯例,它们通常带有 .cmake 扩展名。

项目文件

CMake 项目通过一个 CMakeLists.txt 列表文件进行配置(请注意,由于历史原因,该文件具有非常规扩展名)。该文件必须位于每个项目源代码树的顶层,并且是在配置阶段首个被执行的文件。

顶级 CMakeLists.txt 应至少包含两条命令:

cmake_minimum_required(VERSION <x.xx>) :设置预期的 CMake 版本,并通过策略机制告知 CMake 如何处理遗留行为
project(<name> <OPTIONS>) :为项目命名(提供的名称将存储在 PROJECT_NAME 变量中)并指定配置选项(更多内容详见第 2 章 CMake 语言 

随着软件规模增长,您可能希望将其划分为可单独配置和理解的小型单元。CMake 通过支持包含独立 CMakeLists.txt 文件的子目录来实现这一需求。您的项目结构可能类似以下示例:

myProject/CMakeLists.txt
myProject/api/CMakeLists.txt
myProject/api/api.h
myProject/api/api.cpp

一个非常简单的顶层 CMakeLists.txt 文件便可用于将所有内容整合起来:

cmake_minimum_required(VERSION 3.26)
project(app)
message("Top level CMakeLists.txt")
add_subdirectory(api)

项目的主要方面涵盖在顶层文件中:管理依赖项、声明需求以及检测环境。我们还使用 add_subdirectory(api) 命令从 api 子目录引入另一个 CMakeListst.txt 文件,以执行针对应用程序 API 部分的特定操作。

缓存文件

首次运行配置阶段时,缓存变量将从列表文件生成并存储于 CMakeCache.txt 中。该文件位于构建树的根目录下,格式相当简单(为简洁起见已删除部分行)

# This is the CMakeCache file.
# For build in directory: /root/build tree
# It was generated by CMake: /usr/local/bin/cmake
# The syntax for the file is as follows:
# KEY:TYPE=VALUE
# KEY is the name of a variable in the cache.
# TYPE is a hint to GUIs for the type of VALUE, DO NOT EDIT
  #TYPE!.
# VALUE is the current value for the KEY.
########################
# EXTERNAL cache entries
########################
# Flags used by the CXX compiler during DEBUG builds.
CMAKE_CXX_FLAGS_DEBUG:STRING=/MDd /Zi /Ob0 /Od /RTC1
# ... more variables here ...
########################
# INTERNAL cache entries
########################
# Minor version of cmake used to create the current loaded
  cache
CMAKE_CACHE_MINOR_VERSION:INTERNAL=19
# ... more variables here ...

如头文件注释所示,这种格式非常直观易懂。 EXTERNAL 部分的缓存条目供用户修改,而 INTERNAL 部分则由 CMake 管理。

以下是需要牢记的几个关键要点:

你可以手动管理此文件,通过调用 cmake (参见本章掌握命令行部分中的缓存选项 ),或者通过 ccmake 或 cmake-gui 进行操作。
删除此文件可将项目重置为默认配置;系统会根据列表文件重新生成它。

缓存变量可以从列表文件中读取和写入。有时变量引用评估会有些复杂;我们将在第 2 章的 CMake 语言部分更详细地讨论这个问题。

包定义文件

CMake 生态系统的重要组成部分是项目可以依赖的外部包。它们以无缝、跨平台的方式提供库和工具。希望提供 CMake 支持的包作者会随包附带一个 CMake 包配置文件。

我们将在第 14 章  安装与打包 》中学习如何编写这些文件。同时,这里有几个值得注意的细节:

Config-files(原始拼写)包含有关如何使用库二进制文件、头文件和辅助工具的信息。有时,它们会暴露可在项目中使用的 CMake 宏和函数。
配置文件被命名为 <PackageName>-config.cmake 或 <PackageName>Config.cmake 。
使用 find_package() 命令来包含包。

如果需要特定版本的包,CMake 会将其与关联的 <PackageName>-config-version.cmake 或 <PackageName>ConfigVersion.cmake 进行比对检查。

如果供应商未提供包的配置文件,有时配置会与 CMake 本身捆绑,或可通过项目中的 Find-module(保持原拼写)提供。

生成的文件

在生成阶段, cmake 可执行文件会在构建树中生成许多文件。因此,这些文件不应手动编辑。CMake 将它们用作 cmake 安装操作、CTest 和 CPack 的配置。

您可能会遇到的文件包括:

cmake_install.cmake
CTestTestfile.cmake
CPackConfig.cmake

如果正在实施源码内构建,最好将它们添加到版本控制系统(VCS)的忽略文件中。

JSON 和 YAML 文件

CMake 使用的其他格式包括 JavaScript 对象表示法 JSON)和另一种标记语言 YAML)。这些文件作为与外部工具(如 IDE)通信或提供易于生成和解析的配置的接口而被引入。

预设文件

当我们需要具体设置缓存变量、选择的生成器、构建树路径等内容时——尤其是当项目存在多种构建方式时,高级项目配置可能变得相对繁琐。这时预设功能就派上用场了——我们无需通过命令行手动配置这些值,只需提供一个存储所有细节的文件并与项目一起发布即可。自 CMake 3.25 起,预设功能还支持配置工作流 ,将各阶段(配置、构建、测试和打包)绑定到命名的执行步骤列表中。

如本章掌握命令行部分所述,用户可通过图形界面选择预设,或使用 --list-presets 命令并通过 --preset=<preset> 选项为构建系统选择预设。

预设存储在两个文件中:

CMakePresets.json :此文件供项目作者提供官方预设。
CMakeUserPresets.json :此文件专为希望按个人喜好自定义项目配置的用户而设(可将其添加到版本控制系统忽略文件中)。

项目并非必须使用预设,它们仅在高级场景中发挥作用。详情请参阅第 16 章编写 CMake 预设 

基于文件的 API

CMake 3.14 引入了一个允许外部工具查询构建系统信息的 API:生成文件的路径、缓存条目、工具链等。我们提及这个非常高级的主题,只是为了避免您在文档中遇到基于文件的 API 这个短语时产生困惑。其名称暗示了它的工作原理:必须将一个包含查询的 JSON 文件放置在构建树中的特定路径下。CMake 在生成构建系统时会读取该文件,并将响应写入另一个文件,以便外部应用程序进行解析。

基于文件的 API 被引入以取代一种已弃用的机制,该机制称为服务器模式 (或称 cmake-server ),此机制最终在 CMake 3.26 版本中被移除。

配置日志

自 3.26 版本起,CMake 将在以下路径提供结构化日志文件,用于对配置阶段进行高级调试:

<build tree>/CMakeFiles/CMakeConfigureLog.yaml

这正是那种平时无需已关注、但关键时刻却至关重要的功能。

忽略 Git 中的文件

版本控制系统(VCS)种类繁多,Git 是其中最流行的一种。每当开始新项目时,确保仅将必要文件添加至代码库十分重要。通过在 .gitignore 文件中指定不需要的文件,能更轻松地保持项目整洁。例如,我们可以排除自动生成的、用户特定的或临时性的文件。

Git 在形成新提交时会自动跳过这些文件。以下是我在项目中使用的文件示例:

ch01/01-hello/.gitignore

CMakeUserPresets.json
# If in-source builds are used, exclude their output like so:
build_debug/
build_release/
# Generated and user files
**/CMakeCache.txt
**/CMakeUserPresets.json
**/CTestTestfile.cmake
**/CPackConfig.cmake
**/cmake_install.cmake
**/install_manifest.txt
**/compile_commands.json

此刻,你手握通往项目文件海洋的地图。有些文件至关重要,你会频繁使用它们——而其他文件则不然。虽然了解这些看似无关紧要的文件可能像在浪费时间,但知道哪些地方无需查找答案却可能价值连城。无论如何,本章最后一个问题依然存在:你还能用 CMake 创建哪些独立的功能单元?

发现脚本与模块

CMake 主要专注于构建生成产物的项目,这些产物会被其他系统(如 CI/CD 流水线、测试平台,或部署到机器上、存储在制品仓库中)所使用。然而,CMake 中还有另外两个使用其语言的概念:脚本和模块。下面我们来解释它们是什么以及有何区别。

脚本

CMake 提供了一种与平台无关的编程语言,附带许多实用命令。用该语言编写的脚本既可以捆绑在大型项目中,也可以完全独立存在。

可以将其视为实现跨平台工作的一致性方法。通常,要执行某项任务,您需要为 Linux 单独编写 Bash 脚本,为 Windows 单独编写批处理文件或 PowerShell 脚本等等。CMake 将这些细节抽象化,使您只需一个文件即可在所有平台上正常运行。当然,您也可以使用 Python、Perl 或 Ruby 脚本等外部工具,但这会增加额外依赖并使 C/C++项目更复杂。既然大多数情况下用更简单的方式就能完成任务,何必再引入另一种语言呢?就用 CMake 吧!

我们从掌握命令行部分已经了解到,可以使用 -P 选项来执行脚本: cmake -P script.cmake 。

但我们实际对要使用的脚本文件有哪些要求呢?其实并不复杂:脚本可以随心所欲地复杂,也可以只是一个空文件。不过仍然建议在每个脚本开头调用 cmake_minimum_required() 命令。该命令会告知 CMake 应对本项目后续命令应用哪些策略(更多内容详见第 4 章  配置首个 CMake 项目 )。

这是一个简单脚本的示例:

第一章/02-脚本/脚本.cmake

# An example of a script
cmake_minimum_required(VERSION 3.26.0)
message("Hello world")
file(WRITE Hello.txt "I am writing to a file")

运行脚本时,CMake 不会执行任何常规阶段(如配置或生成),也不会使用缓存,因为在脚本中不存在源代码树构建树的概念。这意味着项目特定的 CMake 命令在脚本模式下不可用/无法使用。就这些。祝您脚本编写愉快!

实用模块

CMake 项目可以使用外部模块来增强其功能。这些模块采用 CMake 语言编写,包含宏定义、变量以及执行各类功能的命令。其复杂程度不一,既有非常复杂的脚本(如 CPack 和 CTest 提供的模块),也有相当简单的模块,例如 AddFileDependencies 或 TestBigEndian 。

CMake 发行版内置了 80 多种不同的实用模块。若仍不满足需求,您可以通过浏览精选列表(如 https://github.com/onqtam/awesome-cmake 上的资源)从互联网下载更多模块,或者从头开始编写自己的模块。

要使用实用模块,我们需要调用 include(<MODULE>) 命令。以下是一个展示此操作的简单项目示例:

第一章/03-模块/CMakeLists.txt

cmake_minimum_required(VERSION 3.26.0)
project(ModuleExample)
include (TestBigEndian)
test_big_endian(IS_BIG_ENDIAN)
if(IS_BIG_ENDIAN)
message("BIG_ENDIAN")
else()
message("LITTLE_ENDIAN")
endif()

随着相关主题的展开,我们将逐步了解可用的模块。若您想提前查阅,完整的捆绑模块列表可在 cmake-modules(7) — CMake 4.1.0-rc1 Documentation 找到。

查找模块

包定义文件部分中,我提到 CMake 有一种机制可以查找那些不支持 CMake 且未提供 CMake 包配置文件的外部依赖项文件。这就是 find-modules 的作用所在。CMake 提供了超过 150 个 find-modules,能够在系统中定位已安装的这些软件包。与实用工具模块一样,网上还有更多可用的 find-modules。作为最后手段,您始终可以编写自己的 find-module。

您可以通过调用 find_package() 命令并提供相关软件包名称来使用它们。这样的 find-module 会进行一场”捉迷藏”游戏,检查它所寻找软件的所有已知位置。如果找到文件,则会定义包含其路径的变量(如该模块手册中所指定)。这样,CMake 就能针对该依赖项进行构建。

例如, FindCURL 模块会搜索流行的 Client URL 库并定义以下变量: CURL_FOUND 、 CURL_INCLUDE_DIRS 、 CURL_LIBRARIES 和 CURL_VERSION_STRING 。

我们将在第 9 章 CMake 依赖项管理 》中更深入地探讨 find-modules。

总结

现在您已经了解了 CMake 是什么及其工作原理;掌握了 CMake 工具家族的核心组件以及在各种系统上的安装方法。作为高级用户,您通晓所有通过命令行运行 CMake 的方式:构建系统生成、项目构建、安装、运行脚本、命令行工具及打印帮助信息。您也熟悉 CTest、CPack 和 GUI 应用程序。这些知识将帮助您以正确的视角为用户和其他开发者创建项目。此外,您还学习了项目的构成要素:目录、列表文件、配置、预设和辅助文件,以及版本控制系统中应忽略的内容。最后,您初步了解了其他非项目文件:独立脚本和两种模块——实用模块与查找模块。

在下一章中,我们将学习如何使用 CMake 编程语言。这将使您能够编写自己的列表文件,并为您的第一个脚本、项目和模块打开大门。

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容