变量与缓存:普通变量、缓存变量、环境变量

说实话,CMake 的变量系统,是很多初学者第一个栽跟头的地方。我当年刚接触 CMake 时,就被普通变量和缓存变量的关系搞晕过——明明设了个变量,换个目录就找不到了?嗯,今天咱们就把这块彻底捋清楚。

1. 普通变量:最基础的存在

普通变量,说白了就是「临时变量」。它在当前 CMakeLists.txt 的作用域内有效,一旦出了这个作用域,就没了。

# 设置普通变量
set(MY_VAR "hello")
set(MY_LIST a b c d)  # 列表也是变量

# 使用变量
message("MY_VAR = ${MY_VAR}")
message("MY_LIST = ${MY_LIST}")

我在项目中遇到过一个问题:一个子目录的 CMakeLists.txt 里设了个变量,父目录死活读不到。当时我还以为是 CMake 的 bug,后来才明白——这就是作用域在作怪。

核心要点:普通变量遵循「就近原则」。子目录的 set() 不会影响父目录,除非你用 PARENT_SCOPE。

# 子目录中想影响父目录
set(MY_VAR "value" PARENT_SCOPE)

2. 缓存变量:跨目录的「持久化」方案

缓存变量就不一样了。它会被写入 CMakeCache.txt,整个构建树都能访问。我习惯把缓存变量理解为「全局配置项」。

# 设置缓存变量
set(MY_CACHE_VAR "cache_value" CACHE STRING "这是缓存变量说明")

# 强制覆盖已有缓存
set(MY_CACHE_VAR "new_value" CACHE STRING "说明" FORCE)

你想想看,为什么要有缓存变量?因为有些配置你不想每次 cmake 都重新设一遍。比如安装路径、编译选项这些,设一次就够了。

注意:缓存变量一旦写入 CMakeCache.txt,下次 cmake 时不会自动覆盖。除非你加了 FORCE 关键字,或者手动删了缓存文件。

3. option 命令:开关变量的优雅写法

option 其实就是缓存变量的语法糖。专门用来处理 ON/OFF 这种开关选项。

option(USE_OPENCV "是否启用 OpenCV" ON)
option(ENABLE_TEST "启用单元测试" OFF)

if(USE_OPENCV)
    find_package(OpenCV REQUIRED)
endif()

我曾经在一个大型项目中,用 option 管理了十几个编译开关。说实话,比手动 set 缓存变量清爽多了。每个 option 都会自动出现在 cmake-gui 里,用户勾选就行。

小技巧:option 的默认值只在第一次 cmake 时生效。如果你改了默认值,记得删掉 CMakeCache.txt 中对应的条目,或者用 -D 参数强制覆盖。

4. 环境变量:和系统打交道

环境变量是 CMake 和操作系统之间的桥梁。你可以读取系统环境变量,也可以设置新的环境变量(但只影响 CMake 自身)。

# 读取环境变量
message("PATH = $ENV{PATH}")
message("HOME = $ENV{HOME}")

# 设置环境变量(仅 CMake 内部有效)
set(ENV{MY_ENV_VAR} "test_value")
message("MY_ENV_VAR = $ENV{MY_ENV_VAR}")

嗯,这里要注意:set(ENV{...}) 设置的环境变量,不会影响父进程(你的 shell)。它只在 CMake 运行期间有效。我刚开始时以为能永久改环境变量,结果折腾了半天。

5. 变量作用域:理解「层级」关系

作用域是 CMake 变量系统最核心的概念。我画了张图,帮你理清关系:

CMake 变量作用域层级 环境变量层(全局,系统级) $ENV{PATH}、$ENV{HOME} 等,所有进程共享 缓存变量层(全局,构建树级) 存储在 CMakeCache.txt,所有目录可见 普通变量层(父目录作用域) 父 CMakeLists.txt 中 set() 的变量 普通变量层(子目录作用域) 子 CMakeLists.txt 中 set() 的变量

从这张图能看出来:环境变量在最顶层,所有进程都能读。缓存变量次之,整个构建树共享。普通变量最底层,受目录作用域限制。

6. 变量优先级:到底听谁的?

当同一个变量名在不同层级出现时,CMake 有一套明确的查找规则:

优先级 变量类型 说明
1(最高) 普通变量(当前作用域) 当前目录的 set() 优先
2 缓存变量 CMakeCache.txt 中的值
3(最低) 环境变量 $ENV{...} 读取

说白了,CMake 先找当前作用域的普通变量,找不到再找缓存变量,最后才看环境变量。这个顺序我建议你记牢,调试时特别有用。

实战经验:我曾经调试一个第三方库的 CMake 脚本,发现某个变量值总是不对。查了半天,原来是父目录的 set() 和缓存变量冲突了。最后用 message() 打印变量来源才定位到问题。

7. 实用技巧与避坑指南

最后分享几个我这些年积累的经验:

  • 用 -D 传参最稳妥:cmake -DMY_VAR=value 这种方式设置的变量,会直接写入缓存,优先级高且可预测。
  • message() 是你的好朋友:不确定变量值?直接 message("VAR=${VAR}") 打印出来看。
  • 缓存变量要加说明:set(... CACHE STRING "说明") 里的说明字符串别偷懒,cmake-gui 里会显示。
  • 避免变量名冲突:我习惯给项目变量加前缀,比如 MYPROJ_XXX,防止和第三方库的变量撞车。

曾经踩过的坑:我在一个项目中用了 add_subdirectory() 加载子模块,子模块里 set() 了一个变量,我以为父目录也能用。结果编译时各种报错。后来才意识到,子目录的普通变量默认不会传到父目录。要么用 PARENT_SCOPE,要么用缓存变量。

嗯,变量这块内容就这些。理解普通变量、缓存变量、环境变量的区别,再搞懂作用域规则,CMake 的变量系统你就掌握了八成。剩下的就是在项目中多练,遇到问题多打 message() 调试。


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