4、CMake变量与缓存:普通变量、缓存变量、环境变量、set与unset、变量作用域
变量这东西,说白了就是CMake里的"便签纸"。你往上面写个值,后面随时能拿出来用。但CMake的变量系统比你想的要复杂一点——它分了三种类型,每种有自己的脾气。我刚开始用CMake时就被缓存变量坑过,编译了半天发现用的还是旧值。嗯,今天咱们就把这事彻底捋清楚。
4.1 普通变量:最常用的"临时便签"
普通变量是CMake里最基础的存在。它只在当前CMakeLists.txt的处理过程中有效,处理完了就没了。就像你开会时在白板上写的数字,散会就擦掉。
设置普通变量用set命令:
set(MY_VAR "hello")
set(MY_LIST "a" "b" "c") # 列表也是用普通变量存
读取时用${}语法:
message("MY_VAR = ${MY_VAR}")
我个人习惯给普通变量起名用大写加下划线,这样一眼就能看出是变量。当然小写也行,但团队里最好统一风格。
set(MY_VAR "")。注意空字符串和"未定义"是两回事,后面会讲到。
4.2 缓存变量:跨运行持久化
缓存变量就不一样了。它会被写入CMakeCache.txt文件,下次运行cmake时还能读到。这玩意儿适合存那些"用户选一次就不想再选"的东西,比如编译选项、安装路径。
设置缓存变量需要加CACHE关键字:
set(MY_CACHE_VAR "default_value" CACHE STRING "这是缓存变量的说明")
这里STRING是变量类型,CMake支持BOOL、FILEPATH、PATH、STRING等。类型决定了在cmake-gui里显示什么控件——BOOL显示复选框,FILEPATH显示文件选择器。
我曾经在一个项目里遇到个坑:缓存变量一旦设置,后续的set命令不会覆盖它,除非你加FORCE。什么意思呢?
# 第一次运行:用户通过-D选项传入
# cmake -DMY_OPT=ON ..
# 第二次运行:即使CMakeLists.txt里写了下面这行
set(MY_OPT OFF CACHE BOOL "我的选项")
# MY_OPT 仍然是 ON!因为缓存里已经有值了
要强制覆盖,得这么写:
set(MY_OPT OFF CACHE BOOL "我的选项" FORCE)
4.3 环境变量:跟操作系统打交道
环境变量是CMake和外部世界沟通的桥梁。它读取的是你系统里的环境变量,比如PATH、HOME、JAVA_HOME。
读取环境变量用$ENV{}:
message("HOME = $ENV{HOME}")
message("PATH = $ENV{PATH}")
设置环境变量用set(ENV{...}):
set(ENV{MY_ENV_VAR} "hello_from_cmake")
但要注意:这个设置只影响当前cmake进程及其子进程,不会修改你系统的环境变量。我刚开始学的时候以为能永久改PATH,结果发现关了终端就没了。
环境变量在找依赖时特别有用。比如你要找某个第三方库,可以先看看$ENV{MY_LIB_DIR}有没有设置:
if(DEFINED ENV{MY_LIB_DIR})
set(MY_LIB_DIR "$ENV{MY_LIB_DIR}")
else()
set(MY_LIB_DIR "/usr/local")
endif()
4.4 set与unset:变量的生与死
set我们已经见过了。那怎么删除一个变量呢?用unset。
unset(MY_VAR) # 删除普通变量
unset(MY_CACHE_VAR CACHE) # 删除缓存变量
unset(ENV{MY_ENV_VAR}) # 删除环境变量(仅当前进程)
为什么要删除变量?我遇到过这种情况:项目里有个全局变量,某个子模块用了它,但另一个子模块不该用。如果不unset,变量会一直传递下去,造成"幽灵依赖"——你根本不知道这个值是从哪来的。
判断变量是否存在用if(DEFINED ...):
if(DEFINED MY_VAR)
message("MY_VAR 存在,值为 ${MY_VAR}")
else()
message("MY_VAR 不存在")
endif()
这里有个细节:空字符串变量是"存在"的。所以set(MY_VAR "")之后,DEFINED返回真。要区分"未设置"和"空值",可以用if(MY_VAR)——空字符串会被当成假。
4.5 变量作用域:函数与子目录的隔离
作用域是CMake变量系统里最容易让人懵的地方。简单说:
- 普通变量在
function()里是局部作用域 - 在
add_subdirectory()里也是局部作用域 - 缓存变量是全局的
- 环境变量也是全局的(但只影响当前进程)
看个例子:
set(GLOBAL_VAR "我是全局的")
function(my_func)
set(LOCAL_VAR "我是局部的")
message("函数内: GLOBAL_VAR = ${GLOBAL_VAR}") # 能访问
message("函数内: LOCAL_VAR = ${LOCAL_VAR}") # 能访问
endfunction()
my_func()
message("函数外: LOCAL_VAR = ${LOCAL_VAR}") # 空!访问不到
为什么会这样?因为函数创建了一个新的作用域。函数结束后,里面的局部变量就销毁了。
那怎么让函数修改外面的变量呢?用PARENT_SCOPE:
function(my_func)
set(LOCAL_VAR "我出去了" PARENT_SCOPE)
endfunction()
my_func()
message("现在能访问了: ${LOCAL_VAR}") # 输出"我出去了"
子目录的情况类似。每个add_subdirectory()都会创建一个新作用域。子目录里定义的变量,父目录看不到。除非你用set(... PARENT_SCOPE)或者用缓存变量。
4.6 知识体系总览
下面这张图把三种变量、它们的操作和作用域关系串起来了。你可以把它当成一个快速参考:
4.7 实战建议
说了这么多,总结几条我自己的经验:
- 能用普通变量就别用缓存变量。 缓存变量会污染CMakeCache.txt,用户用cmake-gui时看到一堆无关选项会很烦。
- 给缓存变量写清楚的说明。 那个
CACHE STRING "说明"里的字符串,是用户在cmake-gui里唯一能看到的信息。我见过有人写"a",这跟没写一样。 - 函数里修改变量记得加PARENT_SCOPE。 否则你改了等于没改,排查起来很痛苦。
- 环境变量只读不写。 除非你有特殊需求,否则别在CMakeLists.txt里改环境变量。改了别人也不知道,容易出诡异问题。
- 用
message()调试变量。 不确定变量当前是什么值?直接message("VAR = ${VAR}")打印出来看看。这是最笨但最有效的方法。
set(SOME_VAR "xxx"),但父目录里也有同名变量。结果子目录的修改没传回去,父目录一直用旧值。排查了整整两个小时才发现是作用域问题。从那以后,我养成了一个习惯:所有跨目录共享的变量,要么用缓存变量,要么显式用PARENT_SCOPE传回去。
好了,变量这块就聊到这儿。记住一句话:普通变量是临时的,缓存变量是持久的,环境变量是外部的。搞清楚了这三者的区别,CMake的变量系统你就掌握了八成。
公众号:蓝海资料掘金营,微信deep3321