条件与继承:使用 condition 和 inherits 实现预设的复用与动态选择
说实话,CMake Presets 最让我心动的功能,就是 inherits 和 condition 这两个机制。它们让预设不再是死板的配置快照,而是变成了一套可以灵活组合、按需激活的构建策略。我在好几个跨平台项目里,就是靠它们把原本散落在各个脚本里的构建逻辑,整整齐齐地收进了这一个 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"
}
}
]
}
你看,debug 和 release 都只写了 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 支持多种操作符:equals、notEquals、inList、notInList、matches(正则匹配)。你还可以用 anyOf 和 allOf 组合多个条件,实现复杂的逻辑判断。
条件与继承的组合使用
把 inherits 和 condition 放在一起用,才是真正的威力所在。你可以定义一个「抽象预设」作为模板,然后让多个具体预设继承它,每个具体预设再附加自己的条件。
我记得在做一个跨平台 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-debug 和 base-release 都标记为 "hidden": true,它们不会出现在 --list-presets 的输出里,只作为模板存在。用户最终看到的只有 debug-win、release-win 等具体预设。
condition 里用了 ${sourceDir} 这样的变量,结果发现 condition 在解析时,某些变量可能还没被赋值。建议只使用 ${hostSystemName}、${fileDir} 等预定义变量,或者用 cmake -E env 传入的环境变量。自定义变量尽量别放 condition 里,容易翻车。
知识体系与核心逻辑
下面这张图,是我自己梳理的「条件与继承」的核心逻辑。你可以看到,inherits 负责纵向的配置复用,condition 负责横向的环境适配,两者交叉组合,就构成了一个灵活的预设矩阵。
实际项目中的最佳实践
最后,分享几个我在真实项目中总结出来的经验:
- 把通用配置抽成 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 自己帮你做条件判断和配置合并,省心不少。