22、用户级预设:CMakeUserPresets.json 的本地覆盖与个人偏好
说实话,CMakePresets.json 是个好东西。团队统一了构建配置,新人来了不用问东问西。但有个问题——每个人的开发环境不一样啊。
我习惯用 Ninja 构建,同事喜欢用 Visual Studio。我本地装了 CUDA,他机器上没有。我调试时喜欢开 AddressSanitizer,他嫌慢从来不开。
这些个人偏好,总不能写到公共的 CMakePresets.json 里吧?
嗯,CMake 团队早就想到了这一点。解决方案就是 CMakeUserPresets.json。
用户级预设的本质
说白了,CMakeUserPresets.json 就是给你自己用的。它和 CMakePresets.json 长得一模一样,结构完全兼容。唯一的区别是:
- CMakePresets.json — 提交到 Git,团队共享,所有人可见
- CMakeUserPresets.json — 不提交到 Git,只属于你,本地覆盖
我建议你立刻把 CMakeUserPresets.json 加到 .gitignore 里。我曾经见过一个团队,有人不小心把带密码的预设提交上去了……嗯,那场面挺尴尬的。
覆盖规则:谁说了算?
当两个文件同时存在时,CMake 会合并它们。规则其实很简单:
- 先加载 CMakePresets.json 中的预设
- 再加载 CMakeUserPresets.json 中的预设
- 同名预设,用户级覆盖项目级
- 不同名预设,两者共存
你想想看,这就像 Git 的配置系统。系统级、全局级、仓库级,层层覆盖。CMake 预设也是这个思路。
核心原则:CMakeUserPresets.json 中的同名预设会完全替换 CMakePresets.json 中的对应预设,而不是合并字段。
举个例子。项目里定义了一个 "debug" 预设:
// CMakePresets.json
{
"version": 6,
"configurePresets": [
{
"name": "debug",
"displayName": "Debug",
"generator": "Visual Studio 17 2022",
"binaryDir": "${sourceDir}/build/debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_CXX_FLAGS": "-Wall -Wextra"
}
}
]
}
但你在 Linux 上工作,没有 Visual Studio。你可以在 CMakeUserPresets.json 里覆盖它:
// CMakeUserPresets.json
{
"version": 6,
"configurePresets": [
{
"name": "debug",
"displayName": "Debug (My Linux)",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_CXX_FLAGS": "-Wall -Wextra -g -O0"
}
}
]
}
这样,你执行 cmake --preset debug 时,用的是你的 Ninja 配置。而你的同事用同样的命令,还是 Visual Studio。互不干扰。
用户级预设的典型场景
我在项目中遇到过不少需要用户级预设的情况。这里列几个最常见的:
| 场景 | 项目级预设 | 用户级覆盖 |
|---|---|---|
| 生成器不同 | Visual Studio | Ninja / Unix Makefiles |
| 安装路径 | /usr/local | ~/local 或自定义路径 |
| 工具链 | 系统默认 GCC | 自定义 Clang 或交叉编译链 |
| 调试选项 | 无 | AddressSanitizer / UBSan |
| 并行度 | 无 | CMAKE_BUILD_PARALLEL_LEVEL=16 |
我的小技巧:我习惯在 CMakeUserPresets.json 里加一个 "my-debug" 预设,专门开 AddressSanitizer。这样既不影响团队配置,又能快速定位内存问题。
继承与组合:别重复造轮子
你可能会想:「难道我要把整个预设重新写一遍?就为了改一个生成器?」
不用。CMakeUserPresets.json 同样支持 inherits 机制。你可以继承项目级预设,只覆盖你想改的部分。
// CMakeUserPresets.json
{
"version": 6,
"configurePresets": [
{
"name": "my-debug",
"inherits": "debug",
"generator": "Ninja",
"cacheVariables": {
"CMAKE_CXX_FLAGS": "-Wall -Wextra -fsanitize=address"
}
}
]
}
这里 "my-debug" 继承了项目级的 "debug" 预设,只改了生成器和编译选项。其他配置,比如 binaryDir、CMAKE_BUILD_TYPE,都从父预设继承过来。
说白了,这就是面向对象编程里的「继承 + 重写」。你想想看,是不是很熟悉?
构建预设和测试预设的覆盖
不只是 configurePresets,buildPresets 和 testPresets 同样可以被覆盖。
比如项目里定义了一个构建预设:
// CMakePresets.json
{
"version": 6,
"buildPresets": [
{
"name": "debug",
"configurePreset": "debug",
"jobs": 4
}
]
}
你的机器有 32 核,4 个并行任务太浪费了。在 CMakeUserPresets.json 里覆盖:
// CMakeUserPresets.json
{
"version": 6,
"buildPresets": [
{
"name": "debug",
"configurePreset": "debug",
"jobs": 16
}
]
}
这样你构建时就能跑满 CPU,同事那边还是 4 个任务。各取所需。
SVG:用户级预设的覆盖逻辑
避坑指南
我曾经踩过的坑:
- 版本号必须一致 — CMakeUserPresets.json 的 version 字段必须和 CMakePresets.json 一致,否则 CMake 会报错。我见过有人升级了项目级预设的版本,忘了改用户级,结果构建失败。
- 不要复制整个文件 — 只写你需要覆盖的预设。复制整个 CMakePresets.json 到用户级,然后改一两行,这是最糟糕的做法。项目级更新了,你的用户级还是旧的。
- 小心 inherit 链 — 如果用户级预设继承了项目级预设,而项目级预设又继承了另一个预设,这个链条可能会很长。调试时记得用
cmake --preset my-preset --debug-preset查看最终展开结果。
最佳实践总结
用了几年的 CMake 预设,我总结了几条经验:
- 项目级放通用配置 — 生成器、编译选项、安装路径的默认值
- 用户级放个人偏好 — 调试工具、并行度、自定义工具链
- 用户级尽量用 inherits — 减少重复,方便跟随项目级更新
- 定期清理用户级预设 — 项目结构变了,旧的预设可能失效
嗯,其实说白了,CMakeUserPresets.json 就是给你一个「私人定制」的空间。团队规范不能丢,但个人效率也要保证。两者结合,才是好的构建体验。
一个小建议:如果你刚接触 CMake 预设,先别急着写用户级。把项目级预设跑通了,再慢慢加自己的覆盖。一步到位容易出问题。
公众号:蓝海资料掘金营,微信deep3321