CMake 缓存机制完全指南:从原理到企业级防御性写法

前言

如果你曾经被下面任意一种现象折磨过,就说明你已经被 CMake 缓存机制“咬”过:

  • 第三方库的某个开关怎么关都关不掉
  • CI 里加了 -DENABLE_XXX=OFF,本地却还是 ON
  • cmake-gui 里改了某个选项,下次重新 configure 又变回来了
  • 同一个变量名在不同子模块里定义了不同的默认值,最后到底是哪个生效了?

这篇文章将一次性把 CMake 缓存的完整模型 讲透,读完后你将彻底掌握所有配置交互的底层规则。

1. CMake 缓存到底是什么?

CMake 缓存 = 构建目录下的 CMakeCache.txt 文件
它是一个全局、持久化、有类型、有描述、有优先级的键值数据库。

所有你见过的配置方式,最终都会收敛到这个文件:

-D 参数
cmake-gui / ccmake
CMakePresets.json
set(... CACHE ...)
option(...)

2. 缓存变量的 6 种类型(TYPE)及其真实表现

TYPE CMakeCache.txt 显示 是否在 GUI 显示 典型用途
BOOL BOOL 是(复选框) 开关(最常用)
STRING STRING 是 版本号、模式枚举
PATH PATH 是(带浏览按钮) 目录路径
FILEPATH FILEPATH 是(带浏览按钮) 文件路径
INTERNAL INTERNAL 隐藏 CMake 内部使用,不希望用户看到
UNINITIALIZED (无 TYPE) 是 没写 TYPE 时的默认(不推荐)

推荐:对外暴露的开关一律用 BOOL,路径用 PATH,永远不要偷懒留空。

3. 缓存变量优先级铁律(从高到低,背下来就无敌了)

1. 命令行 -D 参数(最高优先级)
2. CMakePresets.json / CMakeUserPresets.json 中的 cacheVariables
3. 当前 CMakeCache.txt 中已存在的条目(上一次配置的结果)
4. 项目代码中显式的 set(... CACHE ...)(父项目优先于子项目)
5. 项目代码中第一次出现的 option(...) 的默认值
6. 普通的 set(...)(不进缓存,最低优先级)

只要记住这 6 条,你就能准确预测任何情况下变量的最终值。

4. option() 的真实实现(源码级拆解)

# CMake 内置的 option() 实际上长这样(简化版)
if(NOT DEFINED ENABLE_HEAVY_FEATURE)
    set(ENABLE_HEAVY_FEATURE ON CACHE BOOL "description" FORCE)
endif()

结论:一旦变量已经被定义过(无论来自 -D、CACHE、父项目),option() 的默认值就完全失效。

这正是我们“接管第三方开关”的底层原理。

5. FORCE 关键字的恐怖威力

set(MY_VAR OFF CACHE BOOL "" FORCE)   # 强制覆盖缓存中已有值

加了 FORCE 后,连命令行 -D 都压不住(除非命令行也加 FORCE)。
企业项目里只在极少数“必须锁死”的地方才用,例如:

set(CMAKE_POSITION_INDEPENDENT_CODE ON CACHE BOOL "" FORCE)

6. 企业级防御性写法大全(2025 年标配)

# cmake/options-thirdparty.cmake
# 所有第三方开关统一在这里接管,子模块的 option() 全部失效

set(ENABLE_HEAVY_FEATURE         OFF CACHE BOOL "[3rdParty] Heavy legacy mode (+6 min build)")
set(ENABLE_EXPERIMENTAL_DRIVER   ON 开启 CACHE BOOL "[3rdParty] Next-gen driver (unstable)")
set(THIRD_PARTY_ROOT             ""   CACHE PATH  "[3rdParty] Root directory of all external libs")

# 顶级 CMakeLists.txt 最前面
include(cmake/options-thirdparty.cmake)

这样做的好处:

  • 所有开关集中一处管理
  • GUI 里自动分组显示
  • CI 可以用 -D 轻松覆盖
  • 杜绝“有人偷偷打开了我不知道的开关”

7. 实战最常见的 5 种控制方式对比

方式 优先级 是否推荐 适用场景
命令行 -DENABLE_XXX=OFF 1 强烈推荐 CI 多配置构建
父项目 set(... CACHE ...) 4 强烈推荐 统一接管第三方默认行为
CMakePresets.json cacheVariables 2 强烈推荐 团队统一配置、IDE 集成
子模块自己的 option() 5 基本没用 只能在完全独立项目时才有效
普通 set(ENABLE_XXX ON)(无 CACHE) 6 禁用 会被 option() 覆盖,极易出错

8. 让 GUI 更好看的终极技巧

set(MYCOMPANY_ENABLE_DEBUG_LOG OFF CACHE BOOL
    "[MyCompany] Enable verbose debug logging (very noisy)")

set(MYCOMPANY_THIRD_PARTY_ROOT "" CACHE PATH
    "[MyCompany] Root directory of all third-party libraries")

在 cmake-gui/ccmake 中会自动按 [方括号] 分组,体验直接起飞。

9. 总结:一句话彻底掌握 CMake 缓存

谁最后(优先级最高)往 CMakeCache.txt 里写,谁就赢。

掌握了这套模型,你就拥有了:

  • 完美接管任何第三方库的开关
  • 让 CI、开发者、GUI、Presets 行为完全一致
  • 再也不用猜“这个变量到底是哪来的”

去把你们项目里那些“永远关不掉的开关”全部用 set(... CACHE ...) 接管了吧!