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 ...) 接管了吧!