10、环境变量管理:在 Presets 中设置、覆盖和传递环境变量

环境变量这东西,说大不大,说小不小。我见过太多团队因为环境变量不一致,导致「在我机器上能编译,到你那就挂了」的尴尬局面。说白了,构建环境的不确定性,是很多莫名其妙的编译错误的根源。

CMake Presets 对环境变量的支持,其实就是为了解决这个问题。它让你能把环境变量的设置、覆盖和传递,都写进配置文件里,跟着代码一起走。这样,每个人拉下来代码,构建环境都是一致的。

10.1 环境变量的三种作用域

在 Presets 里,环境变量可以在三个地方设置,它们的优先级和生效范围不一样。我刚开始用的时候也搞混过,后来总结了一张图,你一看就明白。

环境变量作用域与优先级 最高优先级:configurePresets / buildPresets / testPresets 中的 environment 仅对当前 Preset 生效,覆盖下方所有层级 中间优先级:CMakePresets.json 顶层 "environment" 字段 对所有 Preset 生效,但可被上层覆盖 最低优先级:系统环境变量(Shell 中 export 的变量) 作为默认值,可被 Presets 覆盖 箭头方向:高优先级覆盖低优先级 实际生效值 = 最高优先级中存在的变量

嗯,这张图其实已经说得很清楚了。从上往下,优先级递减。你在 Preset 里设置的变量,会覆盖系统环境变量。但要注意,它不会删除系统里已有的变量——除非你显式把它设为空字符串。

10.2 在顶层设置全局环境变量

先看一个最简单的例子。在 CMakePresets.json 的顶层加一个 "environment" 字段,所有 Preset 都能继承这些变量。

{
  "version": 6,
  "environment": {
    "CMAKE_BUILD_TYPE": "Release",
    "MY_PROJECT_ROOT": "${sourceDir}",
    "CACHE_DIR": "${sourceDir}/.cache"
  },
  "configurePresets": [
    {
      "name": "default",
      "displayName": "默认配置",
      "binaryDir": "${sourceDir}/build"
    }
  ]
}

这里有个小技巧:${sourceDir} 这种变量引用,在环境变量里也是可以用的。我个人习惯把项目根目录、缓存目录这类公共路径,都放在顶层 environment 里。这样后面每个 Preset 都能直接用,不用重复写。

💡 我的经验:顶层 environment 适合放「所有配置都通用」的变量。比如编译器的搜索路径、公共库的版本号。我曾经在一个项目里,把 Python 解释器的路径放在这里,结果团队里有人用 pyenv、有人用系统 Python,统一之后再也没有「找不到 Python」的报错了。

10.3 在 Preset 内部覆盖环境变量

有时候,不同 Preset 需要不同的环境变量。比如 Debug 和 Release 版本,可能要用不同的缓存目录或者不同的工具链路径。这时候就在 Preset 内部设置 "environment"

{
  "configurePresets": [
    {
      "name": "debug",
      "displayName": "Debug 模式",
      "environment": {
        "CMAKE_BUILD_TYPE": "Debug",
        "CACHE_DIR": "${sourceDir}/.cache/debug",
        "ENABLE_SANITIZERS": "ON"
      }
    },
    {
      "name": "release",
      "displayName": "Release 模式",
      "environment": {
        "CMAKE_BUILD_TYPE": "Release",
        "CACHE_DIR": "${sourceDir}/.cache/release",
        "ENABLE_SANITIZERS": "OFF"
      }
    }
  ]
}

你看,CMAKE_BUILD_TYPE 在顶层设成了 "Release",但在 debug Preset 里被覆盖成了 "Debug"。这就是优先级的作用——Preset 内部的 environment 会覆盖顶层的同名变量。

⚠️ 注意:覆盖不是合并。如果你在 Preset 内部设置了 "environment",它不会继承顶层 environment 里没被覆盖的变量吗?会的。CMake 的行为是:Preset 内部的 environment 会与顶层 environment 合并,同名变量以 Preset 内部为准。这一点跟 JavaScript 的 Object.assign 有点像。

10.4 继承与传递:让 Preset 复用环境变量

Preset 支持 "inherits" 关键字,这玩意儿在环境变量管理上特别有用。你可以定义一个「基础 Preset」,把公共的环境变量放进去,然后其他 Preset 继承它,再按需覆盖。

{
  "configurePresets": [
    {
      "name": "base",
      "displayName": "基础配置",
      "hidden": true,
      "environment": {
        "TOOLCHAIN_PATH": "/opt/toolchains/arm",
        "CMAKE_PREFIX_PATH": "/opt/libs",
        "BUILD_NUMBER": "0"
      }
    },
    {
      "name": "arm-debug",
      "displayName": "ARM Debug",
      "inherits": "base",
      "environment": {
        "CMAKE_BUILD_TYPE": "Debug",
        "BUILD_NUMBER": "${env.BUILD_NUMBER}"  // 从系统环境变量读取
      }
    },
    {
      "name": "arm-release",
      "displayName": "ARM Release",
      "inherits": "base",
      "environment": {
        "CMAKE_BUILD_TYPE": "Release",
        "BUILD_NUMBER": "${env.BUILD_NUMBER}"
      }
    }
  ]
}

这里有个细节:"${env.BUILD_NUMBER}" 这种写法,是从系统环境变量里读取值。也就是说,你可以在 Preset 里引用 Shell 中 export 的变量。这在实际 CI/CD 流水线里特别常用——比如把 Git 的 commit 号或者构建流水线的 ID 传进来。

为什么会用到 "hidden": true?说白了,base 这个 Preset 只是用来被继承的,不应该出现在 cmake --list-presets 的列表里。加个 hidden 就藏起来了,干净。

10.5 环境变量的传递:从 Preset 到 CMake 变量

环境变量设置好了,怎么让 CMake 的脚本里能读到?有两种方式。

方式一:直接通过 $ENV{} 读取

在 CMakeLists.txt 里,你可以直接用 $ENV{MY_VAR} 来读取环境变量。比如:

# CMakeLists.txt
message(STATUS "构建类型: $ENV{CMAKE_BUILD_TYPE}")
message(STATUS "缓存目录: $ENV{CACHE_DIR}")

方式二:通过 cmake -D 传递

你可以在 Preset 的 "cacheVariables" 里引用环境变量,把它转成 CMake 的缓存变量。这样在 CMake 脚本里就能用 ${MY_VAR} 直接访问了。

{
  "configurePresets": [
    {
      "name": "default",
      "environment": {
        "MY_SECRET_KEY": "abc123"
      },
      "cacheVariables": {
        "MY_SECRET_KEY": "${env.MY_SECRET_KEY}"
      }
    }
  ]
}

我个人更推荐第二种方式。为什么呢?因为 CMake 的缓存变量可以在 CMakeCache.txt 里看到,调试的时候一目了然。而且你可以在 CMake 脚本里用 if(MY_SECRET_KEY) 来做条件判断,比 if(DEFINED ENV{MY_SECRET_KEY}) 要直观得多。

🔑 关键点:环境变量是「一次性」的——它只在 configure 阶段生效。如果你在 build 阶段需要用到同样的变量,记得在 buildPresets 里也设置一遍。或者,更好的做法是把它转成 CMake 缓存变量,这样整个构建过程都能访问。

10.6 实战:一个完整的 CI/CD 环境变量方案

最后,我分享一个我在实际项目中用过的方案。这个方案结合了顶层 environment、Preset 内部覆盖、以及系统环境变量的传递,适合在本地开发和 CI 流水线之间切换。

{
  "version": 6,
  "environment": {
    "BUILD_TYPE": "Release",
    "INSTALL_PREFIX": "/usr/local"
  },
  "configurePresets": [
    {
      "name": "ci-linux",
      "displayName": "CI Linux 构建",
      "environment": {
        "BUILD_TYPE": "${env.CI_BUILD_TYPE}",  // 从 CI 系统传入
        "INSTALL_PREFIX": "${sourceDir}/install",
        "CC": "/usr/bin/gcc-11",
        "CXX": "/usr/bin/g++-11"
      },
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "${env.BUILD_TYPE}",
        "CMAKE_INSTALL_PREFIX": "${env.INSTALL_PREFIX}"
      }
    },
    {
      "name": "local-debug",
      "displayName": "本地调试",
      "environment": {
        "BUILD_TYPE": "Debug",
        "INSTALL_PREFIX": "${sourceDir}/install-debug"
      },
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "${env.BUILD_TYPE}",
        "CMAKE_INSTALL_PREFIX": "${env.INSTALL_PREFIX}"
      }
    }
  ]
}

你看,ci-linux Preset 里的 BUILD_TYPE 是从系统环境变量 CI_BUILD_TYPE 读的。如果 CI 系统没传这个变量,它就变成空字符串了。所以我在 CI 脚本里会先 export 一下:

export CI_BUILD_TYPE=Release
cmake --preset ci-linux

而本地开发时,直接用 cmake --preset local-debug,环境变量就硬编码在 Preset 里了。这样两套逻辑互不干扰。

💡 避坑指南:我曾经在 CI 里忘记 export 某个环境变量,结果 Preset 里引用了 ${env.SOME_VAR},它变成了空字符串,导致 CMake 配置时用了错误的路径。后来我养成了一个习惯:在 Preset 里给关键环境变量加个默认值检查。比如在 CMakeLists.txt 里写:if(NOT DEFINED ENV{CI_BUILD_TYPE}) set(ENV{CI_BUILD_TYPE} "Release") endif()。这样即使 CI 忘了传,也不会崩得太难看。

环境变量管理,说白了就是「谁覆盖谁」的问题。Presets 给了你一套清晰的优先级规则,你只要按照这个规则来设计,就能让团队所有人的构建环境保持一致。嗯,下次再有人跟你说「我机器上能编译」,你可以直接把 CMakePresets.json 甩过去——「来,用这个试试。」


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