3. set命令详解:set()设置普通变量与缓存变量的区别,CACHE与FORCE关键字

好,咱们今天来聊聊 CMake 里最常用的一个命令——set()。

说实话,我见过不少项目,CMakeLists.txt 写得挺长,但变量到底怎么设的、设在哪一层,很多人其实没搞明白。尤其是普通变量和缓存变量的区别,一不留神就踩坑。

嗯,咱们今天就把这个事彻底说清楚。

3.1 普通变量:最基础的变量

先看最简单的用法:

set(MY_VAR "hello")

这行代码干了什么?它在当前作用域里定义了一个叫 MY_VAR 的变量,值是 "hello"。

什么叫「当前作用域」?说白了,就是你在哪个 CMakeLists.txt 文件里写的,这个变量就只在这个文件以及它调用的子目录里有效。出了这个门,人家就不认了。

举个例子:

# 顶层 CMakeLists.txt
set(TOP_VAR "我是顶层")
add_subdirectory(sub)

# sub/CMakeLists.txt
message("TOP_VAR = ${TOP_VAR}")  # 能打印出来

子目录能访问父目录的变量,但反过来不行。这是 CMake 的作用域规则,有点像 C++ 里的局部变量。

小技巧: 我习惯在顶层 CMakeLists.txt 里用 set() 定义一些全局性的路径变量,比如源码目录、输出目录。子目录直接引用就行,省得重复写。

3.2 缓存变量:跨目录、跨构建的持久化

普通变量有个问题——它只在当前 CMake 运行期间有效。你重新运行 cmake,变量就没了。

那如果我想让变量在多次构建之间保持住呢?比如用户选择了一个编译选项,下次构建时不想让他再选一遍。

这时候就需要缓存变量了。

set(MY_CACHE_VAR "world" CACHE STRING "这是一个缓存变量")

看到没?多了 CACHE 关键字,后面还跟了类型和描述。

缓存变量会被写入 CMakeCache.txt 文件。下次再跑 cmake,这个值还在。

类型参数有这些:

类型 说明 典型用途
BOOL 布尔值,ON/OFF 开关选项
STRING 字符串 路径、名称等
FILEPATH 文件路径 指定某个文件
PATH 目录路径 指定某个目录
INTERNAL 内部变量,不显示在 GUI 中 计算出的中间值

我个人最常用的是 BOOL 和 PATH。比如:

set(ENABLE_FEATURE_X ON CACHE BOOL "是否启用特性X")
set(THIRD_PARTY_DIR "" CACHE PATH "第三方库的路径")
注意: 缓存变量一旦设置,后续的 set() 调用不会覆盖它!除非你用了 FORCE。

3.3 CACHE 与 FORCE 关键字:谁说了算?

好,重点来了。

假设你写了一个 CMakeLists.txt:

set(MY_VAL "第一次设置" CACHE STRING "测试")
set(MY_VAL "第二次设置" CACHE STRING "测试")
message("MY_VAL = ${MY_VAL}")

你猜打印出来的是什么?

答案是 "第一次设置"。

为什么会这样?因为 CMake 的规则是:缓存变量一旦存在,就不会被后续的 set(... CACHE ...) 覆盖。它认为第一次设置的值是用户想要的,后续的调用只是「建议」,不能强行改。

那如果我真的想强制更新呢?用 FORCE:

set(MY_VAL "第一次设置" CACHE STRING "测试")
set(MY_VAL "强制更新" CACHE STRING "测试" FORCE)
message("MY_VAL = ${MY_VAL}")  # 输出 "强制更新"

FORCE 的意思就是:「别管之前有没有,给我覆盖掉!」

我曾经在一个项目里遇到过这样的问题:项目依赖一个第三方库的路径,第一次配置时用户填错了。他重新运行 cmake 并传了正确的路径,但 CMake 死活不认——因为缓存里已经有旧值了。后来我加了个 FORCE 才搞定。

核心区别总结:
  • 普通变量:当前作用域有效,每次构建重新计算
  • 缓存变量:跨构建持久化,存储在 CMakeCache.txt
  • 不加 FORCE:缓存变量一旦存在,不会被覆盖
  • 加 FORCE:强制覆盖缓存中的旧值

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

你可能会问:如果普通变量和缓存变量同名,哪个生效?

答案是:普通变量优先。

看这个例子:

set(MY_VAL "我是普通变量")
set(MY_VAL "我是缓存变量" CACHE STRING "测试")
message("MY_VAL = ${MY_VAL}")  # 输出 "我是普通变量"

CMake 在查找变量时,先看当前作用域有没有普通变量。有就用它。没有才去缓存里找。

这个设计其实挺合理的。你想想看,如果你在 CMakeLists.txt 里明确设了一个值,那肯定是你想要的值。缓存里的旧值只是「默认值」而已。

但这里有个坑:如果你在命令行用 -D 传参:

cmake -DMY_VAL="命令行值" ..

这个 -D 设置的是缓存变量。如果 CMakeLists.txt 里又用 set() 设了同名普通变量,那普通变量会覆盖命令行传的值。

嗯,这里要注意:-D 并不是万能的。它只是往缓存里写了个值,但普通变量可以「屏蔽」它。

3.5 实战建议:什么时候用哪种?

说了这么多,我总结一下自己的使用习惯:

  • 内部使用的变量(比如临时路径、计算出的值):用普通变量。没必要写到缓存里,每次重新算就行。
  • 用户可配置的选项(比如开关、路径):用缓存变量。这样用户配一次,下次构建不用再配。
  • 需要强制更新的内部缓存(比如自动检测到的库路径):用 CACHE ... FORCE。确保每次重新检测时能覆盖旧值。

举个例子,我写过一个检测 OpenCV 的模块:

# 先尝试自动检测
find_package(OpenCV QUIET)
if(OpenCV_FOUND)
  set(OpenCV_PATH "${OpenCV_DIR}" CACHE PATH "OpenCV路径" FORCE)
else()
  # 如果没找到,让用户手动指定
  set(OpenCV_PATH "" CACHE PATH "OpenCV路径")
endif()

这样设计的好处是:自动检测到的路径会强制更新缓存,但用户手动指定的路径不会被覆盖。

避坑指南: 我曾经在一个大型项目里,因为忘了加 FORCE,导致用户改了系统环境后,CMake 还在用旧的缓存路径。编译报错找了半天才发现是缓存没更新。从那以后,凡是自动检测的结果,我都习惯加个 FORCE。

3.6 小结

好了,咱们把 set() 的核心用法捋一遍:

  • 普通变量:作用域内有效,不持久化
  • 缓存变量:跨构建持久化,存储在 CMakeCache.txt
  • CACHE 关键字:声明这是一个缓存变量
  • FORCE 关键字:强制覆盖缓存中的旧值
  • 变量查找优先级:普通变量 > 缓存变量

说白了,set() 就是 CMake 里最基础的变量操作工具。但用好它,你的项目配置会清晰很多。下一节咱们聊聊 option() 和 set() 的关系,以及如何用它们构建灵活的配置系统。