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 的核心结构:
从图中可以看到,Presets 分三层:文件定义、预设类型、使用方式。核心是继承与覆盖机制,它让预设变得灵活且可复用。
实际项目中的最佳实践
最后分享几个我在项目中积累的经验:
- 版本号要写对:CMake 3.20 支持 version 3,3.23 支持 version 4,3.25 支持 version 5,3.27 支持 version 6。版本太低有些字段用不了。
- 善用 include 字段:如果预设文件太大,可以用
"include": ["common.json"]拆分成多个文件。 - 环境变量分离:把
PATH、CC、CXX等环境变量写在environment字段里,不要混在cacheVariables中。 - CI 和本地用同一套预设:我在 CI 脚本里直接调用
cmake --preset=ci,确保本地和 CI 环境一致,再也不用担心「在我机器上能跑」这种问题了。
我的习惯:每个项目至少定义三个配置预设——debug(开发用)、release(发布用)、ci(CI 用)。构建预设和测试预设都继承自对应的配置预设,这样整个构建流程清晰又统一。
预设系统看起来简单,但用好了能大幅提升团队的构建效率。你想想看,新人入职第一天,不用问任何人,直接 cmake --preset=debug && cmake --build --preset=default 就能开始干活,这种感觉多好。
公众号:蓝海资料掘金营,微信 deep3321