第19章 预设与缓存:CMakePresets.json、CMakeCache.txt、预设的继承与覆盖

说实话,CMake 的预设系统是我近几年最喜欢的新特性之一。以前做项目,每次换人接手,第一件事就是问「你 CMake 怎么配置的?」——Debug 还是 Release?编译器路径在哪?第三方库装哪了?这些问题能来回扯半天。

后来 CMake 3.19 引入了 CMakePresets.json,我个人的项目从那时起就全面迁移过来了。今天咱们就聊聊这个预设系统,以及它背后的缓存机制。

19.1 为什么需要预设?

你想想看,一个稍微大点的 C++ 项目,构建配置少说也有十几种:

  • Debug 版要开调试符号,关优化
  • Release 版要开最高优化,关断言
  • 有些模块需要特定编译器
  • 不同平台路径不一样

以前的做法是什么?写个 shell 脚本,或者扔个 CMakeCache.txt 让新人导入。但脚本容易过时,缓存文件又太「黑盒」。

预设就是来解决这个问题的。它把构建配置写成结构化的 JSON 文件,可读、可维护、可继承。

核心思想:把「怎么构建」这件事,从人的脑子里搬到文件里。

19.2 CMakePresets.json 长什么样?

一个典型的 CMakePresets.json 放在项目根目录,结构大概这样:

{
  "version": 6,
  "configurePresets": [
    {
      "name": "default",
      "displayName": "默认配置",
      "description": "适用于大多数开发机的通用配置",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/${presetName}",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug",
        "CMAKE_CXX_STANDARD": "17"
      }
    }
  ],
  "buildPresets": [
    {
      "name": "default",
      "configurePreset": "default"
    }
  ]
}

这里有几个关键点:

  • version:预设格式版本,目前主流是 6
  • configurePresets:配置阶段的预设
  • buildPresets:构建阶段的预设(依赖配置预设)
  • ${sourceDir}${presetName}:CMake 提供的宏变量

我个人习惯把 binaryDir 设为 ${sourceDir}/build/${presetName},这样不同预设的构建产物互不干扰,清理也方便。

19.3 预设的继承与覆盖

这才是预设系统的精髓。我在项目中遇到过这样的场景:

  • 基础配置:用 Ninja 生成器,C++17 标准
  • Debug 配置:继承基础,加调试符号
  • Release 配置:继承基础,开优化
  • Windows 配置:继承 Debug,但生成器换成 Visual Studio

用继承写出来就是:

{
  "version": 6,
  "configurePresets": [
    {
      "name": "base",
      "hidden": true,
      "generator": "Ninja",
      "cacheVariables": {
        "CMAKE_CXX_STANDARD": "17"
      }
    },
    {
      "name": "debug",
      "inherits": "base",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug",
        "CMAKE_CXX_FLAGS": "-g -O0"
      }
    },
    {
      "name": "release",
      "inherits": "base",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Release",
        "CMAKE_CXX_FLAGS": "-O3 -DNDEBUG"
      }
    },
    {
      "name": "debug-win",
      "inherits": "debug",
      "generator": "Visual Studio 17 2022",
      "cacheVariables": {
        "CMAKE_CXX_FLAGS": "/Zi /Od"
      }
    }
  ]
}

注意 "hidden": true——这个预设不会出现在用户的选择列表中,只作为基类存在。你想想看,这跟面向对象编程的继承是不是很像?

覆盖规则:子预设的同名字段会覆盖父预设。如果是 cacheVariables,则是合并覆盖——同名变量子预设胜出,不同名变量两者共存。

19.4 CMakeCache.txt 是什么?

每次你运行 cmake -B build,CMake 都会在构建目录生成一个 CMakeCache.txt。这文件说白了就是 CMake 的「记忆」——它记住了你上次配置的所有变量值。

打开看看,内容大概这样:

# 这是 CMake 的缓存文件
# 不要手动编辑,除非你知道自己在做什么

CMAKE_BUILD_TYPE:STRING=Debug
CMAKE_CXX_COMPILER:FILEPATH=/usr/bin/g++-11
CMAKE_CXX_STANDARD:STRING=17
MY_PROJECT_ENABLE_TESTING:BOOL=ON

每一行格式:变量名:类型=值。类型可以是 STRINGBOOLFILEPATHPATH 等。

我曾经踩过的坑:有次我手动改了 CMakeCache.txt 里的编译器路径,结果 CMake 重新配置时直接报错。后来才发现,缓存文件里的路径格式和 CMake 期望的不完全一致。所以我的建议是:别手改缓存文件,用 cmake -D 或者预设来修改变量。

19.5 预设与缓存的交互

预设和缓存是怎么配合的?流程其实很简单:

  1. 你执行 cmake --preset=debug
  2. CMake 读取 CMakePresets.json,找到 debug 预设
  3. 解析预设中的所有配置项(生成器、变量、路径等)
  4. 将这些配置写入 CMakeCache.txt
  5. 执行实际的配置过程

也就是说,预设是「模板」,缓存是「实例」。预设定义了「应该怎么配」,缓存记录了「实际配了什么」。

这里有个细节:如果你第二次运行同样的预设,CMake 会比较缓存中的值和预设中的值。如果预设变了,缓存会更新;如果缓存被手动改过,CMake 会提示你。

19.6 知识体系总览

下面这张图帮你理清预设和缓存的关系:

CMake 预设与缓存交互流程 CMakePresets.json • 预设定义 • 继承关系 • 变量模板 读取 CMake 配置引擎 • 解析预设 • 合并变量 • 生成缓存 写入 CMakeCache.txt • 变量值 • 编译器路径 • 构建类型 预设继承关系示例 base (hidden) debug release debug-win

19.7 实用技巧与避坑

最后分享几个我实际用下来的经验:

  • 预设文件要提交到版本控制:它是项目的一部分,跟 CMakeLists.txt 一样重要
  • 善用 hidden 基类:把公共配置抽出来,减少重复
  • ${presetName} 做构建目录:不同预设自动分目录,互不干扰
  • 缓存文件不要提交:它是机器生成的,每台机器都不一样

我的小技巧:在 CMakePresets.json 里加一个 ci 预设,专门给 CI 用。这样本地开发和 CI 用同一套配置,再也不用担心「在我机器上能跑」的问题了。

嗯,预设和缓存就聊到这儿。记住一句话:预设是给人看的,缓存是给机器用的。把配置逻辑写清楚,项目维护起来会轻松很多。