条件与继承:使用 condition 和 inherits 实现预设的复用与动态选择

说实话,CMake Presets 最让我心动的功能,就是 inheritscondition 这两个机制。它们让预设不再是死板的配置快照,而是变成了一套可以灵活组合、按需激活的构建策略。我在好几个跨平台项目里,就是靠它们把原本散落在各个脚本里的构建逻辑,整整齐齐地收进了这一个 JSON 文件里。

inherits:预设的复用与叠加

先聊聊 inherits。它的作用很简单——让一个预设继承另一个预设的所有字段。你想想看,如果团队里有 5 个开发者,每个人都要配一遍编译器路径、标准版本、输出目录,那得多烦?

我个人习惯的做法是:先定义一个「基础预设」,把那些通用的、几乎不变的配置放进去。然后每个具体场景的预设,直接继承它,只覆盖需要改动的部分。

{
  "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",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug",
        "CMAKE_CXX_FLAGS": "-g -O0"
      }
    },
    {
      "name": "release",
      "inherits": "base",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Release",
        "CMAKE_CXX_FLAGS": "-O3 -DNDEBUG"
      }
    }
  ]
}

你看,debugrelease 都只写了 3 行,剩下的全从 base 继承。如果哪天我想换生成器,只需要改 base 里的 generator 字段,所有子预设自动生效。嗯,这里要注意:inherits 支持链式继承,你可以 A 继承 B,B 继承 C,但别搞出循环依赖,CMake 会直接报错。

核心规则: 子预设的字段会覆盖父预设的同名字段。如果是 cacheVariables 这类 map 类型,默认是合并而非替换。如果你想完全替换,需要显式设置 "cacheVariables": { ... } 来覆盖。

condition:让预设自己决定是否可用

接下来是 condition。这个机制解决了一个很实际的问题:同一个预设,在不同环境下可能根本跑不了。比如你的项目需要 CUDA,但某台 CI 机器上没有装,那这个预设就不该出现在可用列表里。

我曾经在给一个嵌入式项目写预设时,遇到过这样的场景:Windows 上用 MSVC,Linux 上用 GCC,macOS 上用 Clang。如果手动去切换,每次都得改 JSON。后来我用 condition 配合 inherits,一次性搞定。

{
  "configurePresets": [
    {
      "name": "windows-msvc",
      "inherits": "base",
      "condition": {
        "type": "equals",
        "lhs": "${hostSystemName}",
        "rhs": "Windows"
      },
      "cacheVariables": {
        "CMAKE_C_COMPILER": "cl.exe",
        "CMAKE_CXX_COMPILER": "cl.exe"
      }
    },
    {
      "name": "linux-gcc",
      "inherits": "base",
      "condition": {
        "type": "equals",
        "lhs": "${hostSystemName}",
        "rhs": "Linux"
      },
      "cacheVariables": {
        "CMAKE_C_COMPILER": "gcc",
        "CMAKE_CXX_COMPILER": "g++"
      }
    },
    {
      "name": "macos-clang",
      "inherits": "base",
      "condition": {
        "type": "equals",
        "lhs": "${hostSystemName}",
        "rhs": "Darwin"
      },
      "cacheVariables": {
        "CMAKE_C_COMPILER": "clang",
        "CMAKE_CXX_COMPILER": "clang++"
      }
    }
  ]
}

当你在 Windows 上执行 cmake --preset=linux-gcc 时,CMake 会先检查 condition,发现不满足,直接提示「该预设不可用」。说白了,这就是把环境判断的逻辑从 CMakeLists.txt 里搬到了预设层,更干净、更直观。

小技巧: condition 支持多种操作符:equalsnotEqualsinListnotInListmatches(正则匹配)。你还可以用 anyOfallOf 组合多个条件,实现复杂的逻辑判断。

条件与继承的组合使用

inheritscondition 放在一起用,才是真正的威力所在。你可以定义一个「抽象预设」作为模板,然后让多个具体预设继承它,每个具体预设再附加自己的条件。

我记得在做一个跨平台 SDK 项目时,团队需要同时支持 Debug 和 Release 两种模式,每种模式又需要适配三个平台。如果不用继承和条件,我得写 6 个几乎重复的预设。用了之后,代码量直接砍半。

{
  "configurePresets": [
    {
      "name": "base-debug",
      "hidden": true,
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug",
        "ENABLE_LOGGING": "ON"
      }
    },
    {
      "name": "base-release",
      "hidden": true,
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Release",
        "ENABLE_LOGGING": "OFF"
      }
    },
    {
      "name": "debug-win",
      "inherits": ["base-debug"],
      "condition": {
        "type": "equals",
        "lhs": "${hostSystemName}",
        "rhs": "Windows"
      }
    },
    {
      "name": "release-win",
      "inherits": ["base-release"],
      "condition": {
        "type": "equals",
        "lhs": "${hostSystemName}",
        "rhs": "Windows"
      }
    }
  ]
}

这里我把 base-debugbase-release 都标记为 "hidden": true,它们不会出现在 --list-presets 的输出里,只作为模板存在。用户最终看到的只有 debug-winrelease-win 等具体预设。

避坑指南: 我曾经犯过一个错误——在 condition 里用了 ${sourceDir} 这样的变量,结果发现 condition 在解析时,某些变量可能还没被赋值。建议只使用 ${hostSystemName}${fileDir} 等预定义变量,或者用 cmake -E env 传入的环境变量。自定义变量尽量别放 condition 里,容易翻车。

知识体系与核心逻辑

下面这张图,是我自己梳理的「条件与继承」的核心逻辑。你可以看到,inherits 负责纵向的配置复用,condition 负责横向的环境适配,两者交叉组合,就构成了一个灵活的预设矩阵。

条件与继承核心逻辑 基础预设 (base) hidden: true, 通用配置 inherits 条件预设 (conditional) condition + inherits condition 条件 满足? 可用 不可用 核心思想:用 inherits 复用配置,用 condition 适配环境,两者结合实现「一次编写,到处运行」 支持链式继承、多条件组合(anyOf / allOf)、正则匹配等高级用法

实际项目中的最佳实践

最后,分享几个我在真实项目中总结出来的经验:

  • 把通用配置抽成 hidden 预设:所有团队共享的编译器标志、标准版本、输出目录,都放在一个 hidden 的基础预设里。这样后续维护只需要改一处。
  • 用 condition 做平台分流:不要在一个预设里写死平台判断,而是为每个平台单独写一个预设,用 condition 控制可见性。这样每个预设职责单一,容易理解。
  • 避免过度继承:继承链不要超过 3 层,否则调试时很难追踪某个字段到底是从哪一层来的。我一般控制在 2 层:基础层 + 平台层。
  • 善用 hidden 标记:所有只作为模板、不直接使用的预设,都加上 "hidden": true。这样 --list-presets 的输出干净清爽,团队成员不会选错。
我的个人习惯: 我会在项目根目录放一个 CMakePresets.json,里面只包含最顶层的预设。然后把所有 hidden 的基础预设放到 cmake/presets/ 目录下,通过 "include": ["cmake/presets/base.json"] 引入。这样主文件保持简洁,基础配置可以按模块拆分。

嗯,关于条件与继承,核心就是这些。你想想看,如果没有这两个机制,我们可能还在用 shell 脚本或者 Python 来拼凑构建配置,既容易出错,又难以维护。现在一个 JSON 文件就能搞定,而且 CMake 自己帮你做条件判断和配置合并,省心不少。