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} 主机操作系统名称 LinuxWindows
${fileDir} 当前 Presets 文件所在的目录 /home/user/project/CMakePresets
${dollar} 字面量 $ 符号(用于转义) $
小提示: 这些变量不仅可以在 binaryDir 里用,还能用在 cacheVariablesenvironment 甚至 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 文件,不用满项目翻找。

核心思路: 把「变化的部分」提取成变量,把「不变的部分」写成固定值。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 变量替换流程 Presets JSON 文件 CMake 解析 Presets,识别 ${...} 标记 查找内置变量表:sourceDir、presetName、generator... 文本替换:${sourceDir} → /home/user/project 输入 处理 输出

整个流程其实很简单:CMake 读取 Presets 文件 → 扫描所有 ${...} 标记 → 在预定义的变量表中查找对应值 → 做纯文本替换。没有函数调用,没有表达式求值,就是「找到就换,找不到就报错」。

16.7 团队协作中的变量规范

最后聊点团队协作的事。我建议在项目里定几条变量使用的规矩:

  • 优先用内置变量${sourceDir}${presetName} 这些是 CMake 保证存在的,跨平台兼容性好。
  • 避免硬编码路径:任何路径都尽量用变量拼接,不要写死 /home/xxxC:\Users\xxx
  • 变量名要有意义:如果自定义了 ${MY_SPECIAL_PATH},记得在文档里说明它的用途。
  • 测试变量展开:可以用 cmake --preset <name> --trace-expand 查看变量展开后的实际值,调试时很有用。

我记得有一次,团队里新来的同事把 ${sourceDir} 写成了 ${source_dir},结果 CMake 报错说变量未定义。排查了半天才发现是大小写问题。嗯,内置变量都是驼峰命名,这个细节容易忽略。

好了,关于宏与变量替换就聊这么多。说白了,它就是 Presets 的「动态能力」——让配置能根据上下文自动调整,而不是死板地写死。用好它,你的 Presets 文件会变得更简洁、更易维护。


公众号:蓝海资料掘金营,微信 deep3321