CMake 策略 CMP0126 深度解析:`set(CACHE)` 不再清除同名普通变量

原创2026-10-09 18:11:341,766 阅读
文章标签:构建工具开发工具CLI

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 行为在以下三种情况会移除当前作用域中同名的普通变量:

  1. 此前不存在任何同名缓存变量(首次写入缓存);
  2. 此前存在同名缓存变量,但该缓存变量没有类型(type)。典型场景是命令行以 cmake -DMYVAR=blah 形式传入变量——这种形式创建的缓存条目类型是 UNINITIALIZED;相比之下 cmake -DMYVAR:STRING=blah 才会显式指定 STRING 类型;
  3. 写入时使用了 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)

迁移注意事项

  1. OLD 行为已被弃用:策略文档末尾的标准弃用声明(Help/policy/include/DEPRECATED.rst)明确,策略的 OLD 行为"按定义弃用",可能在未来的 CMake 版本中被移除。长期维护的项目应尽早迁移到 NEW 行为。
  2. 排查依赖 OLD 行为的代码:如果项目里存在"先 set(VAR val),再 set(VAR ... CACHE ... [FORCE]),随后又读取 VAR 期望拿到缓存值"的模式,在 NEW 行为下读取到的将是普通变量的旧值而非缓存新值——这类写法是迁移时最需要警惕的隐性行为变化。
  3. 关注与 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 行为。

登录后查看全文
CMake