25、预设(Presets):CMakePresets.json与CMakeUserPresets.json、配置预设、构建预设、测试预设、继承与覆盖

说实话,CMake 的预设系统是我近几年最喜欢的新特性之一。以前每次接手一个新项目,第一件事就是翻 README,看要传哪些 -D 参数。然后手动敲一长串命令,敲错一个字母就得重来。嗯,现在有了 Presets,这些烦恼基本都解决了。

预设说白了就是一套「配置模板」。你把常用的构建参数、生成器、缓存变量都写进 JSON 文件里,别人拉下代码直接 cmake --preset=xxx 就完事。我团队里有个新人,第一次用 Presets 时感叹:「原来 CMake 可以这么友好?」

Presets 文件体系

CMake 预设分两个文件:

  • CMakePresets.json:项目级预设,应该提交到版本控制。所有开发者共享。
  • CMakeUserPresets.json:用户级预设,不要提交到版本控制。适合放个人路径、本地密钥等。

两个文件结构完全一样,但 CMakeUserPresets.json 可以继承 CMakePresets.json 中的预设。我习惯把通用配置放在 CMakePresets.json 里,把本地调试用的特殊参数放在 CMakeUserPresets.json 里。

核心原则:CMakePresets.json 放「项目应该怎么构建」,CMakeUserPresets.json 放「我习惯怎么构建」。

配置预设(Configure Presets)

配置预设是最基础的一类。它定义了 cmake -S . -B build 阶段的全部参数。来看一个实际例子:

{
  "version": 6,
  "configurePresets": [
    {
      "name": "default",
      "displayName": "默认配置",
      "description": "使用 Ninja 生成器,Release 模式",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/${presetName}",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Release",
        "CMAKE_CXX_STANDARD": "20"
      }
    },
    {
      "name": "debug",
      "displayName": "Debug 模式",
      "description": "开启调试符号和 AddressSanitizer",
      "inherits": "default",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug",
        "CMAKE_CXX_FLAGS": "-fsanitize=address -g"
      }
    }
  ]
}

注意看 binaryDir 里的 ${presetName}。这是 CMake 提供的宏变量,会自动替换成当前预设的名字。我用这个技巧让不同预设的构建目录自动隔离,再也不用担心 build 目录被搞乱了。

配置预设支持的关键字段我列一下:

字段 说明 是否必填
name 预设唯一标识符
generator 构建系统生成器(Ninja、Visual Studio 等)
binaryDir 构建目录路径
cacheVariables CMake 缓存变量键值对
environment 设置环境变量
vendor 厂商特定配置(如 IDE 设置)

构建预设(Build Presets)

配置预设搞定之后,构建预设就简单多了。它对应 cmake --build 阶段的参数。说白了就是告诉 CMake:「用哪个配置,构建哪些目标,要不要并行」。

{
  "buildPresets": [
    {
      "name": "default",
      "displayName": "默认构建",
      "configurePreset": "default",
      "jobs": 8,
      "targets": ["all"]
    },
    {
      "name": "test-only",
      "displayName": "只构建测试",
      "configurePreset": "debug",
      "targets": ["unit_tests", "integration_tests"],
      "nativeToolOptions": ["--verbose"]
    }
  ]
}

这里有个关键点:configurePreset 字段把构建预设和配置预设绑定在一起。你选了一个构建预设,CMake 自动知道该用哪个配置预设的构建目录。我曾经见过有人手动传 --build dir 传错路径,折腾了半小时。用 Presets 就不会有这种问题。

小技巧jobs 字段可以设成 0,表示使用所有 CPU 核心。我笔记本是 8 核的,设成 0 后构建速度明显提升。

测试预设(Test Presets)

测试预设对应 ctest 命令。它让你定义测试的过滤条件、并行度、超时时间等。来看一个实际项目中的例子:

{
  "testPresets": [
    {
      "name": "default",
      "displayName": "全部测试",
      "configurePreset": "debug",
      "output": {
        "outputOnFailure": true
      },
      "execution": {
        "jobs": 4,
        "timeout": 120
      }
    },
    {
      "name": "quick",
      "displayName": "快速测试",
      "configurePreset": "debug",
      "filter": {
        "include": {
          "name": "unit_*"
        },
        "exclude": {
          "name": "*_slow"
        }
      },
      "execution": {
        "jobs": 8,
        "noTestsAction": "error"
      }
    }
  ]
}

我个人最常用的是 outputOnFailure 这个选项。以前跑测试,失败了还得重新跑一遍才能看输出。现在失败信息直接打印出来,省了不少时间。

注意:测试预设中的 configurePreset 必须指向一个已经配置过的预设。如果你改了配置预设的缓存变量,记得重新配置一遍,否则测试预设可能找不到构建目录。

继承与覆盖

预设系统的精髓在于继承。你可以定义一个基础预设,然后其他预设继承它,只覆盖需要改的部分。这有点像面向对象里的继承机制。

继承规则很简单:

  • 子预设会继承父预设的所有字段
  • 子预设中同名字段会覆盖父预设的值
  • 对于 cacheVariables 这类字典类型,默认是合并而不是替换

举个例子:

{
  "configurePresets": [
    {
      "name": "base",
      "generator": "Ninja",
      "cacheVariables": {
        "CMAKE_CXX_STANDARD": "17",
        "USE_OPENMP": "ON"
      }
    },
    {
      "name": "cxx20",
      "inherits": "base",
      "cacheVariables": {
        "CMAKE_CXX_STANDARD": "20",
        "ENABLE_DEBUG_LOGGING": "ON"
      }
    }
  ]
}

在这个例子里,cxx20 预设最终会有三个缓存变量:CMAKE_CXX_STANDARD=20(覆盖了 base 的 17)、USE_OPENMP=ON(从 base 继承)、ENABLE_DEBUG_LOGGING=ON(新增)。

我曾经在项目中遇到一个坑:父预设里定义了一个 CMAKE_CXX_FLAGS,子预设想追加而不是覆盖。结果发现字典类型默认是合并,但字符串类型是直接覆盖。后来我改用 CMAKE_CXX_FLAGS_INIT 才解决了这个问题。

知识体系总览

下面这张图总结了 Presets 的核心结构:

CMake Presets 体系结构 CMakePresets.json 项目级 · 提交版本控制 CMakeUserPresets.json 用户级 · 不提交版本控制 配置预设 构建预设 测试预设 继承与覆盖 cmake --preset=debug | cmake --build --preset=default | ctest --preset=quick

从图中可以看到,Presets 分三层:文件定义、预设类型、使用方式。核心是继承与覆盖机制,它让预设变得灵活且可复用。

实际项目中的最佳实践

最后分享几个我在项目中积累的经验:

  • 版本号要写对:CMake 3.20 支持 version 3,3.23 支持 version 4,3.25 支持 version 5,3.27 支持 version 6。版本太低有些字段用不了。
  • 善用 include 字段:如果预设文件太大,可以用 "include": ["common.json"] 拆分成多个文件。
  • 环境变量分离:把 PATHCCCXX 等环境变量写在 environment 字段里,不要混在 cacheVariables 中。
  • CI 和本地用同一套预设:我在 CI 脚本里直接调用 cmake --preset=ci,确保本地和 CI 环境一致,再也不用担心「在我机器上能跑」这种问题了。

我的习惯:每个项目至少定义三个配置预设——debug(开发用)、release(发布用)、ci(CI 用)。构建预设和测试预设都继承自对应的配置预设,这样整个构建流程清晰又统一。

预设系统看起来简单,但用好了能大幅提升团队的构建效率。你想想看,新人入职第一天,不用问任何人,直接 cmake --preset=debug && cmake --build --preset=default 就能开始干活,这种感觉多好。


公众号:蓝海资料掘金营,微信 deep3321