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++ 里的局部变量。
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() 的关系,以及如何用它们构建灵活的配置系统。