配置预设进阶:cacheVariables 的使用,如何通过 Presets 传递 -D 参数
上一章我们聊了基础配置预设,说白了就是给 CMake 项目定了个「身份」。但真正干活的时候,你会发现光有身份不够——你还得告诉 CMake 一堆变量值,比如开关某个功能、指定第三方库路径、设置编译选项。这些变量,在 CMake 里叫 cache variables,也就是我们平时用 -D 传进去的那些东西。
我个人习惯,能写在 Presets 里的绝不手敲。为什么?因为手敲容易错,而且换个人、换个环境就得重新查文档。Presets 里的 cacheVariables 字段,就是用来干这个的。
什么是 cacheVariables?
先看个最简单的例子。假设你的项目有个开关 USE_OPENCV,默认是 OFF。你想在 Debug 构建时打开它,方便调试图像算法。传统做法是:
cmake -B build -DUSE_OPENCV=ON -DCMAKE_BUILD_TYPE=Debug
用 Presets 怎么写?
{
"version": 6,
"configurePresets": [
{
"name": "debug-opencv",
"displayName": "Debug with OpenCV",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/debug-opencv",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"USE_OPENCV": "ON",
"OPENCV_DIR": "/opt/opencv/4.8.0"
}
}
]
}
然后你只需要:
cmake --preset debug-opencv
搞定。所有 -D 参数都藏在 Presets 里了。团队里其他人拉下代码,一条命令就能复现你的环境。我在项目中遇到过好几次新人因为手敲 -D 拼错路径导致编译失败,改用 Presets 后这类问题基本绝迹。
变量类型与引号陷阱
这里有个坑,我踩过不止一次。cacheVariables 里的值,CMake 会按字符串处理。但有些变量需要布尔值或路径,你写的时候要注意格式。
| 变量类型 | CMakeLists.txt 中定义 | Presets 中写法 | 注意 |
|---|---|---|---|
| 布尔 ON/OFF | option(USE_FOO "..." OFF) |
"USE_FOO": "ON" |
必须用字符串 "ON" 或 "OFF" |
| 字符串 | set(MY_STRING "hello") |
"MY_STRING": "hello" |
直接写,不需要额外引号 |
| 路径 | set(MY_PATH "/usr/local") |
"MY_PATH": "/usr/local" |
可以用 ${sourceDir} 等宏 |
| 列表 | set(MY_LIST "a;b;c") |
"MY_LIST": "a;b;c" |
分号分隔,CMake 内部会展开 |
嗯,这里要注意:布尔值千万别写成 true 或 1。CMake 的 option() 只认 ON/OFF,你写 true 它也能用,但语义不清晰。我曾经接手过一个项目,里面全是 TRUE、YES、1 混着写,排查问题时看得我头皮发麻。
宏展开:让变量活起来
硬编码路径显然不够灵活。Presets 支持宏展开,你可以在 cacheVariables 里引用 ${sourceDir}、${sourceParentDir}、${hostSystemName} 等内置宏。
举个例子,你的项目依赖一个子模块,路径是 ${sourceDir}/third_party/sdl2。不同机器上源码位置不同,但相对路径固定。你可以这样写:
{
"name": "with-sdl2",
"cacheVariables": {
"SDL2_DIR": "${sourceDir}/third_party/sdl2"
}
}
这样不管项目放在 /home/user/project 还是 /workspace/project,路径都能自动适配。我个人习惯把第三方库路径都用宏展开,避免绝对路径污染 Presets 文件。
你想想看,如果团队里有人把项目放在 D:/my projects/awesome-app,路径里带空格,硬编码绝对路径直接就炸了。用 ${sourceDir} 就完全没这个问题。
继承与覆盖:别重复自己
真实项目里,你通常会有多个配置预设。比如 Debug、Release、RelWithDebInfo。它们大部分变量相同,只有少数几个不同。这时候继承就派上用场了。
{
"configurePresets": [
{
"name": "base",
"hidden": true,
"cacheVariables": {
"CMAKE_CXX_STANDARD": "17",
"USE_SANITIZER": "OFF",
"ENABLE_TESTING": "ON"
}
},
{
"name": "debug",
"inherits": "base",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"USE_SANITIZER": "ON"
}
},
{
"name": "release",
"inherits": "base",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"ENABLE_TESTING": "OFF"
}
}
]
}
这里 base 预设是 hidden 的,不会出现在 cmake --list-presets 里。它只作为「模板」存在。debug 和 release 继承它,然后覆盖自己需要的变量。
覆盖规则很简单:子预设的变量会覆盖父预设的同名变量。没覆盖的变量,继承父预设的值。所以 debug 预设最终会有:
CMAKE_CXX_STANDARD=17(从 base 继承)USE_SANITIZER=ON(覆盖 base 的 OFF)ENABLE_TESTING=ON(从 base 继承)CMAKE_BUILD_TYPE=Debug(自己定义)
我在项目中维护过一个包含 8 个配置预设的项目,靠继承把重复变量减少了 70%。每次新增一个配置,只需要写几行覆盖就行,省心不少。
环境变量与 cacheVariables 的联动
有时候变量值来自环境变量,比如 CI 里动态传入的版本号。Presets 支持通过 $env{...} 语法引用环境变量。
{
"name": "ci-build",
"cacheVariables": {
"PROJECT_VERSION": "$env{CI_COMMIT_TAG}",
"BUILD_NUMBER": "$env{CI_PIPELINE_ID}"
}
}
这样你在本地开发时,如果环境变量没设置,CMake 会报错提示。我个人建议给这类变量加个默认值兜底,比如在 CMakeLists.txt 里:
if(NOT DEFINED PROJECT_VERSION)
set(PROJECT_VERSION "0.0.0-dev")
endif()
这样即使环境变量缺失,项目也能正常配置,只是版本号变成开发版。CI 里传入真实版本号后自动覆盖,两不耽误。
避坑指南:我踩过的几个雷
讲几个实战中容易翻车的地方,都是我曾经血泪教训换来的。
CMake 变量名是大小写敏感的。USE_OPENCV 和 use_opencv 是两个不同的变量。我见过有人 Presets 里写 use_opencv,CMakeLists.txt 里判断 USE_OPENCV,结果死活不生效。排查了半天才发现大小写没对上。
Windows 上路径习惯用反斜杠 \,但 CMake 里反斜杠是转义字符。如果你在 cacheVariables 里写 "FOO_DIR": "C:\Users\me\lib",CMake 会把它解析成 C:Usersmelib。正确做法是用正斜杠 / 或者双反斜杠 \\。
如果你同时使用 inherits 和 cacheVariables,子预设的变量会覆盖父预设。但如果有多个父预设(inherits: ["base1", "base2"]),后面的父预设优先级更高。这个顺序容易搞混,我建议尽量用单继承,逻辑清晰。
实战:一个完整的配置预设示例
最后,给你看一个我实际项目里用过的 Presets 配置。这个项目需要支持 Debug、Release、以及带 AddressSanitizer 的 Debug 变体。
{
"version": 6,
"configurePresets": [
{
"name": "defaults",
"hidden": true,
"cacheVariables": {
"CMAKE_CXX_STANDARD": "20",
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
"BUILD_SHARED_LIBS": "OFF"
}
},
{
"name": "debug",
"inherits": "defaults",
"binaryDir": "${sourceDir}/build/debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"ENABLE_LOGGING": "ON"
}
},
{
"name": "debug-asan",
"inherits": "debug",
"binaryDir": "${sourceDir}/build/debug-asan",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"USE_ASAN": "ON",
"ENABLE_LOGGING": "ON"
}
},
{
"name": "release",
"inherits": "defaults",
"binaryDir": "${sourceDir}/build/release",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"ENABLE_LOGGING": "OFF"
}
}
]
}
你看,debug-asan 继承了 debug,而 debug 又继承了 defaults。三层继承下来,每个预设只写自己真正不同的变量。这就是 Presets 的威力——用结构化的方式管理配置,而不是靠一堆 shell 脚本拼凑。
cacheVariables 里使用 ${presetName} 宏,动态获取当前预设的名字。比如设置 "BUILD_DIR_SUFFIX": "${presetName}",然后在 CMakeLists.txt 里用它来区分不同构建的输出目录。这样即使新增预设,也不用改 CMakeLists.txt 的逻辑。
知识结构图
下面这张图帮你理清 cacheVariables 的核心逻辑和常见用法: