24、预设文件:CMakePresets.json。如何统一团队的构建配置,简化命令行操作。

说实话,CMake 的命令行参数我记了好几年都没记全。什么 -DCMAKE_BUILD_TYPE=Release-G Ninja-DCMAKE_INSTALL_PREFIX=...,每次新开一个项目都要翻历史记录。更头疼的是,团队里五个人,五个人的构建方式都不一样——有人用 Debug,有人用 Release,有人忘了开测试开关。

后来 CMake 3.19 引入了 CMakePresets.json,我一看就拍大腿:这不就是我一直想要的东西吗?说白了,它就是一份构建配置的「模板文件」,把所有的 -D 参数、生成器、构建类型全部写进 JSON 里。团队成员只要 cmake --preset debug 就完事了,不用再背那一长串参数。

Presets 文件长什么样?

先看一个最简单的例子。我在项目根目录下放一个 CMakePresets.json

{
  "version": 3,
  "configurePresets": [
    {
      "name": "debug",
      "displayName": "Debug 构建",
      "description": "带调试信息的构建,开启测试",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/debug",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug",
        "BUILD_TESTING": "ON"
      }
    },
    {
      "name": "release",
      "displayName": "Release 构建",
      "description": "优化后的发布版本",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/release",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Release",
        "BUILD_TESTING": "OFF"
      }
    }
  ]
}

有了这个文件,团队成员只需要执行:

cmake --preset debug
cmake --build --preset debug

嗯,就这么简单。不用再纠结 -DCMAKE_BUILD_TYPE 拼没拼对,也不用担心有人忘了加 -G Ninja 导致生成器不一致。

为什么我强烈推荐用 Presets?

我在一个十几人的 C++ 团队里待过,那时候没有 Presets,每个人都在自己的 build.sh 里写一堆参数。结果呢?有人用 Visual Studio 生成器,有人用 Ninja,有人把安装路径写死了 /usr/local,CI 上又用另一套参数。每次排查构建问题,都要先问一句「你是怎么配置的?」

Presets 解决了三个核心痛点:

  • 统一性:一份 JSON 文件,所有人都用同一套配置,不会出现「我本地能编译,你那里不行」的尴尬。
  • 可读性displayNamedescription 字段让每个预设的用途一目了然。新同事看一眼就知道该用哪个。
  • 可继承:你可以定义一个基础预设,然后其他预设继承它,只覆盖需要改的字段。后面我会讲这个。

核心思想:Presets 把「构建配置」从人的脑子里、从 shell 脚本里,搬到了一个结构化的 JSON 文件中。机器可读,人也看得懂。

Presets 的完整结构

Presets 文件支持四种预设类型,不过日常最常用的是前两种:

预设类型 作用 对应命令
configurePresets 配置阶段的参数(生成器、缓存变量、二进制目录) cmake --preset <name>
buildPresets 构建阶段的参数(并行数、目标、配置类型) cmake --build --preset <name>
testPresets 测试阶段的参数(CTest 选项、过滤条件) ctest --preset <name>
packagePresets 打包阶段的参数(CPack 选项) cpack --preset <name>

我个人习惯把 configurePresetsbuildPresets 配对使用。比如上面那个 debug 预设,我通常会再配一个对应的 build 预设:

{
  "version": 3,
  "configurePresets": [
    {
      "name": "debug",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/debug",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug",
        "BUILD_TESTING": "ON"
      }
    }
  ],
  "buildPresets": [
    {
      "name": "debug",
      "configurePreset": "debug",
      "jobs": 8
    }
  ]
}

这样 cmake --build --preset debug 就会自动使用 debug 配置阶段的参数,并且并行 8 个任务。你想想看,是不是比手动敲 -j8 省心多了?

继承与条件:让 Presets 更灵活

团队里不同平台、不同编译器怎么办?Presets 支持 inheritscondition 字段。我举个例子:

{
  "version": 3,
  "configurePresets": [
    {
      "name": "base",
      "hidden": true,
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/${presetName}",
      "cacheVariables": {
        "CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
      }
    },
    {
      "name": "linux-debug",
      "inherits": "base",
      "displayName": "Linux Debug",
      "condition": {
        "type": "equals",
        "lhs": "${hostSystemName}",
        "rhs": "Linux"
      },
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug"
      }
    },
    {
      "name": "windows-debug",
      "inherits": "base",
      "displayName": "Windows Debug",
      "condition": {
        "type": "equals",
        "lhs": "${hostSystemName}",
        "rhs": "Windows"
      },
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug"
      }
    }
  ]
}

这里我定义了一个 base 预设,设置了 "hidden": true,意思是它不会出现在 --list-presets 的列表里,只作为基类被继承。然后 linux-debugwindows-debug 分别继承它,并添加了平台条件。

为什么要这么做?因为不同平台的生成器可能不一样。Windows 上你可能想用 Visual Studio,Linux 上用 Ninja。通过 condition,Presets 会自动匹配当前平台,用户只需要执行 cmake --preset debug,CMake 会自动选择正确的预设。

小技巧${sourceDir}${sourceParentDir}${presetName} 这些变量可以在 Presets 中使用。我经常用 ${sourceDir}/build/${presetName} 来组织构建目录,这样每个预设都有自己的独立目录,互不干扰。

避坑指南:我曾经踩过的坑

Presets 虽然好用,但有几个地方容易翻车:

  • 版本号别写错"version": 3 是目前最常用的版本。如果你写了 version: 2,有些新特性(比如 condition)就用不了。我曾经因为这个排查了半天,最后发现是版本号写低了。
  • 路径用正斜杠:即使在 Windows 上,binaryDir 里的路径也要用 /,不要用 \。CMake 内部会帮你转换。
  • hidden 预设别忘了加 displayName:虽然 hidden 预设不会显示在列表中,但如果你在继承时没覆盖 displayName,用户执行 --list-presets 时可能会看到奇怪的空白名字。
  • 条件表达式别嵌套太深condition 支持 andornot 组合,但嵌套超过三层就很难读了。我建议最多两层,复杂条件拆成多个预设。

用 SVG 理清 Presets 的核心逻辑

下面这张图展示了 Presets 的工作流程,从 JSON 文件到最终构建:

CMakePresets.json 工作流程 CMakePresets.json configurePresets buildPresets testPresets 继承 (inherits) 条件 (condition) 变量展开 ${sourceDir} 等 隐藏预设 (hidden: true) cmake --preset <name> 一行搞定

从这张图可以看得很清楚:Presets 文件定义了多种预设类型,每种类型内部支持继承、条件、变量展开等特性,最终用户只需要一个 --preset 参数就能完成配置或构建。

团队协作中的最佳实践

我在实际项目中总结了几条经验,分享给你:

  1. 把 CMakePresets.json 纳入版本控制。这是团队的「构建契约」,所有人都应该使用同一份。如果有平台差异,用 condition 处理,而不是每个人改自己的 JSON。
  2. 提供一份 CMakeUserPresets.json。这个文件不会被版本控制(建议加到 .gitignore 里),允许开发者覆盖某些参数。比如有人想用 Unix Makefiles 而不是 Ninja,可以在用户预设里改。
  3. 命名要有规律。我习惯用 <平台>-<配置> 的格式,比如 linux-debugwindows-release。不要用 mybuildtest1 这种让人摸不着头脑的名字。
  4. CI 也用它。在 GitHub Actions 或 GitLab CI 里,直接 cmake --preset ci-linux,这样本地和 CI 的构建配置完全一致,再也不会出现「CI 上编译不过,本地却好好的」这种问题了。

注意:CMakePresets.json 的 version 字段决定了支持的语法。如果你用了 version: 6 的新特性,但团队里有人用的 CMake 版本太低,就会报错。我建议统一 CMake 版本,或者至少保证最低版本 3.23 以上,这样大部分特性都能用。

好了,关于 Presets 的核心内容就这些。说白了,它就是 CMake 官方给的一个「配置标准化」方案。你想想看,以前我们要写一堆 shell 脚本、Makefile 封装、甚至 Python 脚本来管理构建参数,现在一个 JSON 文件全搞定。而且它是 CMake 原生支持的,不需要任何外部工具。

我个人觉得,Presets 是 CMake 3.19 之后最值得升级的特性之一。如果你团队还在用老版本的 CMake,我建议尽快升级。省下来的时间,足够你喝好几杯咖啡了。