配置预设进阶: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 的核心逻辑和常见用法:

cacheVariables 核心逻辑 cacheVariables 基础用法 直接传递 -D 参数 支持布尔/字符串/路径 值必须用字符串包裹 高级特性 宏展开 ${sourceDir} 环境变量 $env{VAR} 继承与覆盖 常见陷阱 变量名大小写敏感 Windows 反斜杠转义 多父预设覆盖顺序 核心原则:用结构化配置替代手写 -D,提高可维护性和可复现性 继承 + 宏展开 + 环境变量联动,覆盖 90% 的构建场景