目录
2.2.1.示例1——不带force的set是不能覆盖已经存在的缓存变量的
2.2.2.示例2——带force的set才能覆盖已经存在的缓存变量
2.3.2.使用-d来替换掉CMakeLists.txt里面指定的缓存变量
一. 缓存变量
我们去官网看看是怎么说的:set — CMake 4.1.1 Documentation
翻译下来也大概就是下面这样子。
设置 CMake 缓存条目 (Set Cache Entry)
set(<变量名> <值>... CACHE <类型> <说明文字> [FORCE])
这条命令用于在 CMake 中创建或修改一个缓存变量。您可以把缓存变量想象成项目的配置设置,这些设置会被保存下来(在 CMakeCache.txt
文件中),以便下次运行 CMake 时记住用户的选择。正因为它们旨在让用户自定义,所以默认情况下,如果该缓存条目已经存在,set
命令不会覆盖它。如果您希望强制覆盖现有的值,请使用 FORCE
选项。
参数详解:
<类型>
(必须指定): 定义了变量的类型,它决定了在cmake-gui
等图形化工具中如何与用户交互。必须是以下类型之一:BOOL
: 布尔值,只能是ON
或OFF
。例如,用来控制是否编译某个功能模块。在cmake-gui
中会显示为一个复选框,非常直观。FILEPATH
: 指向磁盘上某个文件的路径。例如,指定一个外部工具的路径。在cmake-gui
中会提供一个文件选择对话框,让用户方便地浏览和选择。PATH
: 指向磁盘上某个目录的路径。例如,指定第三方库的安装目录。在cmake-gui
中同样会提供一个目录选择对话框。STRING
: 一行普通的文本。在cmake-gui
中显示为一个文本框。如果您还通过set_property(CACHE <变量名> PROPERTY STRINGS ...)
设置了可选值列表,它则会变成一个下拉选择框,让用户从预定义的选项中选择。INTERNAL
: 也是一行文本,但主要用于 CMake 内部使用。这种类型的变量不会显示在cmake-gui
中,用户无法看到或修改。它通常用于在多次运行 CMake 时在内部持久化存储一些状态或信息。注意:使用此类型隐含了FORCE
选项,即会自动覆盖旧值。
<说明文字>
(必须指定): 这是一段简单的描述文字,用于解释这个缓存变量的作用和目的。当用户在cmake-gui
中将鼠标悬停在变量上时,这段文字就会显示出来,帮助用户理解该如何配置。请务必写得清晰明了。[FORCE]
(可选): 就像上面提到的,加上这个选项会强制用新值覆盖已经存在的缓存条目。如果你确信无论之前用户设置成什么,都需要被当前脚本中的值重置,那就使用它。
重要注意事项 (非常重要!):
变量覆盖规则 (优先级): CMake 中变量的查找规则是:普通变量会覆盖未使用的缓存变量。这意味着,如果你之前用
set(MY_VAR "value")
(没有CACHE
)设置了一个普通变量,那么直接读取MY_VAR
得到的是普通变量的值,而不是缓存变量的值。要访问缓存变量,需要使用$CACHE{MY_VAR}
语法(CMake 3.13及以上版本)。这是一个非常常见的困惑点!处理命令行创建的变量: 用户可能在运行 CMake 时通过
-D<变量>=<值>
的命令行选项创建了一个缓存变量,但没有指定类型。此时,set(... CACHE ...)
命令会为其补充上类型。路径转换: 如果一个通过
-D
创建的、类型为PATH
或FILEPATH
的缓存变量,其值是相对路径(如../mylib
),那么当set
命令为其显式设置类型时,CMake 会自动将这个相对路径转换为基于当前工作目录的绝对路径,从而保证路径的准确性。
二.创建缓存变量
2.1.使用set()来创建缓存变量
📂 目录结构
demo/
└── CMakeLists.txt
🔹 demo/CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(CacheVarDemo)
# 1. 创建一些缓存变量(会写入 build/CMakeCache.txt)
set(MY_BOOL ON CACHE BOOL "是否启用某个功能模块")
set(MY_PATH "../mylib" CACHE PATH "第三方库路径")
set(MY_FILE "../README.md" CACHE FILEPATH "某个文件路径")
set(MY_STR "hello" CACHE STRING "一段字符串")
# 2. 显示结果
message("MY_BOOL='${MY_BOOL}'")
message("MY_PATH='${MY_PATH}'")
message("MY_FILE='${MY_FILE}'")
message("MY_STR='${MY_STR}'")
其实我们可以一键运行下面这个命令来进行搭建这个目录结构
mkdir -p demo && cat > demo/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.15)
project(CacheVarDemo)
# 1. 创建一些缓存变量(会写入 build/CMakeCache.txt)
set(MY_BOOL ON CACHE BOOL "是否启用某个功能模块")
set(MY_PATH "../mylib" CACHE PATH "第三方库路径")
set(MY_FILE "../README.md" CACHE FILEPATH "某个文件路径")
set(MY_STR "hello" CACHE STRING "一段字符串")
# 2. 显示结果
message("MY_BOOL='${MY_BOOL}'")
message("MY_PATH='${MY_PATH}'")
message("MY_FILE='${MY_FILE}'")
message("MY_STR='${MY_STR}'")
EOF
在 demo
目录下执行:
rm -rf build && mkdir build && cd build && cmake ..
大家仔细看看
几乎所有的缓存变量都会被记录到 CMakeCache.txt
文件中。 这个文件本质上就是一个持久化的键值对存储,CMake 的主要目的就是通过这个文件来记住用户的配置选择,从而实现“一次配置,多次使用”,避免每次运行时都重新询问所有配置。
我们现在就去查看CMakeCache.txt中的内容
嗯?怎么这么多别的变量啊?
CMakeCache.txt
文件不仅仅存储了通过 set(... CACHE ...)
或 -D
选项显式设置的变量,它更是一个庞大的数据库,存储了CMake在配置和生成过程中产生的、为了确保下次运行一致所必需的大量内部缓存变量。
那么我们怎么查询我们设置的缓存变量呢?其实我们可以借助grep
grep MY_ CMakeCache.txt
意思是:
grep
:Linux/Unix 下的文本搜索工具。MY_
:要搜索的关键字(这里是匹配所有以MY_
开头的变量名)。CMakeCache.txt
:要搜索的文件,就是 CMake 生成的缓存文件。
怎么样?还是很容易理解的吧。
2.2.使用FORCE参数来覆盖缓存变量
我们来好好了解一下
set(<variable> <value> CACHE <type> <docstring> [FORCE])
命令的执行遵循一套严格的规则:
场景一:缓存变量不存在(初次运行)
触发条件:当你第一次在一个空的构建目录中运行 CMake 时,
CMakeCache.txt
文件不存在或其中没有名为<variable>
的条目。CMake 的决策逻辑:
检查:在缓存中查找
<variable>
,结果是没找到。行动:“用户显然还没有机会设置这个变量。我将把我(开发者)提供的
<value>
作为初始值,并将其持久化到缓存中。”
最终结果:缓存变量 被创建,其值被设置为命令中提供的
<value>
。这个值会被写入CMakeCache.txt
文件,成为一个正式的、可供用户修改的配置选项。比喻:你提供了一份合同的初稿,因为还没有正式版本,所以初稿直接被采纳为正式合同。
场景二:缓存变量已存在(后续运行)
触发条件:在后续的配置运行中,
CMakeCache.txt
文件已经存在,并且包含了名为<variable>
的条目。它的值可能是:之前设置的默认值。
用户通过
cmake-gui
/ccmake
修改后的值。用户通过命令行
-D<variable>=<new_value>
设置的值。
CMake 的决策逻辑(无
FORCE
关键字):检查:在缓存中查找
<variable>
,结果“命中”!其当前值为[cached_value]
。决策:“这个变量已经存在了。这意味着用户可能已经看到了它,并且有机会做出自己的选择。我的职责是提供一个默认值,而不是一个强制值。 既然用户没有表达修改的意图(这次运行没有用新的
-D
重新指定),那么我应该继续信任并保留缓存中现有的值。”行动:忽略本次
set
命令中提供的<value>
。
最终结果:缓存变量的值 保持不变。这次
set
命令实际上成了一个“空操作”。比喻:用户已经在你给的合同初稿上做了修改并签了字。你不会拿一份新的空白的初稿覆盖掉已经签署的合同。你尊重用户的最终决定。
场景三:强制覆盖(使用 FORCE
关键字)
触发条件:在命令中使用了
FORCE
选项:set(... CACHE ... FORCE)
。CMake 的决策逻辑:
检查:在缓存中查找
<variable>
,结果“命中”!其当前值为[old_value]
。决策:“虽然这个变量已经存在,但命令中包含了
FORCE
关键字!这说明开发者明确意图要覆盖当前的值,无论用户之前设置过什么。”行动:使用本次
set
命令中提供的<value>
覆盖缓存中现有的值。
最终结果:缓存变量的值 被强制更新 为命令中提供的新
<value>
。比喻:这是一个“单方面修订条款”的行为。无论合同之前是什么状态,现在都用这份新的版本强制替换它。
2.2.1.示例1——不带force的set是不能覆盖已经存在的缓存变量的
话不多说,我们先看例子
📂 目录结构
demo/
└── CMakeLists.txt
🔹 demo/CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(CacheNoForceDemo)
# 第一次设置缓存变量
set(MY_VAR "first" CACHE STRING "测试变量")
message("第一次设置后 MY_VAR='${MY_VAR}'")
# 第二次尝试覆盖(不带 FORCE)
set(MY_VAR "second" CACHE STRING "测试变量")
message("第二次尝试覆盖后 MY_VAR='${MY_VAR}'")
我们可以通过下面这一条连着的bash语句来搭建这个目录结构和文件
mkdir -p demo && \
cat > demo/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.15)
project(CacheNoForceDemo)
# 第一次设置缓存变量
set(MY_VAR "first" CACHE STRING "测试变量")
message("第一次设置后 MY_VAR='${MY_VAR}'")
# 第二次尝试覆盖(不带 FORCE)
set(MY_VAR "second" CACHE STRING "测试变量")
message("第二次尝试覆盖后 MY_VAR='${MY_VAR}'")
EOF
接下来我们就来构建这个项目
mkdir build && cd build && cmake ..
我们发现覆盖前后都是一样的。也就是说,我们的覆盖是失败的。
2.2.2.示例2——带force的set才能覆盖已经存在的缓存变量
好的 👍,我给你一个「对照实验」:同一个项目里,先演示 不带 FORCE 时无法覆盖,再演示 加 FORCE 成功覆盖。
📂 目录结构
demo/
└── CMakeLists.txt
🔹 demo/CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(CacheForceCompareDemo)
# 第一次设置缓存变量
set(MY_VAR "first" CACHE STRING "测试变量")
message("第一次设置后 MY_VAR='${MY_VAR}'")
# 第二次尝试覆盖(不带 FORCE)
set(MY_VAR "second" CACHE STRING "测试变量")
message("第二次尝试覆盖后 MY_VAR='${MY_VAR}'")
# 第三次尝试覆盖(加 FORCE)
set(MY_VAR "third" CACHE STRING "测试变量" FORCE)
message("第三次使用 FORCE 覆盖后 MY_VAR='${MY_VAR}'")
我们可以通过下面这个命令来一键搭建出这个目录结构和文件
mkdir -p demo && \
cat > demo/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.15)
project(CacheForceCompareDemo)
# 第一次设置缓存变量
set(MY_VAR "first" CACHE STRING "测试变量")
message("第一次设置后 MY_VAR='${MY_VAR}'")
# 第二次尝试覆盖(不带 FORCE)
set(MY_VAR "second" CACHE STRING "测试变量")
message("第二次尝试覆盖后 MY_VAR='${MY_VAR}'")
# 第三次尝试覆盖(加 FORCE)
set(MY_VAR "third" CACHE STRING "测试变量" FORCE)
message("第三次使用 FORCE 覆盖后 MY_VAR='${MY_VAR}'")
EOF
接下来我们就来构建我们的项目
mkdir build && cd build && cmake ..
我们发现,每一次打印的结果都是不一样的,这就更加说明了我们的猜想
- 不带 FORCE → 已存在的缓存值不会被覆盖。
带 FORCE → 立刻覆盖缓存里的旧值。
2.2.3.对比示例
我们可以写一个例子,用 两个缓存变量,分别演示 不使用 FORCE 和 使用 FORCE 的效果。
📂 目录结构
demo/
└── CMakeLists.txt
🔹 demo/CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(CacheTwoVarsDemo)
# 第一次创建两个缓存变量
set(VAR1 "first1" CACHE STRING "第一个缓存变量")
set(VAR2 "first2" CACHE STRING "第二个缓存变量")
# 尝试修改,不加 FORCE
set(VAR1 "second1" CACHE STRING "第一个缓存变量")
set(VAR2 "second2" CACHE STRING "第二个缓存变量" FORCE)
# 打印结果
message("VAR1='${VAR1}' (不使用 FORCE)")
message("VAR2='${VAR2}' (使用 FORCE)")
其实我们可以通过下面这个目录来一键构建出这个目录结构和文件
mkdir -p demo && cat > demo/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.15)
project(CacheTwoVarsDemo)
# 第一次创建两个缓存变量
set(VAR1 "first1" CACHE STRING "第一个缓存变量")
set(VAR2 "first2" CACHE STRING "第二个缓存变量")
# 尝试修改,不加 FORCE
set(VAR1 "second1" CACHE STRING "第一个缓存变量")
set(VAR2 "second2" CACHE STRING "第二个缓存变量" FORCE)
# 打印结果
message("VAR1='${VAR1}' (不使用 FORCE)")
message("VAR2='${VAR2}' (使用 FORCE)")
EOF
接下来我们就来构建项目
rm -rf build && mkdir build && cd build && cmake ..
🔹 解释
- VAR1第一次创建是
"first1"
,之后修改"second1"
没有 FORCE,所以缓存保持"first1"
。 - VAR2第一次创建是
"first2"
,修改"second2"
使用 FORCE,缓存被覆盖成"second2"
。
我们可以去CMakeChace.txt里面看看
和我们的运行结果可是一模一样的。
2.3.命令行 -D 创建/覆盖缓存变量
2.3.1.直接使用-D来创建/覆盖缓存变量
📂 目录结构
demo/
└── CMakeLists.txt
这里的 CMakeLists.txt
可以几乎空,但保留一个最小声明:
cmake_minimum_required(VERSION 3.15)
project(DCacheDemo)
接下来我们就来看看
rm -rf build && mkdir build && cd build
接下来我们就使用 -D 创建新缓存变量
cmake .. -DMY_NEW_VAR="hello"
我们现在就可以去CMakeCache.txt里面查看这个缓存变量
grep MY_NEW_VAR CMakeCache.txt
跟我们设置的一模一样的。
接下来我们将使用 -D 覆盖已有缓存变量
然后运行:
cmake .. -DMY_NEW_VAR="hello world"
我们现在就可以去CMakeCache.txt里面查看这个缓存变量
grep MY_NEW_VAR CMakeCache.txt
怎么样?
2.3.2.使用-d来替换掉CMakeLists.txt里面指定的缓存变量
📂 目录结构
demo/
└── CMakeLists.txt
🔹 demo/CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(DOverrideDemo)
# 定义一个缓存变量
set(MY_OPTION "from_cmakelists" CACHE STRING "演示缓存变量")
# 打印最终结果
message("MY_OPTION='${MY_OPTION}'")
我们可以一键搭建这个项目的目录结构和文件
mkdir -p demo && cat > demo/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.15)
project(DOverrideDemo)
# 定义一个缓存变量
set(MY_OPTION "from_cmakelists" CACHE STRING "演示缓存变量")
# 打印结果
message("MY_OPTION='${MY_OPTION}'")
EOF
接下来我们来构建一下这个项目
rm -rf build && mkdir build && cd build && cmake ..
现在我们可以去CMakeCache.txt
里看看对应条目:
接下来我们使用 -D
覆盖 CMakeLists.txt 中的缓存变量
cmake .. -DMY_OPTION="from_commandline"
这个时候我们回去那个CMakeCache.txt
看看这个条目被更新为:
三.缓存变量的作用域
在 CMake 的变量体系中,缓存变量是一个特殊的存在,它完全超越了普通变量所遵循的“目录作用域”规则。
您可以将其理解为项目配置中的全局变量或持久化设置。
它的核心特征在于其全局可见性和持久化存储,这与普通变量的局部性和临时性形成了鲜明对比。
核心特性:全局唯一与持久化
全局唯一性(单一事实来源):
整个 CMake 项目中,任何一个特定的缓存变量有且只有一个。它被存储在一个独立的、全局的存储区中,通常被视为所有目录作用域之上的一个共享层。无论您在当前目录、子目录,还是父目录中读取一个名为MY_CACHE_VAR
的缓存变量,您访问的都是同一个全局实体。它的值在任何地方、任何时候(在一次配置过程中)都是一致的。无视目录作用域隔离:
这是缓存变量与普通变量最根本的区别。普通变量严格遵守目录作用域的“向下继承,向上隔离”规则。而缓存变量则完全无视这堵“墙”。读操作:在任何目录作用域中读取缓存变量,得到的都是其全局唯一的值。
写操作:在任何目录作用域中修改缓存变量的值,都会立即更新这个全局唯一的值,并且这个更改立刻对所有其他目录作用域可见。在一个子目录中修改了缓存变量,父目录或其他兄弟目录在随后读取它时,会立刻得到这个新值。这彻底打破了普通变量那种“修改互不影响”的隔离性。
持久化存储(跨运行存在):
缓存变量的值不会被保存在CMakeLists.txt
文件里,而是会被写入到 CMake 构建目录下的CMakeCache.txt
文件中。这个文件是 CMake 的“记忆中心”。这意味着:一旦一个缓存变量被设置,它的值会在您多次运行
cmake
配置命令的过程中持续存在。这正是图形化配置工具(如
cmake-gui
或ccmake
)能够展示并允许用户修改的变量列表。要清除一个缓存变量,必须手动删除构建目录、在 GUI 中操作,或使用
unset(... CACHE)
命令。
3.1.示例1——全局可见行和全局唯一性
📂 目录结构
demo/
├── CMakeLists.txt
└── sub/
├── CMakeLists.txt
└── subsub/
└── CMakeLists.txt
🔹 顶层 demo/CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(GlobalCacheVarDemo)
set(MY_CACHE_VAR "from_top" CACHE STRING "演示全局缓存变量" FORCE)
message("顶层看到 MY_CACHE_VAR='${MY_CACHE_VAR}'")
add_subdirectory(sub)
message("顶层再次看到 MY_CACHE_VAR='${MY_CACHE_VAR}'")
🔹 子目录 demo/sub/CMakeLists.txt
message("子目录进入时 MY_CACHE_VAR='${MY_CACHE_VAR}'")
set(MY_CACHE_VAR "from_sub" CACHE STRING "演示全局缓存变量" FORCE)
message("子目录修改后 MY_CACHE_VAR='${MY_CACHE_VAR}'")
add_subdirectory(subsub)
# 在孙子目录修改完之后,再次查看
message("子目录返回后 MY_CACHE_VAR='${MY_CACHE_VAR}'")
🔹 孙子目录 demo/sub/subsub/CMakeLists.txt
message("孙子目录进入时 MY_CACHE_VAR='${MY_CACHE_VAR}'")
set(MY_CACHE_VAR "from_subsub" CACHE STRING "演示全局缓存变量" FORCE)
message("孙子目录修改后 MY_CACHE_VAR='${MY_CACHE_VAR}'")
其实我们可以通过一行 bash(一次性创建文件并运行)来快速搭建这个目录结构和文件。
mkdir -p demo/sub/subsub && cat > demo/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.15)
project(GlobalCacheVarDemo)
set(MY_CACHE_VAR "from_top" CACHE STRING "演示全局缓存变量" FORCE)
message("顶层看到 MY_CACHE_VAR='${MY_CACHE_VAR}'")
add_subdirectory(sub)
message("顶层再次看到 MY_CACHE_VAR='${MY_CACHE_VAR}'")
EOF
cat > demo/sub/CMakeLists.txt <<'EOF'
message("子目录进入时 MY_CACHE_VAR='${MY_CACHE_VAR}'")
set(MY_CACHE_VAR "from_sub" CACHE STRING "演示全局缓存变量" FORCE)
message("子目录修改后 MY_CACHE_VAR='${MY_CACHE_VAR}'")
add_subdirectory(subsub)
message("子目录返回后 MY_CACHE_VAR='${MY_CACHE_VAR}'")
EOF
cat > demo/sub/subsub/CMakeLists.txt <<'EOF'
message("孙子目录进入时 MY_CACHE_VAR='${MY_CACHE_VAR}'")
set(MY_CACHE_VAR "from_subsub" CACHE STRING "演示全局缓存变量" FORCE)
message("孙子目录修改后 MY_CACHE_VAR='${MY_CACHE_VAR}'")
EOF
接下来我们就来构建我们的项目
mkdir build && cd build && cmake ..
大家仔细观察,就会发现这3个CMakeLists.txt里面操作的都是同一个缓存变量!!!!这就验证了缓存变量的全局可见性和全局唯一性。
注意:如果我们不在set里面加force,运行结果就会是下面这样子。
3.2.示例2——全局可见性
接下来我们将
在 每一层目录都各自
set
一个不同名字的缓存变量(例如TOP_CACHE_VAR
、SUB_CACHE_VAR
、SUBSUB_CACHE_VAR
)。并且在 每一层打印出 全部 3 个变量,直观演示「缓存变量是全局唯一的」特性。
📂 目录结构
demo/
├── CMakeLists.txt
└── sub/
├── CMakeLists.txt
└── subsub/
└── CMakeLists.txt
🔹 demo/CMakeLists.txt (顶层)
cmake_minimum_required(VERSION 3.15)
project(CacheThreeVarsDemo)
# 顶层设置一个缓存变量
set(TOP_CACHE_VAR "set_in_top" CACHE STRING "顶层缓存变量")
message("[顶层] 进入时: TOP='${TOP_CACHE_VAR}', SUB='${SUB_CACHE_VAR}', SUBSUB='${SUBSUB_CACHE_VAR}'")
# 进入子目录
add_subdirectory(sub)
message("[顶层] 返回时: TOP='${TOP_CACHE_VAR}', SUB='${SUB_CACHE_VAR}', SUBSUB='${SUBSUB_CACHE_VAR}'")
🔹 demo/sub/CMakeLists.txt (子目录)
message("[子目录] 进入时: TOP='${TOP_CACHE_VAR}', SUB='${SUB_CACHE_VAR}', SUBSUB='${SUBSUB_CACHE_VAR}'")
# 设置子目录自己的缓存变量
set(SUB_CACHE_VAR "set_in_sub" CACHE STRING "子目录缓存变量")
message("[子目录] 设置后: TOP='${TOP_CACHE_VAR}', SUB='${SUB_CACHE_VAR}', SUBSUB='${SUBSUB_CACHE_VAR}'")
# 进入孙子目录
add_subdirectory(subsub)
message("[子目录] 从孙子返回时: TOP='${TOP_CACHE_VAR}', SUB='${SUB_CACHE_VAR}', SUBSUB='${SUBSUB_CACHE_VAR}'")
🔹 demo/sub/subsub/CMakeLists.txt (孙子目录)
message("[孙子目录] 进入时: TOP='${TOP_CACHE_VAR}', SUB='${SUB_CACHE_VAR}', SUBSUB='${SUBSUB_CACHE_VAR}'")
# 设置孙子目录自己的缓存变量
set(SUBSUB_CACHE_VAR "set_in_subsub" CACHE STRING "孙子目录缓存变量")
message("[孙子目录] 设置后: TOP='${TOP_CACHE_VAR}', SUB='${SUB_CACHE_VAR}', SUBSUB='${SUBSUB_CACHE_VAR}'")
我们可以一键复制下面的 Bash 语句来创建我们的目录结构和文件
mkdir -p demo/sub/subsub && cat > demo/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.15)
project(CacheThreeVarsDemo)
set(TOP_CACHE_VAR "set_in_top" CACHE STRING "顶层缓存变量")
message("[顶层] 进入时: TOP='${TOP_CACHE_VAR}', SUB='${SUB_CACHE_VAR}', SUBSUB='${SUBSUB_CACHE_VAR}'")
add_subdirectory(sub)
message("[顶层] 返回时: TOP='${TOP_CACHE_VAR}', SUB='${SUB_CACHE_VAR}', SUBSUB='${SUBSUB_CACHE_VAR}'")
EOF
cat > demo/sub/CMakeLists.txt <<'EOF'
message("[子目录] 进入时: TOP='${TOP_CACHE_VAR}', SUB='${SUB_CACHE_VAR}', SUBSUB='${SUBSUB_CACHE_VAR}'")
set(SUB_CACHE_VAR "set_in_sub" CACHE STRING "子目录缓存变量")
message("[子目录] 设置后: TOP='${TOP_CACHE_VAR}', SUB='${SUB_CACHE_VAR}', SUBSUB='${SUBSUB_CACHE_VAR}'")
add_subdirectory(subsub)
message("[子目录] 从孙子返回时: TOP='${TOP_CACHE_VAR}', SUB='${SUB_CACHE_VAR}', SUBSUB='${SUBSUB_CACHE_VAR}'")
EOF
cat > demo/sub/subsub/CMakeLists.txt <<'EOF'
message("[孙子目录] 进入时: TOP='${TOP_CACHE_VAR}', SUB='${SUB_CACHE_VAR}', SUBSUB='${SUBSUB_CACHE_VAR}'")
set(SUBSUB_CACHE_VAR "set_in_subsub" CACHE STRING "孙子目录缓存变量")
message("[孙子目录] 设置后: TOP='${TOP_CACHE_VAR}', SUB='${SUB_CACHE_VAR}', SUBSUB='${SUBSUB_CACHE_VAR}'")
EOF
接下来我们就来构建我们的项目
mkdir build && cd build && cmake ..
这样你就能看到:
每层定义的缓存变量 对全局都可见。
即使是后设置的变量(
SUB_CACHE_VAR
、SUBSUB_CACHE_VAR
),顶层最后也能读到它们的值。
要不要我再帮你加一个 普通变量版本(不带 CACHE),并打印对比差异?
四. 缓存变量与普通变量的交互:优先级规则
当一个变量名既作为普通变量存在,又作为缓存变量存在时,CMake 遵循一条明确的优先级规则:
普通变量的设置会“遮盖”缓存变量。
一旦通过`set(MY_VAR "value")`这样的语句在当前作用域内定义了一个普通变量,该作用域及其所有由此向下延伸的子作用域(通过`add_subdirectory()`或`function()`调用进入的新作用域)中,任何对`${MY_VAR}`的求值操作都会直接返回这个新设置的普通变量的值。缓存中存储的值依然完好无损地存在于全局缓存中,只是在当前的变量解析路径上被暂时地“绕过”了。
但是,这个“遮盖”效应是局部的,仅限于当前目录作用域及其子作用域。一旦跳出这个范围,仍然可以访问到底层缓存变量的值。
为了提供一种显式且可靠的访问方式,不受当前作用域内普通变量的干扰,CMake引入了`$CACHE{MY_VAR}`语法。这是一种强制的、指向性的访问。如果您使用
$CACHE{MY_VAR}
语法(CMake 3.13+),无论你在哪个作用域,仍然可以访问到底层缓存变量的值。它明确指示CMake解释器绕过所有当前作用域内的普通变量查找,直接访问全局缓存命名空间并获取其中存储的值。
4.1.示例1——普通变量的设置会“遮盖”缓存变量
好 👌 我给你写一个最简单的例子,演示 同名普通变量和缓存变量的优先级关系。
📂 目录结构
demo/
├── CMakeLists.txt
└── sub/
└── CMakeLists.txt
🔹 顶层 demo/CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(ShadowCacheDemo)
# 定义一个缓存变量
set(MY_VAR "cache_value" CACHE STRING "缓存变量 MY_VAR")
message("[顶层] 初始: MY_VAR='${MY_VAR}' (缓存)")
# 进入子目录
add_subdirectory(sub)
# 回到顶层后,再次读取
message("[顶层] 返回时: MY_VAR='${MY_VAR}' (缓存)")
🔹 子目录 demo/sub/CMakeLists.txt
# 子目录开始
message("[子目录] 进入时: MY_VAR='${MY_VAR}' (继承自缓存)")
# 定义同名的普通变量(遮盖缓存变量)
set(MY_VAR "normal_value")
message("[子目录] 设置普通变量后: MY_VAR='${MY_VAR}' (普通变量覆盖缓存)")
# 如果想显式访问缓存值(CMake 3.13+ 支持)
message("[子目录] 显式访问缓存: $CACHE{MY_VAR}")
🔹 一行 Bash 运行
mkdir -p demo/sub && cat > demo/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.15)
project(ShadowCacheDemo)
set(MY_VAR "cache_value" CACHE STRING "缓存变量 MY_VAR")
message("[顶层] 初始: MY_VAR='${MY_VAR}' (缓存)")
add_subdirectory(sub)
message("[顶层] 返回时: MY_VAR='${MY_VAR}' (缓存)")
EOF
cat > demo/sub/CMakeLists.txt <<'EOF'
message("[子目录] 进入时: MY_VAR='${MY_VAR}' (继承自缓存)")
set(MY_VAR "normal_value")
message("[子目录] 设置普通变量后: MY_VAR='${MY_VAR}' (普通变量覆盖缓存)")
message("[子目录] 显式访问缓存: $CACHE{MY_VAR}")
EOF
接下来我们就来构建我们的项目
mkdir build && cd build && cmake ..
输出
这也就说明了
当一个普通变量被设置成一个与缓存变量同名的名字时,在其作用域内,普通变量的值会“遮盖”(Shadow)掉缓存变量的值。这意味着,直接使用 ${MY_VAR}
将会访问到普通变量的值。
但是,缓存变量本身的值并没有被改变,它依然安全地存储在 CMakeCache.txt
中。一旦离开了那个普通变量的作用域(例如,从子目录返回到父目录),${MY_VAR}
又会重新指向那个未被改变的缓存变量的值。
我们来仔细讲解一下
1.顶层目录,初始阶段
set(MY_VAR "cache_value" CACHE STRING "缓存变量 MY_VAR")
这行代码定义了一个名为
MY_VAR
的缓存变量,其值为"cache_value"
。
message("[顶层] 初始: MY_VAR='${MY_VAR}' (缓存)")
此时,
${MY_VAR}
读取到的就是这个缓存变量的值。输出:
[顶层] 初始: MY_VAR='cache_value' (缓存)
2.进入子目录 sub/
add_subdirectory(sub)
命令执行,CMake 开始处理sub/CMakeLists.txt
。message("[子目录] 进入时: MY_VAR='${MY_VAR}' (继承自缓存)")
子目录继承了父目录的作用域。此时还没有同名的普通变量,所以
${MY_VAR}
仍然解析为缓存变量的值。输出:
[子目录] 进入时: MY_VAR='cache_value' (继承自缓存)
3.在子目录中设置普通变量
set(MY_VAR "normal_value")
(注意:没有CACHE
关键字)这行代码定义了一个同名的普通变量。根据“遮盖”规则,从现在开始,在当前目录(子目录)的作用域内,
${MY_VAR}
将指向这个新的普通变量。
message("[子目录] 设置普通变量后: MY_VAR='${MY_VAR}' (普通变量覆盖缓存)")
正如规则所述,它现在读取到的是普通变量的值
"normal_value"
。输出:
[子目录] 设置普通变量后: MY_VAR='normal_value' (普通变量覆盖缓存)
4.显式访问缓存变量
message("[子目录] 显式访问缓存: $CACHE{MY_VAR}")
CMake 3.13 引入了
$CACHE{VARNAME}
语法,允许你显式地、直接地访问缓存变量的值,绕过任何可能存在的同名普通变量。所以,这里它成功地读取到了被“遮盖”的缓存变量的原始值
"cache_value"
。输出:
[子目录] 显式访问缓存: cache_value
这个操作非常重要,它证明了缓存变量
MY_VAR
的值自始至终都没有被改变过。
5.返回顶层目录
子目录的
CMakeLists.txt
处理完毕,CMake 返回到顶层目录继续执行。当离开子目录的作用域时,在那个作用域内定义的普通变量
MY_VAR
(值为"normal_value"
)就被销毁了。message("[顶层] 返回时: MY_VAR='${MY_VAR}' (缓存)")
现在,
${MY_VAR}
前面已经没有同名的普通变量来遮盖它了,所以它再次清晰地指向了那个全局的、一直未变的缓存变量。输出:
[顶层] 返回时: MY_VAR='cache_value' (缓存)
4.2.示例2——普通变量的遮盖效应会传递到子作用域
那我在上一个例子的基础上再加一个 孙子目录,让你清楚看到:
普通变量的遮盖效应会传递到子作用域(子目录、孙子目录),
但是一旦跳出当前作用域,就恢复为缓存变量。
📂 目录结构
demo/
├── CMakeLists.txt
└── sub/
├── CMakeLists.txt
└── subsub/
└── CMakeLists.txt
🔹 顶层 demo/CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(ShadowCacheDemo)
# 定义一个缓存变量
set(MY_VAR "cache_value" CACHE STRING "缓存变量 MY_VAR")
message("[顶层] 初始: MY_VAR='${MY_VAR}' (缓存)")
# 进入子目录
add_subdirectory(sub)
# 回到顶层后,再次读取
message("[顶层] 返回时: MY_VAR='${MY_VAR}' (缓存)")
🔹 子目录 demo/sub/CMakeLists.txt
# 子目录开始
message("[子目录] 进入时: MY_VAR='${MY_VAR}' (继承自缓存)")
# 定义同名普通变量 → 遮盖缓存变量
set(MY_VAR "normal_value")
message("[子目录] 设置普通变量后: MY_VAR='${MY_VAR}' (普通变量覆盖缓存)")
message("[子目录] 显式访问缓存: $CACHE{MY_VAR}")
# 进入孙子目录
add_subdirectory(subsub)
# 回到子目录后
message("[子目录] 返回时: MY_VAR='${MY_VAR}' (普通变量还在遮盖缓存)")
🔹 孙子目录 demo/sub/subsub/CMakeLists.txt
# 孙子目录开始
message("[孙子目录] 进入时: MY_VAR='${MY_VAR}' (继承了子目录的普通变量覆盖)")
message("[孙子目录] 显式访问缓存: $CACHE{MY_VAR}")
我们可以直接复制下面这个代码去一键构建出目录结构和文件
mkdir -p demo/sub/subsub && cat > demo/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.15)
project(ShadowCacheDemo)
set(MY_VAR "cache_value" CACHE STRING "缓存变量 MY_VAR")
message("[顶层] 初始: MY_VAR='${MY_VAR}' (缓存)")
add_subdirectory(sub)
message("[顶层] 返回时: MY_VAR='${MY_VAR}' (缓存)")
EOF
cat > demo/sub/CMakeLists.txt <<'EOF'
message("[子目录] 进入时: MY_VAR='${MY_VAR}' (继承自缓存)")
set(MY_VAR "normal_value")
message("[子目录] 设置普通变量后: MY_VAR='${MY_VAR}' (普通变量覆盖缓存)")
message("[子目录] 显式访问缓存: $CACHE{MY_VAR}")
add_subdirectory(subsub)
message("[子目录] 返回时: MY_VAR='${MY_VAR}' (普通变量还在遮盖缓存)")
EOF
cat > demo/sub/subsub/CMakeLists.txt <<'EOF'
message("[孙子目录] 进入时: MY_VAR='${MY_VAR}' (继承了子目录的普通变量覆盖)")
message("[孙子目录] 显式访问缓存: $CACHE{MY_VAR}")
EOF
接下来我们就来构建这个项目
mkdir build && cd build && cmake ..
✅ 这样你就能清楚看到:
子目录定义的普通变量会传递到孙子目录,继续遮盖缓存变量。
但是回到顶层时,普通变量作用域消失,重新读取的是缓存值。