16、宏与变量替换:使用 ${sourceDir}、${presetName} 等内置变量
说实话,我第一次看到 CMake Presets 里的变量替换时,第一反应是:「这不就是模板引擎那套东西吗?」后来用多了才发现,它比我想象的要实用得多。说白了,Presets 文件本质上是 JSON 格式,但 CMake 给它加了一层「宏展开」的能力,让你能在配置里引用各种动态值。
我个人习惯把这种机制叫做「Presets 的胶水」——它把项目路径、预设名称、生成器这些零散信息粘合在一起,让配置变得灵活又统一。你想想看,如果没有变量替换,每个预设都得写死路径,那还谈什么「共享」和「复用」?
16.1 内置变量一览
CMake Presets 提供了一组内置变量,它们以 ${variableName} 的形式使用。我整理了一张表,方便你快速查阅:
| 变量名 | 含义 | 典型值示例 |
|---|---|---|
${sourceDir} |
项目的源码根目录(即 CMakeLists.txt 所在目录) | /home/user/project |
${sourceParentDir} |
源码根目录的父目录 | /home/user |
${presetName} |
当前使用的预设名称 | debug |
${generator} |
当前预设指定的生成器 | Ninja |
${hostSystemName} |
主机操作系统名称 | Linux、Windows |
${fileDir} |
当前 Presets 文件所在的目录 | /home/user/project/CMakePresets |
${dollar} |
字面量 $ 符号(用于转义) |
$ |
binaryDir 里用,还能用在 cacheVariables、environment 甚至 vendor 字段中。我经常用它来统一管理输出目录。
16.2 实战:用变量构建输出目录
我在项目中遇到过最典型的场景,就是团队里每个人都要手动指定构建目录。有人用 build/debug,有人用 out/Debug,还有人直接建在源码目录里……乱得不行。
用变量替换后,一个预设就能搞定:
{
"version": 6,
"configurePresets": [
{
"name": "debug",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/${presetName}",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"MY_PROJECT_VERSION": "1.0.0"
}
},
{
"name": "release",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/${presetName}",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"MY_PROJECT_VERSION": "1.0.0"
}
}
]
}
你看,binaryDir 里用了 ${sourceDir}/build/${presetName}。当执行 cmake --preset debug 时,${presetName} 会被展开为 debug,最终输出目录就是 /path/to/project/build/debug。release 预设同理,自动生成 build/release。
嗯,这里要注意:${sourceDir} 是 CMake 自动推导的,你不需要手动设置。它始终指向包含顶层 CMakeLists.txt 的目录。如果你把 Presets 文件放在子目录里,${fileDir} 会更有用。
16.3 变量在 cacheVariables 中的妙用
除了路径,变量还能用在缓存变量里。我曾经接手过一个项目,它的库版本号散落在各个 CMakeLists.txt 里,每次升级都得改好几个地方。后来我用 Presets 变量统一管理:
{
"configurePresets": [
{
"name": "dev",
"cacheVariables": {
"PROJECT_VERSION": "2.5.0",
"OUTPUT_DIR": "${sourceDir}/artifacts/${presetName}",
"ENABLE_TESTS": "ON"
}
}
]
}
这样,PROJECT_VERSION 只在 Presets 文件里定义一次,所有 CMakeLists.txt 通过 $CACHE{PROJECT_VERSION} 读取。改版本号时,只需要改 Presets 文件,不用满项目翻找。
16.4 条件变量与生成器表达式
你可能会问:「如果不同平台要用不同的变量值怎么办?」CMake Presets 支持条件变量,但方式比较特别——它通过 condition 字段来实现。不过我个人更推荐结合 generator 变量来做平台适配:
{
"configurePresets": [
{
"name": "windows-msvc",
"generator": "Visual Studio 17 2022",
"binaryDir": "${sourceDir}/build/${presetName}",
"cacheVariables": {
"PLATFORM_TOOLSET": "v143",
"WINDOWS_TARGET_PLATFORM": "10.0"
}
},
{
"name": "linux-gcc",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/${presetName}",
"cacheVariables": {
"CMAKE_C_COMPILER": "gcc-12",
"CMAKE_CXX_COMPILER": "g++-12"
}
}
]
}
你看,虽然两个预设都用了 ${presetName},但它们的 cacheVariables 完全不同。这就是变量替换的灵活之处——它只负责展开,不关心内容是什么。具体的逻辑由你通过预设结构来组织。
16.5 避坑指南:变量展开的陷阱
我曾经踩过一个坑:在 environment 块里用 ${sourceDir},结果发现环境变量没展开。后来查文档才知道,environment 里的变量替换只支持 ${presetName} 和 ${dollar},不支持 ${sourceDir}。嗯,这是个容易忽略的点。
另一个常见问题是路径中的反斜杠。在 Windows 上,${sourceDir} 展开后是 C:\Users\...\project 这种格式。如果你把它传给某些 Unix 风格的工具,可能会出问题。我的建议是:在 Windows 上尽量用正斜杠,或者用 file(TO_CMAKE_PATH) 做转换。
${} 里写复杂的逻辑,比如 ${sourceDir}/../other 这种写法虽然能工作,但可读性很差。建议用 ${sourceParentDir} 代替。
16.6 知识体系:变量替换的核心逻辑
为了让你更直观地理解变量替换的运作方式,我画了一张流程图:
整个流程其实很简单:CMake 读取 Presets 文件 → 扫描所有 ${...} 标记 → 在预定义的变量表中查找对应值 → 做纯文本替换。没有函数调用,没有表达式求值,就是「找到就换,找不到就报错」。
16.7 团队协作中的变量规范
最后聊点团队协作的事。我建议在项目里定几条变量使用的规矩:
- 优先用内置变量:
${sourceDir}、${presetName}这些是 CMake 保证存在的,跨平台兼容性好。 - 避免硬编码路径:任何路径都尽量用变量拼接,不要写死
/home/xxx或C:\Users\xxx。 - 变量名要有意义:如果自定义了
${MY_SPECIAL_PATH},记得在文档里说明它的用途。 - 测试变量展开:可以用
cmake --preset <name> --trace-expand查看变量展开后的实际值,调试时很有用。
我记得有一次,团队里新来的同事把 ${sourceDir} 写成了 ${source_dir},结果 CMake 报错说变量未定义。排查了半天才发现是大小写问题。嗯,内置变量都是驼峰命名,这个细节容易忽略。
好了,关于宏与变量替换就聊这么多。说白了,它就是 Presets 的「动态能力」——让配置能根据上下文自动调整,而不是死板地写死。用好它,你的 Presets 文件会变得更简洁、更易维护。
公众号:蓝海资料掘金营,微信 deep3321