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 里。这样既保证了团队一致性,又保留了个人灵活性。
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,表示它不会出现在预设列表中,只能被继承。 - debug 和 release 通过
"inherits": "base"继承了 base 的所有设置。 - 它们只覆盖了
CMAKE_BUILD_TYPE和编译选项,其他东西(比如生成器、构建目录结构)都从 base 来。
这样做的好处很明显:如果以后要换生成器,只需要改 base 预设,所有子预设自动生效。我在项目中就靠这个特性,把 12 个预设缩减到了 4 个基预设 + 8 个子预设,维护成本直线下降。
20.5 缓存初始化:CMakePresets 与 CMake Cache 的联动
预设不仅仅是帮你省掉敲命令的功夫。它还能初始化 CMake 缓存。什么意思呢?
当你运行 cmake --preset=debug 时,CMake 会:
- 读取
CMakePresets.json中名为debug的预设。 - 解析所有继承关系,合并配置。
- 将
cacheVariables中的变量写入 CMake Cache。 - 执行
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 的核心就是通过两个 JSON 文件,把构建配置标准化、模块化、可继承。最终你只需要记住一个预设名字,就能完成整个构建流程。
20.9 小结
CMake Presets 是我近几年最喜欢的 CMake 特性之一。它把那些散落在命令行里的参数,整理成了结构化的 JSON 配置。团队协作时,大家用同一套预设,再也不用担心“为什么你的构建能过,我的不行”这种问题了。
如果你还没用过预设,我建议从一个小项目开始试试。先写一个简单的 CMakePresets.json,定义 Debug 和 Release 两个预设。用熟了以后,再尝试继承、条件判断这些高级特性。相信我,一旦用上,你就回不去了。
公众号:蓝海资料掘金营,微信deep3321