第三步:配置和缓存变量

CMake 项目通常有一些项目特定的配置变量,用户和打包者对此很感兴趣。CMake 有多种方式可以让调用者(用户或进程)传达这些配置选择,但其中最基本的是 -D 标志。

在本节中,我们将探讨如何在 CMake 项目内部提供项目配置选项,以及如何调用 CMake 来利用 CMake 和单个项目提供的配置选项。

背景

如果我们有一个支持多种压缩算法的压缩软件 CMake 项目,我们可能希望让项目的打包者在构建我们的软件时决定启用哪些算法。我们可以通过使用 -D 标志设置的变量来实现。

if(COMPRESSION_SOFTWARE_USE_ZLIB)
  message("I will use Zlib!")
  # ...
endif()

if(COMPRESSION_SOFTWARE_USE_ZSTD)
  message("I will use Zstd!")
  # ...
endif()
$ cmake -B build \
    -DCOMPRESSION_SOFTWARE_USE_ZLIB=ON \
    -DCOMPRESSION_SOFTWARE_USE_ZSTD=OFF
...
I will use Zlib!

当然,我们希望为这些配置选择提供合理的默认值,并提供一种沟通给定选项目的的方式。此功能由 option() 命令提供。

option(COMPRESSION_SOFTWARE_USE_ZLIB "Support Zlib compression" ON)
option(COMPRESSION_SOFTWARE_USE_ZSTD "Support Zstd compression" ON)

if(COMPRESSION_SOFTWARE_USE_ZLIB)
  # Same as before
# ...
$ cmake -B build \
    -DCOMPRESSION_SOFTWARE_USE_ZLIB=OFF
...
I will use Zstd!

-D 标志和 option() 创建的名称不是普通变量,它们是 **缓存** 变量。缓存变量是全局可见的、粘性 的变量,其值在首次设置后很难更改。事实上,它们是如此粘性,以至于在项目模式下,CMake 会在多次配置之间保存和恢复缓存变量。如果一个缓存变量被设置一次,它将保持不变,直到另一个 -D 标志覆盖了已保存的变量。

注意

CMake 本身有几十个用于配置的普通变量和缓存变量。这些变量在 cmake-variables(7) 中进行了文档说明,并且与项目提供的配置变量以相同的方式运行。

set() 也可以用来操作缓存变量,但不会更改已创建的变量。

set(StickyCacheVariable "I will not change" CACHE STRING "")
set(StickyCacheVariable "Overwrite StickyCache" CACHE STRING "")

message("StickyCacheVariable: ${StickyCacheVariable}")
$ cmake -P StickyCacheVariable.cmake
StickyCacheVariable: I will not change

由于 -D 标志在任何其他命令之前处理,因此它们在设置缓存变量值时具有优先权。

$ cmake \
  -DStickyCacheVariable="Commandline always wins" \
  -P StickyCacheVariable.cmake
StickyCacheVariable: Commandline always wins

虽然缓存变量通常不能更改,但它们可以被普通变量 *覆盖*。我们可以通过设置一个与缓存变量同名的变量,然后使用 set() 来设置一个普通变量,然后使用 unset() 删除普通变量来观察这一点。

set(ShadowVariable "In the shadows" CACHE STRING "")
set(ShadowVariable "Hiding the cache variable")
message("ShadowVariable: ${ShadowVariable}")

unset(ShadowVariable)
message("ShadowVariable: ${ShadowVariable}")
$ cmake -P ShadowVariable.cmake
ShadowVariable: Hiding the cache variable
ShadowVariable: In the shadows

练习 1 - 使用选项

我们可以设想这样一个场景:消费者非常想要我们的 MathFunctions 库,而 Tutorial 实用程序是“可选项”。在这种情况下,我们可能希望添加一个选项,允许消费者禁用构建我们的 Tutorial 可执行文件,只构建 MathFunctions 库。

借助我们对选项、条件和缓存变量的了解,我们拥有了使此配置可用所需的所有要素。

目标

添加一个名为 TUTORIAL_BUILD_UTILITIES 的选项,以控制是否配置和构建 Tutorial 可执行文件。

注意

CMake 允许我们确定在配置后构建哪些目标。我们的用户可以仅请求 MathFunctions 库而不请求 Tutorial。CMake 还有机制可以从 ALL(构建所有其他可用目标的默认目标)中排除目标。

然而,完全排除目标配置的选项非常方便且受欢迎,特别是当配置这些目标涉及可能花费一些时间的重量级步骤时。

这也能简化 install() 逻辑,我们将在后续步骤中讨论,如果打包者不感兴趣的目标被完全排除。

有用资源

要编辑的文件

  • CMakeLists.txt

开始

Help/guide/tutorial/Step3 文件夹包含 Step1 的完整、推荐的解决方案以及本节相关的 TODO。花点时间回顾并熟悉 Tutorial 项目。

当你认为你理解了当前代码后,从 TODO 1 开始,完成到 TODO 2

构建并运行

我们现在可以重新配置我们的项目。但是,这次我们要通过 -D 标志来控制配置。我们再次从导航到 Help/guide/tutorial/Step3 并调用 CMake 开始,但这次带有我们的配置选项。

cmake -B build -DTUTORIAL_BUILD_UTILITIES=OFF

我们现在可以像往常一样构建。

cmake --build build

构建完成后,我们应该会发现没有生成 Tutorial 可执行文件。因为缓存变量是粘性的,即使重新配置也不会改变这一点,尽管选项默认是 ON

cmake -B build
cmake --build build

不会生成 Tutorial 可执行文件,缓存变量是“锁定的”。要改变这一点,我们有两种选择。首先,我们可以编辑存储 CMake 配置运行之间缓存变量的文件,即“CMake Cache”。此文件是 build/CMakeCache.txt,其中我们可以找到选项缓存变量。

//Build the Tutorial executable
TUTORIAL_BUILD_UTILITIES:BOOL=OFF

我们可以将其从 OFF 更改为 ON,然后重新构建,我们将获得 Tutorial 可执行文件。

注意

CMakeCache.txt 条目形式为 <Name>:<Type>=<Value>,但是“类型”仅为提示。CMake 中的所有对象都是字符串,无论缓存如何说。

或者,我们可以在命令行上更改缓存变量的值,因为命令行在加载 CMakeCache.txt 之前运行,其值优先于缓存文件中的值。

cmake -B build -DTUTORIAL_BUILD_UTILITIES=ON
cmake --build build

这样做,我们观察到 CMakeCache.txt 中的值已从 OFF 翻转为 ON,并且构建了 Tutorial 可执行文件。

解决方案

首先,我们创建 option() 来为我们的缓存变量提供一个合理的默认值。

TODO 1:点击显示/隐藏答案
TODO 1: CMakeLists.txt
option(TUTORIAL_BUILD_UTILITIES "Build the Tutorial executable" ON)

然后,我们可以检查缓存变量以有条件地启用 Tutorial 可执行文件(通过添加其子目录)。

TODO 2:点击显示/隐藏答案
TODO 2: CMakeLists.txt
if(TUTORIAL_BUILD_UTILITIES)
  add_subdirectory(Tutorial)
endif()

练习 2 - CMAKE 变量

CMake 提供了几个重要的普通变量和缓存变量,供打包者控制构建。编译器、默认标志、软件包搜索位置等决策都由 CMake 自有的配置变量控制。

其中最重要的是语言标准。语言标准可能对给定软件包提供的 ABI 产生重大影响。例如,库经常使用较新标准中的标准 C++ 模板,并在早期标准中提供填充程序。如果一个库在不同的标准下被使用,标准模板和填充程序之间的 ABI 不兼容可能导致难以理解的错误和运行时崩溃。

确保我们的所有目标都使用相同的语言标准是通过 CMAKE_<LANG>_STANDARD 缓存变量实现的。对于 C++,它是 CMAKE_CXX_STANDARD

注意

由于这些变量如此重要,因此开发者不应在他们的 CML 中覆盖或隐藏它们。在 CML 中隐藏 CMAKE_<LANG>_STANDARD,因为库想要 C++20,而打包者已决定使用 C++23 构建其其余的库和应用程序,这可能导致前面提到的可怕的、难以理解的错误。

除非有非常强烈的理由,否则不要在 CMAKE_ 全局变量上使用 set()。我们将在后续步骤中讨论更好的方法,让目标传达定义和最低标准等要求。

在此练习中,我们将向我们的库和可执行文件引入一些 C++20 代码,并通过设置适当的缓存变量来使用 C++20 构建它们。

目标

使用 `std::format` 而不是流运算符来格式化打印的字符串。为确保 `std::format` 的可用性,请将 CMake 配置为对 C++ 目标使用 C++20 标准。

有用资源

要编辑的文件

  • Tutorial/Tutorial.cxx

  • MathFunctions/MathFunctions.cxx

开始

继续编辑 Step3 中的文件。完成 TODO 3TODO 7。我们将修改我们的打印语句,使用 std::format 而不是流运算符。

确保你的缓存变量已设置,以便构建 Tutorial 可执行文件,使用上一个练习中讨论的任何方法。

构建并运行

我们需要用新的标准重新配置我们的项目,我们可以使用与我们的 TUTORIAL_BUILD_UTILITIES 缓存变量相同的方法来做到这一点。

cmake -B build -DCMAKE_CXX_STANDARD=20

注意

根据约定,配置变量以提供变量的提供者作为前缀。CMake 配置变量以 CMAKE_ 开头,而项目应以 `_` 作为其变量前缀。

教程配置变量遵循此约定,并以 TUTORIAL_ 开头。

现在我们已经配置了 C++20,我们可以像往常一样构建。

cmake --build build

解决方案

我们需要包含 `` 并使用它。

TODO 3-5: 点击显示/隐藏答案
TODO 3: Tutorial/Tutorial.cxx
#include <format>
#include <iostream>
#include <string>
TODO 4: Tutorial/Tutorial.cxx
if (argc < 2) {
  std::cout << std::format("Usage: {} number\n", argv[0]);
  return 1;
}
TODO 5: Tutorial/Tutorial.cxx
// calculate square root
double const outputValue = mathfunctions::sqrt(inputValue);
std::cout << std::format("The square root of {} is {}\n", inputValue,
                         outputValue);

同样也适用于 MathFunctions 库。

TODO 6-7: 点击显示/隐藏答案
TODO 6: MathFunctions.cxx
#include <format>
#include <iostream>
TODO 7: MathFunctions.cxx
double delta = x - (result * result);
result = result + 0.5 * delta / result;

std::cout << std::format("Computing sqrt of {} to be {}\n", x, result);

练习 3 - CMakePresets.json

管理这些配置值很快就会变得不堪重负。在 CI 系统中,适合将它们记录为给定 CI 步骤的一部分。例如,在 Github Actions CI 步骤中,我们可能会看到类似以下的示例

- name: Configure and Build
  run: |
    cmake \
      -B build \
      -DCMAKE_BUILD_TYPE=Release \
      -DCMAKE_CXX_STANDARD=20 \
      -DCMAKE_CXX_EXTENSIONS=ON \
      -DTUTORIAL_BUILD_UTILITIES=OFF \
      # Possibly many more options
      # ...

    cmake --build build

在本地开发代码时,即使只输入一次所有这些选项也可能容易出错。如果出于任何原因需要重新配置,多次执行此操作可能会令人筋疲力尽。

解决此问题的方法有很多种,您的选择最终取决于您作为开发者的偏好。面向 CLI 的开发者通常使用任务运行器来调用 CMake 并为其项目设置所需的选项。大多数 IDE 也有一个自定义机制来控制 CMake 配置。

在此处不可能完全枚举所有可能的配置工作流程。相反,我们将探讨 CMake 内置的解决方案,称为 CMake Presets。Presets 提供了一种格式来命名和表达 CMake 配置选项的集合。

注意

Presets 能够表达完整的 CMake 工作流程,从配置到构建,再到安装软件软件包。

它们比我们在这里的空间要灵活得多。我们将仅限于使用它们进行配置。

CMake Presets 包含在两个标准文件中:CMakePresets.json,它旨在成为项目的一部分并纳入源代码控制;以及 CMakeUserPresets.json,它旨在用于本地用户配置,不应纳入源代码控制。

对开发人员最有用的最简单的 preset 仅仅是配置变量。

{
  "version": 4,
  "configurePresets": [
    {
      "name": "example-preset",
      "cacheVariables": {
        "EXAMPLE_FOO": "Bar",
        "EXAMPLE_QUX": "Baz"
      }
    }
  ]
}

在调用 CMake 时,之前我们会这样做:

cmake -B build -DEXAMPLE_FOO=Bar -DEXAMPLE_QUX=Baz

我们现在可以使用 preset:

cmake -B build --preset example-preset

CMake 将搜索名为 CMakePresets.jsonCMakeUserPresets.json 的文件,并在可用时加载其中的命名配置。

注意

命令行标志可以与 presets 混合。命令行标志优先于 preset 中的值。

Presets 还支持有限的宏,这些宏可以在 preset 中进行花括号展开。唯一对我们感兴趣的是 `${sourceDir}` 宏,它展开为项目的根目录。我们可以使用它来设置我们的构建目录,从而在配置项目时跳过 `-B` 标志。

{
  "name": "example-preset",
  "binaryDir": "${sourceDir}/build"
}

目标

使用 CMake Preset 而不是命令行标志来配置和构建教程。

有用资源

要编辑的文件

  • CMakePresets.json

入门

继续编辑 Step3 中的文件。完成 TODO 8TODO 9

注意

CMakePresets.json 中的 TODOs 需要被 *替换*。当你完成练习时,文件中不应该有任何 TODO 键。

在配置之前删除现有的构建文件夹,可以验证 preset 是否正常工作,这将确保你没有重复使用现有的 CMake Cache 进行配置。

注意

在 CMake 3.24 及以上版本中,可以通过使用 `cmake --fresh` 来达到相同的效果。

所有未来的配置更改都将通过 `CMakePresets.json` 文件进行。

构建和运行

我们现在可以使用 preset 文件来管理我们的配置。

cmake --preset tutorial

Presets 能够为我们运行构建步骤,但在此教程中,我们将继续自己运行构建。

cmake --build build

解决方案

我们需要进行两个更改:首先,我们将构建目录(也称为“二进制目录”)设置为项目文件夹下的 `build` 子目录;其次,我们需要将 `CMAKE_CXX_STANDARD` 设置为 `20`。

TODO 8-9: 点击显示/隐藏答案
TODO 8-9: CMakePresets.json
{
  "version": 4,
  "configurePresets": [
    {
      "name": "tutorial",
      "displayName": "Tutorial Preset",
      "description": "Preset to use with the tutorial",
      "binaryDir": "${sourceDir}/build",
      "cacheVariables": {
        "CMAKE_CXX_STANDARD": "20"
      }
    }
  ]
}