现代C++ CMake 指南 – 16 编写 CMake 预设

CMake 在 3.19 版本中引入了预设功能,以简化项目管理设置。在预设出现前,用户需要记忆冗长的命令行配置或直接在项目文件中设置覆盖项,这种方式既复杂又容易出错。预设功能让用户能够以更直观的方式处理各类设置,包括项目配置使用的生成器、并发构建任务数量、以及需要构建或测试的项目组件等。通过预设,CMake 的使用变得更加简单。用户只需一次性设置好预设,即可在需要时随时调用,使得每次执行 CMake 时都能保持一致性且更易于理解。预设还有助于在不同用户和计算机之间标准化设置,从而简化协作项目工作。

预设功能兼容 CMake 的四种主要模式:构建系统配置、构建、运行测试和打包。它们允许用户将这些环节串联成工作流,使整个过程更加自动化且有条理。此外,预设还提供了条件判断和宏表达式(简称宏)等功能,赋予用户更强大的控制能力。

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

预设文件必须放置在项目的顶层目录中,CMake 才能识别它们。每个预设文件可以为每个阶段定义多个预设:配置、构建、测试、打包以及包含多个阶段的工作流预设。用户随后可以通过 IDE、图形界面或命令行选择预设来执行。

通过向命令行添加 --list-presets 参数可以列出预设,该参数针对我们要列出的特定阶段。例如,可以使用以下命令列出构建预设:

cmake –build –list-presets

测试预设可以通过以下命令列出:

ctest –list-presets

使用项目中定义的预设
编写预设文件
定义阶段特定预设
定义工作流预设
添加条件和宏

使用项目中定义的预设

当我们需要明确指定缓存变量、选择的生成器等元素时,项目配置可能变得复杂——尤其是在存在多种构建方式的情况下。这时预设功能就派上用场了。我们无需记忆命令行参数或编写 shell 脚本来用不同参数执行 cmake ,而是可以创建预设文件并将所需配置存储在项目本身中。

CMake 使用两个可选文件来存储项目预设:

CMakePresets.json :由项目作者提供的官方预设。
CMakeUserPresets.json :专为希望在项目中添加自定义预设的用户设计。项目应将此文件添加到版本控制系统(VCS)的忽略列表中,以确保自定义设置不会意外地共享到代码仓库中。

要使用预设,我们需要遵循相同的模式,并在 –preset 参数后提供预设名称。

此外,您无法使用 cmake 命令列出包预设;需要使用 cpack 命令。以下是用于包预设的命令行示例:

cpack –preset <preset-name>

选择预设后,当然可以添加特定阶段的命令行参数,例如指定构建树或安装路径。添加的参数将覆盖预设中的任何设置。

工作流预设有一个特殊情况:在运行 cmake 命令时,如果存在额外的 --workflow 参数,则可以列出并应用这些预设。

$ cmake --workflow --list-presets
Available workflow presets:
  "myWorkflow"
$ cmake --workflow --preset myWorkflow
Executing workflow step 1 of 4: configure preset "myConfigure"
...

这就是在项目中应用和查看可用预设的方法。现在,让我们来了解预设文件的结构。

编写预设文件

CMake 会在顶层目录中搜索 CMakePresets.json 和 CMakeUserPresets.json 。这两个文件使用相同的 JSON 结构来定义预设,因此它们之间没有太多需要讨论的区别。该格式是一个包含以下键的 JSON 对象:

version :这是一个必需的整数,用于指定预设 JSON 架构的版本
cmakeMinimumRequired :这是一个对象,用于指定所需的 CMake 版本
include : 这是一个字符串数组,包含来自数组中指定文件路径的外部预设(自模式版本4起)
configurePresets : 这是一个对象数组,用于定义配置阶段的预设
buildPresets : 这是一个对象数组,用于定义构建阶段的预设
testPresets : 这是一个对象数组,专门用于测试阶段的预设
packagePresets : 这是一个包含特定于包阶段预设的对象数组
workflowPresets : 这是一个包含特定于工作流模式预设的对象数组
vendor : 这是一个包含由 IDE 和其他厂商定义的自定义设置的对象;CMake 不会处理此字段

在编写预设时,CMake 要求必须包含 version 条目;其他值均为可选。以下是一个预设文件示例(实际预设将在后续章节中添加):

{
  "version": 6,
  "cmakeMinimumRequired": {
    "major": 3,
    "minor": 26,
    "patch": 0
  },
  "include": [],
  "configurePresets": [],
  "buildPresets": [],
  "testPresets": [],
  "packagePresets": [],
  "workflowPresets": [],
  "vendor": {
    "data": "IDE-specific information"
  }
}

并不需要像前面示例那样添加空数组;除了 version 之外的其他条目都是可选的。说到这个,CMake 3.26 对应的正确模式版本是 6 。

既然我们已经了解了预设文件的结构,现在就来实际学习如何定义预设本身。

定义阶段特定预设

阶段特定预设就是用于配置各个 CMake 阶段的预设:配置(configure)、构建(build)、测试(test)、打包(package)和安装(install)。它们允许采用细粒度的结构化方法来定义构建配置。以下是所有预设阶段共有的常见特性概述,随后将介绍如何为各个阶段定义预设。

预设的通用特性

有三个特性用于配置预设,无论 CMake 处于哪个阶段。它们分别是唯一名称字段、可选字段以及与配置预设的关联。接下来的章节将分别介绍这些特性。

唯一名称字段

每个预设在其所属阶段必须具有唯一的名称字段。鉴于 CMakeUserPresets.json (如果存在)隐式包含 CMakePresets.json (如果存在),这两个文件共享命名空间,从而防止名称重复。例如,您不能在两个文件中同时存在名为 myPreset 的打包阶段预设。

一个最小的预设文件可能如下所示:

{
  "version": 6,
  "configurePresets": [
    {
      "name": "myPreset"
    },
    {
      "name": "myPreset2"
    }
  ]
}

可选字段

每个阶段特定的预设都可以使用相同的可选字段:

displayName : 这是一个字符串,为预设提供用户友好的名称
description : 这是一个字符串,用于解释预设功能的作用
inherits : 这是一个字符串或字符串数组,其作用是将本字段中指定的预设配置作为基础进行复制,以便进一步扩展或修改
hidden : 这是一个布尔值,用于将预设从列表中隐藏;此类隐藏预设只能通过继承方式使用
environment : 这是一个对象,用于覆盖本阶段的 ENV 变量;每个键标识一个独立变量,值可以是字符串或 null ;支持宏
condition : 这是一个对象,用于启用或禁用此预设(后续将详细说明)
vendor :这是一个自定义对象,包含供应商特定的值,并遵循与根级 vendor 字段相同的约定

预设可以形成类似图的继承结构,前提是不存在循环依赖。 CMakeUserPresets.json 可以从项目级预设继承,但反之则不行。

与配置阶段预设的关联

所有阶段特定的预设都必须与一个配置预设相关联,因为它们需要知道构建树的位置。虽然 configure 预设本质上与自身相关联,但构建、测试和打包预设需要通过 configurePreset 字段显式定义这种关联。

与您可能认为的相反,这种关联并不意味着当您决定运行任何后续预设时,CMake 会自动执行配置预设。您仍然需要手动执行每个预设,或者使用工作流预设(我们稍后会讲到这一点)。

有了这些基础概念后,我们可以继续探讨各个阶段预设的具体内容,从配置阶段开始。随着深入讲解,我们将探索这些预设如何相互作用,以及如何利用它们来简化 CMake 中的项目配置和构建流程。

定义配置阶段预设

如前所述,配置预设存储在 configurePresets 数组中。通过在命令行中添加特定于配置阶段的 --list-presets 参数,可以列出这些预设:

cmake –list-presets

要使用选定的预设配置项目,请在 --preset 参数后指定其名称,如下所示:

cmake –preset myConfigurationPreset

该配置预设包含一些通用字段如 name 和 description ,同时也具有自身特有的一组可选字段。以下是最重要字段的简化说明:

generator : 指定预设使用的生成器的字符串;对于模式版本< 3 为必填项
architecture 和 toolset :用于配置支持这些选项的生成器的字符串
binaryDir :提供构建树相对或绝对路径的字符串;对于模式版本 < 3 是必需的;支持宏
installDir :提供安装目录相对或绝对路径的字符串;对于模式版本 < 3 是必需的且支持宏
cacheVariables :定义缓存变量的映射;其值支持宏

在定义 cacheVariables 映射时,请记住项目中变量解析的顺序。如图 16.1 所示,通过命令行定义的任何缓存变量都将覆盖预设变量。任何缓存或环境预设变量都会覆盖来自缓存文件或主机环境的变量。

让我们声明一个简单的 myConfigure 配置预设,指定生成器、构建树和安装路径: 

...
  "configurePresets": [
    {
      "name": "myConfigure",
      "displayName": "Configure Preset",
      "description": "Ninja generator",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build",
      "installDir": "${sourceDir}/build/install"
    }
  ],
...

我们对 configure 预设的介绍到此结束,接下来将进入构建阶段预设的内容。

定义构建阶段预设

您不会感到意外,构建预设位于 buildPresets 数组中。通过添加特定于构建阶段的 --list-presets 参数到命令行,可以列出这些预设。

cmake –build –list-presets

要使用选定的预设构建项目,请在 --preset 参数后指定其名称,如下所示:

cmake –build –preset myBuildingPreset

构建预设还包含一些通用字段如 name 和 description ,同时具有其特有的一组可选字段。以下是最重要字段的简要说明:

jobs :整型数值,用于设置构建项目时使用的并行任务数量
targets :字符串或字符串数组,用于设置要构建的目标并支持宏替换
configuration :字符串类型,决定多配置生成器(如 Debug 、 Release 等)的构建类型
cleanFirst : 一个布尔值,用于确保在构建前始终清理项目

就是这样。现在,我们可以像这样编写构建预设:

...
  "buildPresets": [
    {
      "name": "myBuild",
      "displayName": "Build Preset",
      "description": "Four jobs",
      "configurePreset": "myConfigure",
      "jobs": 4
    }
  ],
...

您会注意到,必填字段 configurePreset 已设置为指向我们在上一节中定义的 myConfigure 预设。现在,我们可以继续处理测试预设了。

定义测试阶段预设

测试预设位于 testPresets 数组中。通过向命令行添加特定于测试阶段的 --list-presets 参数,可以显示这些预设。

ctest –list-presets

要使用预设测试项目,请在 --preset 参数后指定其名称,如下所示:

ctest –preset myTestPreset

测试预设也有其独特的一组可选字段。以下是其中最重要字段的简要说明:

configuration :字符串类型,决定多配置生成器(如 Debug 、 Release 等)的构建类型
output : 用于配置输出的对象
filter : 用于指定要运行哪些测试的对象
execution : 用于配置测试执行的对象

每个对象都会将相应的命令行选项映射到配置值。我们将重点介绍几个关键选项,但这并非完整列表。完整参考请查阅延伸阅读部分。

output 对象的可选配置项包括:

shortProgress :布尔值;进度信息将在单行内显示
verbosity :字符串类型,用于将输出详细程度设置为以下级别之一:default(默认)、verbose(详细)或 extra(超详细)
outputOnFailure : 一个布尔值,用于在测试失败时打印程序输出
quiet : 布尔值;禁止所有输出

对于排除项,部分可接受的条目包括:

name : 一个排除名称匹配正则表达式模式的测试的字符串,支持宏
label : 一个字符串,用于排除标签匹配正则表达式模式的测试,并支持宏
fixtures : 一个用于确定要从测试中排除哪些夹具的对象(详见官方文档)

最后,执行对象接受以下可选条目:

outputLogFile : 一个指定输出日志文件路径的字符串,支持宏替换

filter 对象接受 include 和 exclude 键来配置测试用例的筛选;以下是一个部分填充的结构示例:

 "testPresets": [
    {
      "name": "myTest",
      "configurePreset": "myConfigure",
      "filter": {
        "include": {
                     ... name, label, index, useUnion ...
                   },
        "exclude": {
                     ... name, label, fixtures ...
                   }
      }
    }
  ],
...

每个键都定义了自己的选项对象:

对于  include ,条目包括:

name :一个字符串,包含名称与正则表达式模式匹配的测试,并支持宏
label :一个字符串,包含标签与正则表达式模式匹配的测试,并支持宏
index :一个对象,通过接受整数 start 、 end 、 stride 以及整数数组 specificTests 来选择要运行的测试;它支持宏
useUnion :一个布尔值,用于启用由 index 和 name 确定的测试联合使用,而非交集

对于 exclude ,条目包括:

name : 一个排除名称匹配正则表达式模式的测试的字符串,支持宏
label : 一个字符串,用于排除标签匹配正则表达式模式的测试,并支持宏
fixtures : 一个用于确定要从测试中排除哪些夹具的对象(详见官方文档)

最后, execution 对象可以在此处直接添加:

  "testPresets": [
    {
      "name": "myTest",
      "configurePreset": "myConfigure",
      "execution": {
                   ... stopOnFailure, enableFailover, ...
                   ... jobs, repeat, scheduleRandom,  ...
                   ... timeout, noTestsAction ...
                   }     
    }
  ],
...

它接受以下可选条目:

stopOnFailure : 一个布尔值,用于在任意测试失败时停止所有测试
enableFailover : 一个布尔值,用于恢复之前中断的测试
jobs : 一个整数,用于指定并行运行的测试数量
repeat : 一个对象,用于确定如何重复测试;该对象必须包含以下字段:

mode – 取值为以下之一的字符串: until-fail 、 until-pass 、 after-timeout
count – 决定重复次数的整数值

scheduleRandom :布尔值,用于启用测试执行的随机顺序
timeout :整数值,设置所有测试总执行时间的上限(以秒为单位)
noTestsAction : 一个定义未找到测试时执行操作的字符串,可选值包括 default 、 error 和 ignore

尽管有许多配置选项,但简单的预设方案同样可行:

...
  "testPresets": [
    {
      "name": "myTest",
      "displayName": "Test Preset",
      "description": "Output short progress",
      "configurePreset": "myConfigure",
      "output": {
        "shortProgress": true
      }
    }
  ],
...

与构建预设类似,我们也为新测试预设设置了必需的 configurePreset 字段,以完美衔接各项配置。接下来让我们看看最后一个阶段专属的预设类型——打包预设。

定义包阶段预设

包预设功能在模式版本 6 中引入,这意味着您至少需要 CMake 3.25 才能使用它们。这些预设应包含在 packagePresets 数组中。通过向命令行添加特定于测试阶段的 --list-presets 参数,也可以显示这些预设。

cpack –list-presets

要使用预设创建项目包,请在 --preset 参数后指定其名称,如下所示:

cpack –preset myTestPreset

包预设利用了与其他预设相同的共享字段,同时引入了一些自身特有的可选字段:

generators :一个字符串数组,用于设置要使用的包生成器( ZIP 、 7Z 、 DEB 等)
configuration :一个字符串数组,用于确定 CPack 要打包的构建类型列表( Debug 、 Release 等)
filter : 用于指定要运行哪些测试的对象
packageName 、  packageVersion 、  packageDirectory 和  vendorName :用于   指定所创建包的元数据的字符串

让我们用一个简洁的包预设来扩展我们的预设文件:

...
  "packagePresets": [
    {
      "name": "myPackage",
      "displayName": "Package Preset",
      "description": "ZIP generator",
      "configurePreset": "myConfigure",
      "generators": [
        "ZIP"
      ]
    }
  ],
...

这样的配置将有助于简化项目包的创建流程,但我们还缺少一个关键要素:项目安装。接下来让我们探讨如何实现这一功能。

添加安装预设

你可能已经注意到 CMakePresets.json 对象不支持定义 " installPresets" 。没有明确的方法通过预设来安装你的项目,这看起来很奇怪,因为配置预设提供了 installDir 字段!那么,我们是否必须退而求其次使用手动安装命令呢?

幸运的是,并非如此。有一个变通方案能让我们使用构建预设来实现目标。请看:

...
  "buildPresets": [
    {
      "name": "myBuild",
      ...
    },
    {
      "name": "myInstall",
      "displayName": "Installation",
      "targets" : "install",
      "configurePreset": "myConfigure"
    }
  ],
...

我们可以创建一个构建预设,其中 targets 字段设置为 install 。当我们正确配置安装时,项目会隐式定义 install 目标。使用此预设进行构建将执行必要步骤,将项目安装到关联配置预设中指定的 installDir 位置(如果 installDir 字段为空,则将使用默认位置):

$ cmake --build --preset myInstall
[0/1] Install the project...
-- Install configuration: ""
-- Installing: .../install/include/calc/basic.h
-- Installing: .../install/lib/libcalc_shared.so
-- Installing: .../install/lib/libcalc_static.a
-- Installing: .../install/lib/calc/cmake/CalcLibrary.cmake
-- Installing: .../install/lib/calc/cmake/CalcLibrary-noconfig.cmake
-- Installing: .../install/lib/calc/cmake/CalcConfig.cmake
-- Installing: .../install/bin/calc_console
-- Set non-toolchain portion of runtime path of ".../install/bin/calc_console" to ""

这个小技巧能帮我们节省一些处理周期。如果能给终端用户提供一个从配置到安装一步到位的命令就更好了。事实上,我们可以通过工作流预设来实现。下面让我们具体看看。

定义工作流预设

工作流预设是我们项目的终极自动化解决方案。它们允许我们按照预定顺序自动执行多个阶段特定的预设。这样一来,我们实际上只需一步就能完成端到端的构建。

要发现项目中可用的工作流,我们可以执行以下命令:

cmake –workflow –list-presets

要选择并应用预设,请使用以下命令:

cmake –workflow –preset <preset-name>

此外,使用 --fresh 标志,我们可以清除构建树并清空缓存。

定义工作流预设相当简单;我们需要定义一个名称,并可选择性地提供 displayName 和 description ,就像阶段特定预设一样。之后,我们必须列举工作流应执行的所有阶段特定预设。这通过提供一个包含带有 type 和 name 属性的对象的 steps 数组来实现,如下所示:

...
  "workflowPresets": [
    {
      "name": "myWorkflow",
      "steps": [
        {
          "type": "configure",
          "name": "myConfigure"
        },
        {
          "type": "build",
          "name": "myBuild"
        },
        {
          "type": "test",
          "name": "myTest"
        },
        {
          "type": "package",
          "name": "myPackage"
        },
        {
          "type": "build",
          "name": "myInstall"
        }
      ]
...

steps 数组中的每个对象都引用本章前面定义的预设,指明其类型( configure 、 build 、 test 或 package )及名称。这些预设通过单一命令即可完整执行从零开始构建和安装项目所需的所有步骤。

cmake –workflow –preset myWorkflow

工作流预设是自动化 C++构建、测试、打包和安装的终极解决方案。接下来,我们将探讨如何通过条件和宏来管理一些边缘情况。

添加条件和宏

当我们讨论各阶段特定预设的通用字段时,曾提到过 condition 字段。现在该回到这个话题了。condition 字段用于启用或禁用预设,当与工作流集成时,它能展现出真正的潜力。本质上,它允许我们跳过不适合特定条件的预设,并创建适合的替代预设。

条件功能需要预设模式版本 3 或更高(CMake 3.22 引入),这些条件是以 JSON 对象形式编码的简单逻辑运算,可判断操作系统、环境变量甚至所选生成器等环境因素是否符合预设场景。CMake 通过宏提供这些数据,宏本质上是一组可在预设文件中使用的只读变量。

条件对象的结构根据检查类型而异。每个条件必须包含一个 type 字段以及该类型定义的其他字段。已识别的基本类型包括:

const :此操作检查 value 字段中提供的值是否为布尔值 true
equals 、 notEquals :此操作将 lhs 字段值与 rhs 字段中的值进行比较
inList 和 notInList :这些操作检查 list 字段的数组中是否存在 string 字段提供的值
matches 和 notMatches :这些操作评估 string 字段的值是否符合 regex 字段中定义的模式

示例条件如下所示:

"condition": {
               "type": "equals",
               "lhs": "${hostSystemName}",
               "rhs": "Windows"
             }

const 条件的实际用途主要是用于禁用预设而不将其从 JSON 文件中移除。除 const 外,所有基本条件都允许在它们引入的字段中使用宏: lhs 、 rhs 、 string 、 list 和 regex 。

高级条件类型,其功能类似于“非”、“与”和“或”运算,将其他条件作为参数使用:

not :对 condition 字段中提供的条件进行布尔取反
anyOf 和 allOf :这些条件检查 conditions 数组中的任意或所有条件是否 true

例如:

"condition": {
              "type": "anyOf",
              "conditions": [
                              {
                                "type": "equals",
                                "lhs": "${hostSystemName}",
                                "rhs": "Windows"
                              },{
                                "type": "equals",
                                "lhs": "${hostSystemName}",
                                "rhs": "Linux"
                              }
                            ]
             }

此条件在系统为 Linux 或 Windows 时评估为 true 。

通过这些示例,我们介绍了第一个宏: ${hostSystemName} 。宏遵循简单的语法规则,并且仅适用于特定场景,例如:

${sourceDir} :这是源码树的路径
${sourceParentDir} :这是源码树父目录的路径
${sourceDirName} :这是项目的目录名称
${presetName} : 这是预设名称
${generator} : 这是用于创建构建系统的生成器
${hostSystemName} : 这是系统名称:在 macOS 上为 Linux 、 Windows 或 Darwin
${fileDir} : 这是包含当前预设的文件名称(当使用 include 数组导入外部预设时适用)
${dollar} : 这是转义后的美元符号( $ )
${pathListSep} : 这是环境特定的路径分隔符
$env{<variable-name>} : 如果由预设指定(区分大小写),则返回该环境变量,否则返回父环境值
$penv{<variable-name>} : 从父环境中返回该环境变量
$vendor{<macro-name>} : 这允许 IDE 厂商引入自己的宏

这些宏指令为预设及其条件提供了足够的灵活性,能够根据需要有效切换工作流程步骤。

总结

我们刚刚全面概述了 CMake 3.19 引入的预设功能,它简化了项目管理。预设允许产品作者通过配置项目构建和交付的所有阶段,为用户提供精心准备的体验。预设不仅简化了 CMake 的使用,还增强了一致性并支持环境感知设置。

我们详细讲解了 CMakePresets.json 和 CMakeUserPresets.json 文件的结构与用法,深入介绍了如何定义各类预设,包括配置预设、构建预设、测试预设、打包预设和工作流预设。每种类型都有详细说明:我们了解了通用字段、预设内部结构设计、预设间的继承关系,以及最终用户可用的具体配置选项。

配置预设部分,我们探讨了选择生成器、构建和安装目录等关键主题,以及如何通过 configurePreset 字段将预设相互关联。现在我们已经掌握如何处理构建预设 ,包括设置构建任务数量、目标及清理选项。随后,我们了解到测试预设如何通过丰富的筛选排序选项、输出格式设置以及超时和容错等执行参数来辅助测试选择。我们还学会了通过指定包生成器、筛选条件和包元数据来管理打包预设 ,甚至介绍了一种通过特殊构建预设应用来执行安装阶段的变通方案。

接着,我们发现工作流预设能够将多个阶段特定的预设组合起来。最后,我们讨论了条件和宏表达式,为项目作者提供了对单个预设行为及其在工作流中集成的更强控制力。

我们的 CMake 之旅到此结束!恭喜你——现在你已经掌握了开发、测试和打包高质量 C++软件所需的所有工具。接下来最好的方式就是运用所学知识,为用户打造出色的软件。祝你好运!

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

请登录后发表评论

    暂无评论内容