CMake 策略 CMP0019 深度解析:include 与 link 信息中的变量不再二次展开
CMake 策略 CMP0019 深度解析:include 与 link 信息中的变量不再二次展开
导读
CMP0019 是 CMake 于 2.8.11 引入、并在 4.0 版本中移除的一条历史兼容策略,核心内容是:不再对 include_directories、link_directories、link_libraries 命令接收的值做"二次展开"(re-expand)。本文将以该策略的官方文档为骨架,结合仓库中的策略注册表与 RunCMake 测试用例,说明它要解决的问题、OLD/NEW 两种行为的具体差异,以及它在现代 CMake 工程中的实际意义与迁移要点。
策略要解决的历史问题:配置阶段末尾的"二次求值"
在 CMake 2.8.10 及更早版本中,CMake 语言处理(language processing)阶段完成之后,还会对传给以下三个命令的值做一次额外的变量引用展开:
include_directorieslink_directorieslink_libraries
也就是说,即使你在脚本中把变量引用转义成了 \${VAR} 这种形式,旧版 CMake 仍会在整个配置阶段的最后一步把它们再次求值,导致转义失效。这一行为是为"非常早期的 CMake 版本"(VERY early CMake versions)做严格兼容而保留的——因为在正常情况下,所有变量引用都应当在 CMake 语言处理阶段就被求值完毕,末尾的这次额外求值本不该存在。
从 CMake 2.8.11 开始,官方认为这次多余的求值应当被移除,因此引入 CMP0019 作为过渡策略:
- OLD 行为:对这些值进行再求值,以严格兼容旧版本的行为;
- NEW 行为:让这些值保持原样(leave the values untouched),不做二次展开。
从源码看策略的注册与状态
CMP0019 在源码中的注册位于 Source/cmPolicies.h 的策略表中:
SELECT(POLICY, CMP0019,
"Do not re-expand variables in include and link information.", 2, 8,
11, NEW)
这段注册信息说明:
- 策略全名为
CMP0019,对应描述即为 "Do not re-expand variables in include and link information"; - 引入版本为 2.8.11(表格中第 4~6 个参数依次为 major、minor、patch);
- 默认状态为 NEW(第 7 个参数)。按照 Source/cmPolicies.h 的注释约定:
NEW状态表示该策略已被移除(removed policies),即现代 CMake 中该策略始终按 NEW 语义生效。
这也与策略文档中 REMOVED_EPILOGUE 模板(见 Help/policy/include/REMOVED_EPILOGUE.rst)的表述一致:该策略在 CMake 2.8.11 引入,在 4.0 版本移除;在移除之前,它可以通过 cmake_policy 或 cmake_minimum_required 设置,若未显式设置,CMake 会给出警告并回退到 OLD 行为(警告来源见 Help/policy/include/REMOVED_PROLOGUE.rst,其中声明 OLD 行为已在 CMake 4.0 中被删除)。
用 RunCMake 测试理解 NEW 行为的具体表现
仓库中保留了专门针对该策略的回归测试,目录为 Tests/RunCMake/CMP0019/。测试用例文件 CMP0019-code.cmake 非常直观地展示了"被转义的变量引用"场景:
set(VAR_INCLUDE "VAL_INCLUDE")
set(VAR_LINK_DIRS "VAL_LINK_DIRS")
set(VAR_LINK_LIBS "VAL_LINK_LIBS")
add_custom_target(some_target)
include_directories("/usr/include/\${VAR_INCLUDE}" /usr/include/normal)
link_directories("/usr/lib/\${VAR_LINK_DIRS}" /usr/lib/normal)
link_libraries("\${VAR_LINK_LIBS}" normal)
add_custom_target(other_target)
set_property(TARGET other_target PROPERTY INCLUDE_DIRECTORIES "")
测试的入口逻辑为:
- RunCMakeTest.cmake 通过
run_cmake(CMP0019-NEW)执行用例; - CMakeLists.txt 声明
cmake_minimum_required(VERSION 3.10),使该工程处于 CMP0019 的 NEW 语义之下; - CMP0019-NEW.cmake 仅一行
include(CMP0019-code.cmake),加载上述用例脚本。
这段用例刻意用 \${VAR_INCLUDE} 这样的转义形式写入值,正是为了验证:在 NEW 行为下,CMake 不会在配置收尾阶段把这些残留的变量引用再次展开——转义就是转义,值应当"保持原样"。
注意:
include_directories、link_directories、link_libraries属于目录级(非 target 级)命令。在当前仓库源码中,link_directories的实现(见 Source/cmLinkDirectoriesCommand.cxx)会发出 "Use of non-target directives is not recommended" 的诊断,推荐改用target_link_directories等基于 target 的命令——这并不影响本文讨论的展开语义,只是现代工程实践上的一个方向。
实际影响:这到底改变了什么?
要理解 CMP0019 的价值,关键在于区分 CMake 的两个求值时机:
| 阶段 | 旧版(< 2.8.11) | 新版(≥ 2.8.11,NEW 行为) |
|---|---|---|
| CMake 语言处理阶段(脚本执行时) | 正常展开 $VAR 形式的引用 |
正常展开 $VAR 形式的引用(不变) |
| 配置阶段末尾(额外步骤) | 对 include_directories / link_directories / link_libraries 的值再次展开残留的 \${VAR} |
不再做任何二次求值 |
旧行为带来的后果是:转义失效。开发者若出于某些原因(如路径中确实包含 $ 字符、或希望把展开推迟到生成器表达式阶段)写了 \${VAR},旧版 CMake 依然会在最后一步强行展开,造成路径被错误替换。CMP0019 的 NEW 行为保证了:脚本中写什么,目录/链接信息中就保存什么,行为可预期、可控制。
工程中的设置与升级要点
虽然 CMP0019 在 CMake 4.0 已彻底移除(OLD 行为被删除),但在需要维护旧版 CMake 兼容性的工程中,理解其设置方式仍然重要:
# 方式一:通过 cmake_minimum_required 统一设定(推荐)
cmake_minimum_required(VERSION 2.8.11)
# 方式二:在任意位置显式声明策略
cmake_policy(SET CMP0019 NEW) # 不再二次展开,保留转义值
cmake_policy(SET CMP0019 OLD) # 严格兼容旧版,配置末尾再次求值(2.8.11~3.x 可用)
需要留意的是:
cmake_minimum_required与cmake_policy是设置该策略仅有的两条途径(参见 Help/policy/include/REMOVED_EPILOGUE.rst);- 在 2.8.11 到 3.x 的版本区间内,若未显式设置,CMake 会打印警告并默认采用 OLD 行为;
- 从 CMake 4.0 起,策略的 OLD 行为已被删除(见 Help/policy/include/REMOVED_PROLOGUE.rst),必须通过
cmake_minimum_required或cmake_policy将其置于 NEW 状态,换言之:现代工程中该策略永远按 NEW 语义运行,二次展开已彻底成为历史行为。
迁移建议
对工程维护者来说,CMP0019 的启示可以归纳为三点:
- 升级 CMake 版本时不要恐慌:把
cmake_minimum_required提升到 2.8.11 及以上即可自动获得 NEW 行为;由于默认状态就是 NEW,绝大多数工程不会感知到差异; - 检查是否存在"依赖二次展开"的写法:如果旧工程中曾利用
include_directories("/path/\${X}")这种形式依赖配置末尾的再求值,升级后必须改为在脚本中显式展开(如先set(...)再传入,或去掉转义),否则路径会保持\${X}字面量导致编译/链接找不到路径; - 优先使用基于 target 的新式命令:如
target_include_directories、target_link_directories、target_link_libraries,它们天然不存在目录级命令的历史展开语义,配合生成器表达式($<...>)可以更精确地控制求值时机,也是当前仓库源码所推荐的方向。
总结
CMP0019 是一条典型的"历史包袱清理"策略:它终结了早期 CMake 在配置阶段末尾对 include/link 信息做二次变量求值的行为,使转义语义重新变得可靠。虽然它在 CMake 4.0 中已被移除,但通过策略注册表 Source/cmPolicies.h 与回归测试 Tests/RunCMake/CMP0019/,我们依然能完整还原这段兼容史,并从中提炼出"变量求值时机必须单一、可预期"这一贯穿 CMake 设计始终的原则。