1. CMake Presets 初探:为什么需要 Presets?

说实话,我刚开始用 CMake 那几年,最头疼的事就是记命令行参数。每次换项目、换编译器、或者换到同事的机器上构建,都得翻半天文档,或者从历史记录里扒拉命令。你想想看,一个简单的构建命令,动不动就十几行:

cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \
      -DCMAKE_CXX_COMPILER=g++-11 \
      -DCMAKE_CXX_FLAGS="-O3 -march=native" \
      -DBUILD_TESTING=ON \
      -DUSE_OPENMP=ON \
      -DCMAKE_INSTALL_PREFIX=/opt/myapp

这还只是 Release 模式。Debug 模式呢?静态分析呢?覆盖率构建呢?每个配置都得来一套类似的参数。我在团队里见过太多次了——有人把 Debug 和 Release 的构建目录搞混,有人漏传关键 flag,还有人干脆把参数写死在 CMakeLists.txt 里,导致不同平台编译不过。

嗯,这就是传统 CMake 构建的痛点。说白了,CMake 本身是个好工具,但它把「配置」这件事完全丢给了开发者。你每次构建都得手动告诉它:用什么编译器、开什么选项、输出到哪。这就像你每天上班都得重新告诉同事你的名字——明明昨天已经说过了。

传统 CMake 构建的四大痛点

我根据自己的项目经验,把这些问题归纳成四类。你看看是不是也遇到过:

痛点 具体表现 后果
参数冗余 每次构建都要重复输入 -D 参数、编译器路径、构建类型 容易出错,团队协作时难以统一
环境依赖 构建参数硬编码在脚本或 CI 配置中,换台机器就失效 新人上手慢,环境迁移成本高
配置分散 构建参数散落在 Makefile、shell 脚本、CI 配置文件、IDE 设置里 维护困难,改一个参数要改多个地方
可复现性差 没人能说清某个构建到底用了哪些参数 调试困难,问题难以复现

核心矛盾:CMake 本身是声明式的构建系统,但它的「配置」环节却是命令式的。Presets 要解决的,正是这个「声明式配置」的缺失。

Presets 解决了什么?

CMake Presets 是 CMake 3.19 引入的特性(3.20 完善,3.21 稳定)。它的思路很简单:把构建配置写成 JSON 文件,放在项目根目录下。这样,所有开发者、CI 系统、IDE 都能共享同一套配置。

我个人习惯把 Presets 理解为「构建配置的声明式快照」。你只需要定义一次,之后构建就变成这样:

# 以前
cmake -S . -B build/release -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=g++-11 ...

# 现在
cmake --preset release

是不是清爽多了?

Presets 的核心能力

Presets 不是简单地把参数存起来。它提供了一套完整的配置管理机制:

  • 配置预设(Configure Presets):定义 cmake 配置阶段的参数,包括编译器、缓存变量、构建目录等
  • 构建预设(Build Presets):定义 cmake --build 阶段的参数,如并行数、目标名称
  • 测试预设(Test Presets):定义 ctest 的参数,如测试过滤、超时时间
  • 打包预设(Package Presets):定义 cpack 的参数
  • 预设继承:一个预设可以继承另一个预设,避免重复定义
  • 条件预设:根据操作系统、编译器版本等条件自动选择预设

我的经验:刚开始用 Presets 时,别想着一步到位。先定义最常用的 Debug 和 Release 两个配置预设,跑通了再慢慢加。我曾经在一个大项目里一口气写了 20 个预设,结果一半都没人用——白费功夫。

一个简单的 Presets 文件长什么样?

我们来看一个最基础的 CMakePresets.json:

{
    "version": 6,
    "configurePresets": [
        {
            "name": "debug",
            "displayName": "Debug",
            "description": "Debug build with address sanitizer",
            "generator": "Ninja",
            "binaryDir": "${sourceDir}/build/debug",
            "cacheVariables": {
                "CMAKE_BUILD_TYPE": "Debug",
                "CMAKE_CXX_FLAGS": "-fsanitize=address -fno-omit-frame-pointer",
                "BUILD_TESTING": "ON"
            }
        },
        {
            "name": "release",
            "displayName": "Release",
            "description": "Release build with optimizations",
            "generator": "Ninja",
            "binaryDir": "${sourceDir}/build/release",
            "cacheVariables": {
                "CMAKE_BUILD_TYPE": "Release",
                "CMAKE_CXX_FLAGS": "-O3 -march=native",
                "BUILD_TESTING": "OFF"
            }
        }
    ]
}

有了这个文件,团队成员只需要知道预设名字,就能得到完全一致的构建环境。我曾经在一个跨平台项目里,用 Presets 统一了 Windows(MSVC)、macOS(Clang)、Linux(GCC)三套配置,新人上手时间从两天缩短到两小时。

Presets 与传统方式的对比

为了让你更直观地感受差异,我画了一张对比图:

传统方式 vs Presets 方式 传统方式 手动输入 cmake 命令 参数分散在多个脚本中 环境依赖,换机器就失效 难以复现,调试困难 ❌ 效率低,易出错 Presets 方式 定义 CMakePresets.json 配置集中管理 跨平台、跨环境一致 一键构建,可复现 ✅ 高效,标准化

注意:Presets 文件(CMakePresets.json)应该提交到版本控制系统中。但 CMakeUserPresets.json(用户级预设)不要提交——它通常包含本地路径、个人偏好等敏感信息。我曾经见过有人把用户预设提交到仓库,结果所有人的构建目录都变成了同一个绝对路径,乱成一团。

什么时候该用 Presets?

不是所有项目都需要 Presets。我个人觉得,以下情况值得引入:

  • 多人协作的项目:统一团队构建配置,减少「在我机器上能跑」的问题
  • 跨平台项目:Windows/Linux/macOS 各有不同的编译器、工具链、路径
  • 有 CI/CD 的项目:让本地构建和 CI 构建使用完全相同的配置
  • 多配置项目:Debug、Release、Sanitizer、Coverage、ASan 等多种构建类型
  • 开源项目:降低贡献者的上手门槛

如果你的项目只是自己用,或者只有一两个构建配置,那 Presets 带来的收益可能不大。但话说回来,写一次 Presets 文件,以后每次构建都能省下敲命令的时间——这笔账怎么算都划算。

小结

这一章我们聊了传统 CMake 构建的痛点,以及 Presets 是怎么解决这些问题的。说白了,Presets 就是把「怎么构建」这件事从人的脑子里、从散落的脚本里,搬到了一个标准化的 JSON 文件中。它不改变 CMake 的能力,但改变了你使用 CMake 的方式。