10、环境变量管理:在 Presets 中设置、覆盖和传递环境变量
环境变量这东西,说大不大,说小不小。我见过太多团队因为环境变量不一致,导致「在我机器上能编译,到你那就挂了」的尴尬局面。说白了,构建环境的不确定性,是很多莫名其妙的编译错误的根源。
CMake Presets 对环境变量的支持,其实就是为了解决这个问题。它让你能把环境变量的设置、覆盖和传递,都写进配置文件里,跟着代码一起走。这样,每个人拉下来代码,构建环境都是一致的。
10.1 环境变量的三种作用域
在 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 都能直接用,不用重复写。
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 会覆盖顶层的同名变量。
"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}) 要直观得多。
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 里了。这样两套逻辑互不干扰。
${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 甩过去——「来,用这个试试。」