20、预设与缓存:CMakePresets.json、CMakeUserPresets.json、预设继承、缓存初始化

说实话,CMake 的命令行参数我用了好几年。每次构建项目都要敲一堆 -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr,手指都快形成肌肉记忆了。直到有一天,我接手了一个有十几个子模块的大型项目……嗯,那感觉就像在玩拼图,少一块都不行。

后来我发现了 CMake Presets。说白了,这就是把常用的构建配置写成 JSON 文件,一键加载。今天我们就来聊聊这个“懒人福音”。

20.1 为什么需要预设?

先问你一个问题:你每次构建项目时,要敲多少个 -D 参数?

我在一个嵌入式项目中,光编译器路径就要指定三个:-DCMAKE_C_COMPILER=arm-none-eabi-gcc -DCMAKE_CXX_COMPILER=arm-none-eabi-g++ -DCMAKE_ASM_COMPILER=arm-none-eabi-as。再加上调试选项、链接脚本路径、输出目录……每次构建都要复制粘贴一大串命令。

更麻烦的是,团队里每个人用的 IDE 不一样。有人用 VS Code,有人用 CLion,还有人直接在终端里敲。每个人都要自己记住那串参数,或者写在 README 里。一旦参数变了,就得挨个通知。

CMake Presets 就是为了解决这个问题。它把构建配置标准化,放在项目里,大家共用一套。你只需要敲 cmake --preset=release,剩下的交给 JSON 文件。

20.2 两个预设文件:CMakePresets.json 与 CMakeUserPresets.json

CMake 预设分两种:

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

我个人的习惯是:把通用的构建类型(Debug、Release、RelWithDebInfo)放在 CMakePresets.json 里,把本地特有的东西(比如自定义安装路径、个人工具链)放在 CMakeUserPresets.json 里。这样既保证了团队一致性,又保留了个人灵活性。

小提示:CMakeUserPresets.json 一定要加到 .gitignore 里。我曾经见过有人把用户预设提交到仓库,结果每个人的本地路径都暴露了……嗯,那场面挺尴尬的。

20.3 预设的基本结构

一个最简单的 CMakePresets.json 长这样:

{
  "version": 6,
  "configurePresets": [
    {
      "name": "default",
      "displayName": "默认配置",
      "description": "使用 Ninja 构建器的 Debug 模式",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/default",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug",
        "CMAKE_CXX_STANDARD": "17"
      }
    }
  ]
}

这里有几个关键字段:

  • version:预设格式的版本。目前最新是 6,建议用最新的。
  • configurePresets:配置预设的数组。每个预设就是一个构建配置。
  • name:预设的名字,后面用 --preset=name 来引用。
  • generator:构建系统生成器,比如 Ninja、Unix Makefiles、Visual Studio。
  • binaryDir:构建目录。支持变量替换,比如 ${sourceDir} 表示源码根目录。
  • cacheVariables:缓存变量,相当于 -D 参数。

使用的时候,只需要:

cmake --preset=default
cmake --build --preset=default

是不是清爽多了?

20.4 预设继承:别重复造轮子

项目大了以后,预设会越来越多。Debug、Release、Test、Coverage……每个预设可能只有一两个变量不同,其他都一样。如果每个都从头写,那 JSON 文件会膨胀得很快。

CMake Presets 支持继承。你可以定义一个“基预设”,然后其他预设继承它,只覆盖需要改的部分。

举个例子:

{
  "version": 6,
  "configurePresets": [
    {
      "name": "base",
      "hidden": true,
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/${presetName}",
      "cacheVariables": {
        "CMAKE_CXX_STANDARD": "17",
        "CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
      }
    },
    {
      "name": "debug",
      "inherits": "base",
      "displayName": "Debug 模式",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug",
        "CMAKE_CXX_FLAGS": "-g -O0"
      }
    },
    {
      "name": "release",
      "inherits": "base",
      "displayName": "Release 模式",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Release",
        "CMAKE_CXX_FLAGS": "-O3 -DNDEBUG"
      }
    }
  ]
}

注意看:

  • base 预设设置了 "hidden": true,表示它不会出现在预设列表中,只能被继承。
  • debugrelease 通过 "inherits": "base" 继承了 base 的所有设置。
  • 它们只覆盖了 CMAKE_BUILD_TYPE 和编译选项,其他东西(比如生成器、构建目录结构)都从 base 来。

这样做的好处很明显:如果以后要换生成器,只需要改 base 预设,所有子预设自动生效。我在项目中就靠这个特性,把 12 个预设缩减到了 4 个基预设 + 8 个子预设,维护成本直线下降。

核心思想:预设继承的本质是“差异化管理”。把公共部分抽到基预设里,子预设只描述自己的特殊性。这样既减少了重复代码,又让配置意图更清晰。

20.5 缓存初始化:CMakePresets 与 CMake Cache 的联动

预设不仅仅是帮你省掉敲命令的功夫。它还能初始化 CMake 缓存。什么意思呢?

当你运行 cmake --preset=debug 时,CMake 会:

  1. 读取 CMakePresets.json 中名为 debug 的预设。
  2. 解析所有继承关系,合并配置。
  3. cacheVariables 中的变量写入 CMake Cache。
  4. 执行 cmake 配置过程。

这意味着,你可以在预设中定义任何缓存变量,包括:

  • 构建类型(CMAKE_BUILD_TYPE
  • 编译器路径(CMAKE_C_COMPILER
  • 安装前缀(CMAKE_INSTALL_PREFIX
  • 自定义选项(比如 MY_PROJECT_ENABLE_TESTING

而且,预设中的变量优先级高于 CMakeLists.txt 中的 set() 命令,但低于命令行中的 -D 参数。也就是说,你可以在预设中设置默认值,用户仍然可以通过命令行覆盖。

举个例子:

{
  "name": "test",
  "inherits": "base",
  "cacheVariables": {
    "CMAKE_BUILD_TYPE": "Debug",
    "MY_PROJECT_ENABLE_TESTING": "ON",
    "MY_PROJECT_COVERAGE": "ON"
  }
}

然后用户想临时关闭测试:

cmake --preset=test -DMY_PROJECT_ENABLE_TESTING=OFF

这样既保留了预设的便利性,又给了用户足够的灵活性。我个人觉得,这是 CMake Presets 最优雅的设计之一。

20.6 预设的变量替换与条件

预设支持变量替换,常用的变量有:

变量 含义
${sourceDir} 源码根目录
${sourceParentDir} 源码根目录的父目录
${presetName} 当前预设的名字
${generator} 当前预设使用的生成器
${hostSystemName} 主机系统名(Linux、Windows、Darwin)

我最常用的是 ${presetName},用它来命名构建目录:

"binaryDir": "${sourceDir}/build/${presetName}"

这样,debug 预设的构建目录就是 build/debug,release 就是 build/release,一目了然。

另外,预设还支持条件判断。比如,只在 Linux 上启用某个选项:

{
  "name": "linux-only",
  "condition": {
    "type": "equals",
    "lhs": "${hostSystemName}",
    "rhs": "Linux"
  },
  "cacheVariables": {
    "MY_LINUX_FEATURE": "ON"
  }
}

这个特性在跨平台项目中特别有用。你可以为不同平台定义不同的预设,CMake 会自动跳过不适用的。

20.7 避坑指南

我在使用 CMake Presets 的过程中,踩过不少坑。分享几个给你:

  • 版本兼容性:CMake 3.19 才开始支持预设,3.21 支持继承,3.23 支持条件。如果你的项目需要兼容旧版 CMake,记得检查版本。
  • JSON 格式严格:CMake 对 JSON 的解析很严格,多一个逗号都会报错。建议用带 JSON 校验的编辑器。
  • 隐藏预设的坑:如果你继承了一个隐藏预设,但子预设没有设置 displayName,它也会变成隐藏的。记得给子预设加上 displayName
  • 缓存变量覆盖:预设中的 cacheVariables 会覆盖 CMakeLists.txt 中的 set(),但不会覆盖 option() 的默认值。如果你用 option() 定义了变量,预设中的值可能不会生效。
特别注意:我曾经在项目中把 CMAKE_INSTALL_PREFIX 写死在预设里,结果导致 CI 和本地构建的安装路径不一致。后来我改用 ${sourceDir}/install 这样的相对路径,才解决了问题。建议预设中尽量使用相对路径或变量替换,避免硬编码绝对路径。

20.8 知识体系总览

为了让你更直观地理解 CMake Presets 的整体结构,我画了一张图:

CMake Presets 知识体系 CMakePresets.json(项目级,提交版本控制) CMakeUserPresets.json(用户级,不提交) 核心概念:预设继承、缓存初始化、变量替换、条件判断 预设继承 基预设 → 子预设,差异化管理 缓存初始化 cacheVariables 写入 CMake Cache 变量替换与条件 ${presetName}、condition 等 最终效果:cmake --preset=xxx 一键构建 减少重复输入,统一团队配置,提升构建效率

从这张图可以看出,CMake Presets 的核心就是通过两个 JSON 文件,把构建配置标准化、模块化、可继承。最终你只需要记住一个预设名字,就能完成整个构建流程。

20.9 小结

CMake Presets 是我近几年最喜欢的 CMake 特性之一。它把那些散落在命令行里的参数,整理成了结构化的 JSON 配置。团队协作时,大家用同一套预设,再也不用担心“为什么你的构建能过,我的不行”这种问题了。

如果你还没用过预设,我建议从一个小项目开始试试。先写一个简单的 CMakePresets.json,定义 Debug 和 Release 两个预设。用熟了以后,再尝试继承、条件判断这些高级特性。相信我,一旦用上,你就回不去了。

一句话总结:CMakePresets.json 是团队的“构建公约”,CMakeUserPresets.json 是你的“私人定制”。两者结合,既统一又灵活。

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