15、预设与缓存:CMakePresets.json配置、缓存变量、option()命令、高级缓存编辑

说实话,CMake 的缓存机制是我早期最头疼的部分。明明改了个变量,重新构建时它还是老样子。后来我才明白——缓存这东西,就像程序员的短期记忆,你不主动刷新,它就记着旧数据。

这一章,我们来彻底搞懂 CMake 的缓存体系。包括预设文件怎么用、缓存变量怎么玩、option() 命令的坑,以及一些高级编辑技巧。

15.1 CMakePresets.json:项目配置的“快捷方式”

你有没有遇到过这种情况?一个新同事加入项目,光配置 CMake 就花了半天。各种 -D 参数、构建类型、工具链路径……每次都要敲一长串命令。

CMakePresets.json 就是来解决这个问题的。它把常用的配置选项打包成“预设”,一键切换。

15.1.1 预设文件的结构

一个典型的 CMakePresets.json 长这样:

{
  "version": 3,
  "configurePresets": [
    {
      "name": "default",
      "displayName": "默认配置",
      "description": "Debug 模式,使用 Ninja",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/default",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug",
        "CMAKE_CXX_STANDARD": "17"
      }
    },
    {
      "name": "release",
      "displayName": "发布配置",
      "description": "Release 模式,优化全开",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/release",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Release",
        "CMAKE_CXX_FLAGS": "-O3 -march=native"
      }
    }
  ],
  "buildPresets": [
    {
      "name": "default",
      "configurePreset": "default"
    }
  ]
}

我个人习惯把预设文件放在项目根目录。这样团队成员 clone 下来就能直接用 cmake --preset=default 开始构建。

小技巧: 预设文件支持变量展开,比如 ${sourceDir} 会自动替换成源码路径。你还可以用 ${userHome} 引用用户目录。

15.1.2 预设的继承与覆盖

预设可以互相继承。比如你有一个基础预设,然后在此基础上微调:

{
  "configurePresets": [
    {
      "name": "base",
      "hidden": true,
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/${presetName}"
    },
    {
      "name": "debug",
      "inherits": "base",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug"
      }
    },
    {
      "name": "asan",
      "inherits": "debug",
      "cacheVariables": {
        "CMAKE_CXX_FLAGS": "-fsanitize=address"
      }
    }
  ]
}

这里 base 预设设置了 hidden: true,它不会出现在预设列表中,只作为模板使用。我经常用这种模式来组织不同平台的配置——Windows 一套基础,Linux 一套基础,然后各自派生。

注意: 预设文件中的路径尽量使用相对路径或变量。我曾经见过有人把绝对路径写死在预设里,结果换台机器就崩了。

15.2 缓存变量:CMake 的“记忆系统”

缓存变量是 CMake 在第一次运行 cmake 命令时生成的。它们保存在 CMakeCache.txt 文件中。下次再运行 cmake,它会先读这个文件,而不是重新计算。

为什么会这样?因为有些信息一旦确定就不需要变了。比如编译器路径、安装前缀、第三方库的位置。每次都重新检测太慢,缓存就是用来加速的。

15.2.1 缓存变量的定义方式

在 CMakeLists.txt 里,你可以用 set() 命令定义缓存变量:

set(MY_VARIABLE "hello" CACHE STRING "这是一个缓存变量")

注意 CACHE 关键字。没有它,变量就是普通变量,只在当前 CMakeLists.txt 作用域有效。加上 CACHE,它就会被写入 CMakeCache.txt

缓存变量有类型:

  • STRING:字符串,最常用
  • BOOL:布尔值,ON/OFF
  • FILEPATH:文件路径
  • PATH:目录路径
  • INTERNAL:内部变量,不显示在 cmake-gui 中

我个人建议:凡是需要用户配置的选项,都用缓存变量。比如第三方库的路径、安装目录等。

15.2.2 缓存变量的优先级

这里有个容易踩坑的地方。缓存变量、普通变量、命令行参数,它们之间有优先级:

  1. 命令行 -D 参数优先级最高
  2. 缓存变量次之
  3. 普通变量优先级最低

举个例子:

# CMakeLists.txt
set(MY_VAR "默认值" CACHE STRING "")
message("MY_VAR = ${MY_VAR}")

如果你第一次运行 cmake -DMY_VAR=命令行值 ..,输出是 MY_VAR = 命令行值。第二次运行 cmake ..(不加 -D),输出还是 MY_VAR = 命令行值。因为缓存记住了。

嗯,这里要注意:如果你想重置缓存变量,要么删掉 CMakeCache.txt,要么用 -D 重新赋值。

15.3 option() 命令:开关的艺术

option() 是缓存变量的语法糖,专门用来定义布尔开关:

option(USE_OPENCV "是否启用 OpenCV 支持" ON)

它等价于:

set(USE_OPENCV ON CACHE BOOL "是否启用 OpenCV 支持")

option() 有个特殊行为:如果变量已经在缓存中,它不会覆盖。也就是说,用户第一次设置后,后续的 option() 调用不会改变它的值。

我曾经在项目中遇到过一个问题:某个库的 option() 默认是 ON,但用户想关闭它。结果用户改了缓存,但代码里又用 set() 强制覆盖了……嗯,那场面挺混乱的。

最佳实践:option() 定义用户可配置的开关,用 set(... CACHE BOOL ...) 定义内部开关。前者尊重用户选择,后者可以强制覆盖。

15.4 高级缓存编辑技巧

除了基本的 set()option(),还有一些高级玩法。

15.4.1 强制更新缓存

有时候你需要强制更新缓存变量,即使它已经存在。可以用 FORCE 关键字:

set(MY_VAR "新值" CACHE STRING "" FORCE)

这会覆盖缓存中的旧值。我一般只在初始化脚本或工具链文件中用这个,普通 CMakeLists.txt 里尽量少用——容易让用户困惑。

15.4.2 缓存变量的高级类型

除了基本类型,CMake 还支持 STRINGS 属性,用来限制可选值:

set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "构建类型")
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug;Release;RelWithDebInfo;MinSizeRel")

这样在 cmake-gui 中,用户只能从下拉列表中选择,不能乱填。我特别喜欢这个特性,尤其是管理编译选项时。

15.4.3 缓存变量的依赖关系

有时候一个缓存变量的值依赖于另一个。比如:

option(ENABLE_SSL "启用 SSL" OFF)
if(ENABLE_SSL)
  set(OPENSSL_ROOT_DIR "" CACHE PATH "OpenSSL 安装路径")
endif()

但这里有个问题:如果用户先关闭 SSL,再打开,OPENSSL_ROOT_DIR 可能还是空的。我建议用 cmake_dependent_option() 来处理这种依赖:

include(CMakeDependentOption)
cmake_dependent_option(ENABLE_SSL "启用 SSL" OFF
  "NOT WIN32" ON)

这个命令来自 CMakeDependentOption 模块。它可以根据条件自动启用或禁用选项。我在跨平台项目中经常用它——Windows 上默认关闭某些功能,Linux 上默认打开。

15.5 本章知识体系

下面这张图总结了预设、缓存和选项之间的关系:

CMake 预设与缓存体系 CMakePresets.json 配置预设 构建预设 测试预设 生成 CMakeCache.txt 缓存变量 类型:STRING/BOOL/... 优先级:-D > 缓存 > 默认 读取 option() 命令 布尔开关 语法糖 不覆盖已有缓存 高级缓存编辑技巧 FORCE 强制更新 STRINGS 属性限制可选值 cmake_dependent_option() 依赖管理

从图中可以看出:预设文件生成缓存,缓存被 option() 读取,而高级技巧则是对缓存进行精细控制。这三者构成了 CMake 配置管理的核心。

15.6 避坑指南

最后分享几个我踩过的坑:

  • 缓存不更新:修改了 CMakeLists.txt 但缓存没变?试试 cmake --fresh 强制重新生成缓存。
  • 预设路径问题:预设中的路径尽量用 ${sourceDir} 而不是 .,否则在不同目录执行 cmake 时可能出错。
  • option() 的陷阱:如果你在子目录中用 option(),它只在那个子目录作用域有效。想全局生效?用 set(... CACHE BOOL ...) 代替。
  • 缓存文件冲突:多人协作时,不要把 CMakeCache.txt 提交到版本控制。每个开发者的环境不同,缓存应该本地生成。

嗯,缓存这东西,说简单也简单,说复杂也复杂。但只要你理解了它的“记忆”机制,就能驾驭它。记住:缓存是你的朋友,不是敌人。


公众号:蓝海资料掘金营,微信 deep3321