预设与配置:CMakePresets.json

说实话,CMake 的配置参数多到让人头疼。-DCMAKE_BUILD_TYPE=Release-DCMAKE_INSTALL_PREFIX=/usr/local……每次都要敲一遍?我刚开始用 CMake 那会儿,经常在终端里翻历史命令,翻得眼花缭乱。后来 CMake 3.19 引入了 CMakePresets.json,我才算真正解脱了。

说白了,预设(Presets)就是一套配置模板。你把常用的编译选项、生成器、环境变量统统写进 JSON 文件里,下次直接 cmake --preset=release 就完事。团队协作时,大家用同一套预设,再也不会出现「我本地能编译,你那边怎么就崩了」的尴尬。

配置预设(Configure Presets)

配置预设是最基础的一类。它定义了 CMake 配置阶段的所有参数。我习惯在项目根目录放一个 CMakePresets.json,结构大概是这样:

{
  "version": 6,
  "configurePresets": [
    {
      "name": "default",
      "displayName": "默认配置",
      "description": "使用 Ninja 生成器,Debug 模式",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/${presetName}",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug",
        "CMAKE_CXX_STANDARD": "17"
      }
    },
    {
      "name": "release",
      "displayName": "发布配置",
      "description": "Release 模式,开启 LTO",
      "inherits": "default",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Release",
        "CMAKE_INTERPROCEDURAL_OPTIMIZATION": "ON"
      }
    }
  ]
}

注意 inherits 字段。你可以让 release 预设继承 default 预设,只覆盖需要改的变量。我在项目中遇到过有人把每个预设都从头写到尾,结果改一个公共变量要改七八个地方——嗯,用继承就清爽多了。

核心字段说明:

  • name:预设的唯一标识,命令行用这个名称
  • generator:指定生成器,比如 Ninja、Visual Studio 17 2022
  • binaryDir:构建目录,支持 ${sourceDir}${presetName} 等变量
  • cacheVariables:等价于命令行 -D 参数
  • environment:设置环境变量,比如 CCCXX

小技巧:${sourceDir}/build/${presetName} 作为 binaryDir,不同预设的构建产物自动隔离。你想想看,debug 和 release 的 .o 文件混在一起有多乱?

构建预设(Build Presets)

配置完,下一步就是编译。构建预设对应 cmake --build 的参数。它可以引用某个配置预设,也可以独立存在。

{
  "buildPresets": [
    {
      "name": "default",
      "configurePreset": "default",
      "jobs": 8,
      "targets": ["my_app"]
    },
    {
      "name": "release",
      "configurePreset": "release",
      "jobs": 8,
      "targets": ["my_app", "my_tests"]
    }
  ]
}

这里 configurePreset 字段把构建预设和配置预设绑定在一起。你执行 cmake --build --preset=release,它会自动找到 release 配置预设的构建目录,然后编译指定的 target。

我曾经踩过一个坑:构建预设里忘了写 configurePreset,结果 CMake 不知道去哪个目录编译,直接报错。所以,除非你有特殊需求,否则一定要关联一个配置预设。

注意:构建预设的 targets 字段是数组,可以指定多个目标。如果留空,则编译 all target。我个人建议显式写出目标,避免编译不需要的东西,尤其是大型项目。

测试预设(Test Presets)

测试预设对应 ctest 的参数。它同样可以引用配置预设,还能设置测试过滤、超时、并行度等。

{
  "testPresets": [
    {
      "name": "default",
      "configurePreset": "default",
      "output": {
        "outputOnFailure": true
      },
      "execution": {
        "jobs": 4,
        "timeout": 120
      },
      "filter": {
        "include": {
          "name": "unit_*"
        }
      }
    }
  ]
}

你看,filter 字段可以只跑名字以 unit_ 开头的测试。我在做 CI 流水线时,经常把单元测试和集成测试分成两个预设,一个跑得快,一个跑得全。

常用测试预设字段:

字段 说明 示例
outputOnFailure 失败时输出详细信息 true / false
jobs 并行测试数 4
timeout 单个测试超时(秒) 120
filter.include.name 包含匹配的测试 "unit_*"
filter.exclude.name 排除匹配的测试 "slow_*"

工作流预设(Workflow Presets)

工作流预设是 CMake 3.23 引入的。它可以把配置、构建、测试串成一个流水线。你执行一个工作流预设,CMake 会自动按顺序执行多个步骤。

{
  "workflowPresets": [
    {
      "name": "ci",
      "steps": [
        {
          "type": "configure",
          "name": "default"
        },
        {
          "type": "build",
          "name": "default"
        },
        {
          "type": "test",
          "name": "default"
        }
      ]
    }
  ]
}

执行 cmake --workflow --preset=ci,它会先配置,再构建,最后测试。一步到位。我在公司的 CI 脚本里就用这个,再也不用写三行 cmake 命令了。

注意:工作流预设的 steps 里,每个 step 的 type 可以是 configurebuildtest 之一。你甚至可以混合不同预设,比如用 release 配置预设 + debug 构建预设?嗯,虽然不常见,但确实可以。

知识体系总览

下面这张图帮你理清四种预设的关系:

CMakePresets.json 配置预设 cmake --preset=xxx 构建预设 cmake --build --preset=xxx 测试预设 ctest --preset=xxx 工作流预设 cmake --workflow --preset=xxx 步骤1: 配置 → 构建 → 测试 继承关系:子预设可继承父预设的配置 变量引用:${sourceDir} ${presetName} ${sourceParentDir} CMake 3.19+ 支持配置/构建预设,3.23+ 支持工作流预设

预设的继承与变量

预设支持多层继承。你可以定义一个基础预设,然后让其他预设继承它。比如:

{
  "configurePresets": [
    {
      "name": "base",
      "hidden": true,
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/${presetName}"
    },
    {
      "name": "debug",
      "inherits": "base",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug"
      }
    },
    {
      "name": "release",
      "inherits": "base",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Release"
      }
    }
  ]
}

注意 hidden: true。隐藏预设不会出现在命令行补全里,也不能直接使用。它纯粹作为基类存在。我在项目中经常用这个技巧,把公共配置抽出来,避免重复。

可用变量列表:

  • ${sourceDir}:项目根目录
  • ${sourceParentDir}:项目根目录的父目录
  • ${presetName}:当前预设的名称
  • ${generator}:生成器名称
  • ${hostSystemName}:主机系统名(Linux、Windows、Darwin)
  • ${fileDir}:CMakePresets.json 所在目录

多平台与条件预设

如果你的项目跨平台,可以用 condition 字段让预设只在特定环境下生效:

{
  "configurePresets": [
    {
      "name": "windows-msvc",
      "condition": {
        "type": "equals",
        "lhs": "${hostSystemName}",
        "rhs": "Windows"
      },
      "generator": "Visual Studio 17 2022",
      "architecture": "x64"
    },
    {
      "name": "linux-gcc",
      "condition": {
        "type": "equals",
        "lhs": "${hostSystemName}",
        "rhs": "Linux"
      },
      "generator": "Ninja",
      "cacheVariables": {
        "CMAKE_C_COMPILER": "gcc",
        "CMAKE_CXX_COMPILER": "g++"
      }
    }
  ]
}

这样,在 Windows 上你只能看到 windows-msvc 预设,在 Linux 上只能看到 linux-gcc 预设。不会出现「选了 Linux 预设但在 Windows 上跑不了」的问题。

避坑指南:我曾经在 condition 里写错了变量名,结果预设死活不生效。调试了半天才发现是 ${hostSystemName} 拼成了 ${hostSystem}。记住,CMake 的变量名是大小写敏感的,而且必须用花括号括起来。

实战建议

最后,分享几个我在实际项目中的经验:

  • 版本号别写错:CMakePresets.json 的 version 字段从 1 到 6,不同版本支持的特性不同。CMake 3.19 用 version 2,3.21 用 version 3,3.23 用 version 4,3.25 用 version 5,3.27 用 version 6。我建议直接用最新版,除非你要兼容旧 CMake。
  • include 机制:可以用 include 字段引入其他 JSON 文件,比如 CMakeUserPresets.json(这个文件通常被 .gitignore 忽略,适合放个人配置)。
  • 不要滥用隐藏预设:隐藏预设虽然好用,但太多层继承会让预设关系变得复杂。我一般只做两层:一层 base,一层具体平台/配置。
  • CI 里用工作流预设:把配置、构建、测试打包成一个工作流预设,CI 脚本里一行命令搞定,维护起来也方便。

预设这个东西,说白了就是「把重复劳动交给机器」。你花半小时写好 CMakePresets.json,以后每次编译都能省下敲命令的几秒钟。积少成多,一年下来也能省下好几个小时呢。


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