CMake 策略 CMP0126 深度解析:`set(CACHE)` 不再清除同名普通变量
CMake 策略 CMP0126 深度解析:set(CACHE) 不再清除同名普通变量
CMP0126 是 CMake 3.21 引入的兼容性策略,它改变了 set(<var> ... CACHE ...) 命令对同名普通(normal)变量的处理方式:在 NEW 行为下,写入缓存变量时不再把当前作用域中同名的普通变量一并移除。本文以该策略文档为主体,结合 CMake 源码实现(Source/cmMakefile.cxx、Source/cmSetCommand.cxx、Source/cmOptionCommand.cxx)与官方回归测试(Tests/RunCMake/CMP0126),完整讲解 OLD/NEW 两种行为的触发条件、与 CMP0077 的关键差异、警告控制方式以及迁移到 NEW 行为时的注意事项。
一、策略概述:这条策略到底在管什么
在 CMake 中,变量分为两种存储位置:普通变量(normal variable),属于当前作用域(目录、函数、块),以及缓存变量(cache variable),全局持久化存储于 CMakeCache.txt。两者名字可以相同,查找顺序是普通变量优先于缓存变量。
set() 命令的 CACHE 签名形如:
set(<var> <value> CACHE <type> <docstring> [FORCE])
它的作用是把 <var> 写入缓存。而 CMP0126 策略文档(Help/policy/CMP0126.rst)明确规定:
当该策略设为
NEW时,set(CACHE)命令不会从当前作用域移除任何同名普通变量;OLD行为则会移除。
换句话说,OLD 行为下,写入缓存变量的那一刻,当前作用域里同名的普通变量会"凭空消失";NEW 行为下两者可以和平共处。该策略的引入正是为了让"写缓存"和"改普通变量"两件事在语义上解耦,避免 set(VAR 1) 之后再 set(VAR 2 CACHE STRING "") 时,前面的普通变量 VAR 被意外清除。
二、OLD 行为:什么情况下会移除普通变量
根据策略文档,OLD 行为在以下三种情况会移除当前作用域中同名的普通变量:
- 此前不存在任何同名缓存变量(首次写入缓存);
- 此前存在同名缓存变量,但该缓存变量没有类型(type)。典型场景是命令行以
cmake -DMYVAR=blah形式传入变量——这种形式创建的缓存条目类型是UNINITIALIZED;相比之下cmake -DMYVAR:STRING=blah才会显式指定STRING类型; - 写入时使用了
FORCE或INTERNAL关键字。FORCE表示无条件覆盖缓存值,INTERNAL表示内部专用条目(不显示在cmake-gui中),两者都会强制改写缓存,同时带走同名的普通变量。
对照源码,cmMakefile::AddCacheDefinition(Source/cmMakefile.cxx#L1968-L2018)中对策略状态的判定如下:
switch (this->GetPolicyStatus(cmPolicies::CMP0126)) {
case cmPolicies::WARN:
if (this->PolicyOptionalWarningEnabled("CMAKE_POLICY_WARNING_CMP0126") &&
this->IsNormalDefinitionSet(name)) {
this->IssuePolicyWarning(
cmPolicies::CMP0126, {},
cmStrCat("For compatibility with older versions of CMake, "
"normal variable \"", name, "\" will be removed "
"from the current scope."_s));
}
CM_FALLTHROUGH;
case cmPolicies::OLD:
// if there was a definition then remove it
this->StateSnapshot.RemoveDefinition(name);
break;
case cmPolicies::NEW:
break;
}
可以看到实现非常直接:WARN 状态(策略未显式设置时的默认状态)下,如果 CMAKE_POLICY_WARNING_CMP0126 被开启且确有同名普通变量,则发出告警并继续走 OLD 逻辑移除该普通变量;OLD 状态下通过 StateSnapshot.RemoveDefinition(name) 直接删除普通变量定义;NEW 状态下什么都不做。
三、NEW 行为:普通变量与缓存变量共存
将策略设为 NEW 后:
cmake_policy(SET CMP0126 NEW)
set(VAR 1) # 定义普通变量
set(VAR 2 CACHE STRING "") # 写入缓存,不再清除普通变量
message("${VAR}") # 输出 1,普通变量仍在
官方回归测试 Tests/RunCMake/CMP0126/CMP0126-NEW.cmake 验证的正是这一点:
cmake_policy(SET CMP0126 NEW)
set(VAR 1)
set(VAR 2 CACHE STRING "")
if (NOT VAR EQUAL 1)
message(FATAL_ERROR "normal variable does not exist anymore.")
endif()
而 CMP0126-OLD.cmake 中同样的写法,OLD 行为下普通变量已被清除,if (VAR EQUAL 1) 成立便会触发 FATAL_ERROR——两个测试文件一正一反,恰好把新旧行为钉死在回归用例里。
测试文件还覆盖了第三种 OLD 触发场景(FORCE + 无类型缓存条目):先通过 set_property(CACHE VAR PROPERTY TYPE UNINITIALIZED) 把缓存条目类型置为空,再调用 find_file(VAR ...)。此时 find_file 会同时设置缓存变量与普通变量,在 OLD 行为下普通变量同样被移除,而 NEW 行为下普通变量被保留(配合 CMP0125 保证 find_* 行为可预期)。
四、与 CMP0077 的关键区别:set(CACHE) vs option()
CMP0126 的 NEW 行为与 CMP0077(option() 是否尊重普通变量)的 NEW 行为表面相似,但有一条重要差异,策略文档专门强调了这一点:
set(CACHE)命令:如果缓存变量此前不存在,无论 CMP0126 取何值,它总是会创建该缓存变量;option()命令:如果同名的非缓存变量已经存在,且 CMP0077 设为 NEW,则option()不会设置缓存变量。
换句话说,CMP0126 只决定"写缓存时是否顺带删普通变量",而 CMP0077 决定"同名普通变量存在时是否还写缓存"——两者作用点不同,不能混为一谈。
在源码层面,option() 的实现(Source/cmOptionCommand.cxx#L42-L76)在 CMP0077 为 NEW 时,一旦发现同名普通变量存在就直接 return true 跳过缓存写入:
case cmPolicies::NEW: {
// See if a local variable with this name already exists.
// If so we ignore the option command.
if (existsBeforeSet) {
return true;
}
} break;
而 set() 的 CACHE 路径(Source/cmSetCommand.cxx#L230-L267)则会先检查缓存中是否已有带类型的条目,仅当存在且未使用 FORCE/INTERNAL 时才提前返回,否则一定会调用 AddCacheDefinition 写入缓存:
cmValue existingValue = state->GetCacheEntryValue(variable);
if (existingValue &&
(state->GetCacheEntryType(variable) != cmStateEnums::UNINITIALIZED)) {
if (cache && type != cmStateEnums::INTERNAL && !force) {
return true;
}
}
这也是文档所述"命令行无类型变量(UNINITIALIZED)会被视为不存在"的代码来源:UNINITIALIZED 类型的缓存条目不会触发提前返回,从而为 OLD 行为下的普通变量移除创造了条件。
另外值得留意的是 option() 内部同样引用了 CMP0126(Source/cmOptionCommand.cxx#L70-L76):当 CMP0077 不是 NEW、而 CMP0126 是 NEW 时,option() 写入缓存后仍会显式移除同名普通变量,以保持兼容路径下的历史语义。可见 CMP0126 的影响范围并不局限于 set(CACHE),也辐射到了基于缓存写入的其他命令。
五、警告控制:默认不告警,需显式开启
策略文档与标准建议段落(Help/policy/include/STANDARD_ADVICE.rst)说明:
- 该策略引入于 CMake 3.21;
- 可通过
cmake_policy或cmake_minimum_required设置; - 若未设置,CMake 默认不发出警告,并使用
OLD行为。
对于 CMP0126,默认不告警意味着:即便项目命中 OLD 行为下的变量移除场景,配置过程也默不作声。如需显式观察告警,可通过 CMAKE_POLICY_WARNING_CMP0126 变量开启:
set(CMAKE_POLICY_WARNING_CMP0126 1)
set(MY_VAR 1)
set(MY_VAR 2 CACHE STRING "") # 此时会产生策略告警
这正是回归测试 CMP0126-WARN.cmake 的写法,其预期输出(CMP0126-WARN-stderr.txt)为:
For compatibility with older versions of CMake, normal variable "MY_VAR"
will be removed from the current scope.
源码中 PolicyOptionalWarningEnabled("CMAKE_POLICY_WARNING_CMP0126") 的判定(Source/cmMakefile.cxx#L2002)与策略注册表(Source/cmPolicies.h#L373-L375,状态为 WARN)共同构成了这条告警链路。
六、如何设置与迁移建议
设置方式
方式一:项目内显式设置
# 推荐放在 cmake_minimum_required 之后尽早执行
cmake_policy(SET CMP0126 NEW)
方式二:通过 cmake_minimum_required 隐式继承
由于 3.21 及更高版本会把 cmake_minimum_required(VERSION 3.21) 及以上的项目自动视为该策略为 NEW,大多数新项目无需手动设置:
cmake_minimum_required(VERSION 3.21) # CMP0126 自动为 NEW
方式三:为第三方子项目指定策略
若项目包含无法直接修改的第三方子目录,可用 CMAKE_POLICY_DEFAULT_CMP0126 变量为其预设策略,而不必改动其源码:
# 在 add_subdirectory 之前设置
set(CMAKE_POLICY_DEFAULT_CMP0126 NEW)
迁移注意事项
- OLD 行为已被弃用:策略文档末尾的标准弃用声明(Help/policy/include/DEPRECATED.rst)明确,策略的
OLD行为"按定义弃用",可能在未来的 CMake 版本中被移除。长期维护的项目应尽早迁移到 NEW 行为。 - 排查依赖 OLD 行为的代码:如果项目里存在"先
set(VAR val),再set(VAR ... CACHE ... [FORCE]),随后又读取VAR期望拿到缓存值"的模式,在 NEW 行为下读取到的将是普通变量的旧值而非缓存新值——这类写法是迁移时最需要警惕的隐性行为变化。 - 关注与
find_*、option()的联动:如第四节所述,CMP0126 与 CMP0077、CMP0125 的策略行为相互交织。迁移时建议组合阅读 CMP0077、CMP0125 文档,并在切换策略后运行ctest或对应模块的回归测试验证结果一致性。
七、小结
CMP0126 是一次小而关键的语义修正:它把"设置缓存变量"与"清除同名普通变量"两件事彻底解耦,消除了 set(CACHE) 对当前作用域的隐性副作用。理解它需要抓住三个要点:OLD 行为的三种触发场景(首次写入、无类型缓存条目、FORCE/INTERNAL)、与 CMP0077 在"是否写入缓存"上的本质区别,以及"默认不告警、需通过 CMAKE_POLICY_WARNING_CMP0126 显式开启"的告警机制。借助 cmMakefile.cxx 的 AddCacheDefinition 实现与 Tests/RunCMake/CMP0126 的 NEW/OLD/WARN 三组回归用例,开发者可以清晰地验证并安全地迁移到 NEW 行为。