21、CMake中的预设模式:CMakePresets.json详解、构建预设、测试预设、打包预设

说实话,CMake 的预设模式(Presets)是我近几年最喜欢的一个特性。以前做项目,尤其是跨平台项目,每次都要跟团队成员说「你记得加这个参数」「别忘了那个选项」,结果总有人漏掉。后来我干脆写了个脚本,但脚本多了又乱。直到 CMake 3.19 引入了 CMakePresets.json,我才觉得这事儿终于被「官方」解决了。

这一章,我就带你彻底搞懂预设模式。说白了,它就是一套标准化的配置模板,把构建、测试、打包的常用参数都写进 JSON 文件里。你想想看,一个文件搞定所有环境,多省心。

预设模式解决了什么问题?

我在项目中遇到过最头疼的事:同一个项目,Windows 上要指定 -G "Visual Studio 17 2022",Linux 上要用 -G "Unix Makefiles",macOS 上还得加个 -DCMAKE_OSX_DEPLOYMENT_TARGET。每次换机器都要重新敲一遍命令,或者翻文档找参数。

预设模式就是把这些「环境相关」的配置固化下来。你只需要写一次,团队所有人都能复用。而且它支持继承、条件判断、变量替换,非常灵活。

核心价值: 一次配置,到处复用。减少人为错误,提升团队协作效率。

CMakePresets.json 文件结构

这个 JSON 文件通常放在项目根目录。它的顶层结构是这样的:

{
  "version": 6,
  "configurePresets": [],
  "buildPresets": [],
  "testPresets": [],
  "packagePresets": [],
  "workflowPresets": []
}

嗯,这里要注意:version 字段必须写,而且不同 CMake 版本支持的版本号不同。CMake 3.19 支持 version 1,到 3.27 已经支持 version 6 了。我建议你直接用最新版,功能最全。

构建预设(configurePresets)

构建预设是最常用的。它定义了 cmake -S . -B build 时需要的所有参数。来看一个实际例子:

{
  "configurePresets": [
    {
      "name": "default",
      "displayName": "默认配置",
      "description": "适用于大多数开发环境",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/${presetName}",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug",
        "CMAKE_CXX_STANDARD": "17"
      }
    },
    {
      "name": "release",
      "inherits": "default",
      "displayName": "发布配置",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Release",
        "CMAKE_INTERPROCEDURAL_OPTIMIZATION": "ON"
      }
    }
  ]
}

你看,release 预设通过 inherits 继承了 default 的所有设置,只覆盖了构建类型和开启 LTO。这就是继承的好处——避免重复写相同的参数。

我的习惯: 我会把通用的编译器选项、标准版本、输出目录都放在一个 base 预设里,然后各个平台或构建类型去继承它。这样改一处,所有地方都生效。

构建预设(buildPresets)

构建预设对应的是 cmake --build 命令。它需要引用一个 configurePreset,因为构建必须基于某个配置。看例子:

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

这里 cleanFirst 表示构建前先清理。我曾经因为忘记清理旧文件,导致调试了半天才发现链接的是旧库。嗯,从那以后我习惯在发布构建时加上这个选项。

测试预设(testPresets)

测试预设对应 ctest 命令。它同样需要引用一个 configurePreset

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

我个人习惯把单元测试和集成测试分开成不同的预设。比如 unit 只跑 test_ 开头的测试,integrationit_ 开头的。这样在 CI 里可以并行跑,节省时间。

注意: 测试预设中的 filter 支持正则表达式。但要注意转义字符,JSON 里写正则有点麻烦。我建议尽量用简单的通配符,比如 test_*

打包预设(packagePresets)

打包预设是 CMake 3.23 才引入的,对应 cpack 命令。它需要引用一个 configurePreset 和一个 buildPreset

{
  "packagePresets": [
    {
      "name": "zip",
      "configurePreset": "release",
      "buildPreset": "release",
      "generators": ["ZIP"],
      "variables": {
        "CPACK_PACKAGE_NAME": "MyApp",
        "CPACK_PACKAGE_VERSION": "1.0.0"
      }
    },
    {
      "name": "deb",
      "inherits": "zip",
      "generators": ["DEB"],
      "variables": {
        "CPACK_DEBIAN_PACKAGE_MAINTAINER": "dev@example.com"
      }
    }
  ]
}

你看,打包预设也支持继承。我通常会在一个基础预设里设置好包名、版本号,然后各个平台(deb、rpm、zip)去继承并添加平台特有的变量。

条件预设与平台判断

预设模式还支持条件判断,根据当前系统环境自动选择配置。这个功能非常实用:

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

为什么会这样?因为不同平台的编译器、生成器都不一样。有了条件预设,你只需要执行 cmake --preset=windowscmake --preset=linux,CMake 会自动匹配当前系统。

工作流预设(workflowPresets)

工作流预设是 CMake 3.23 引入的,它可以把多个步骤串起来。比如先配置、再构建、再测试、最后打包:

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

执行 cmake --workflow --preset=ci,就会自动完成整个流程。我在 CI 脚本里就是这么用的,省去了写一堆 shell 命令的麻烦。

知识体系结构图

下面这张图展示了预设模式的整体关系:

CMakePresets.json configurePresets 生成器、缓存变量、条件 buildPresets 并行数、目标、清理 testPresets 过滤、超时、输出 packagePresets 生成器、版本、平台 workflowPresets 继承、条件判断、变量替换贯穿所有预设

实际使用技巧

最后分享几个我在项目中积累的经验:

  • 版本控制:CMakePresets.json 提交到 Git 仓库,但 CMakeUserPresets.json(用户私有预设)要加到 .gitignore 里。这样团队共享公共配置,个人可以覆盖自己的偏好。
  • 变量替换: 预设支持 ${sourceDir}${presetName}${hostSystemName} 等内置变量。我经常用 ${presetName} 来区分不同构建目录,比如 build/debugbuild/release
  • 环境变量: 可以通过 environment 字段设置环境变量。比如指定 CCCXX 编译器路径,避免系统默认的编译器版本不对。
  • 隐藏预设: 如果某个预设不想让用户直接使用,可以设置 "hidden": true。它只能被其他预设继承,不会出现在 --list-presets 输出中。
避坑指南: 我曾经在 cacheVariables 里写了一个布尔值 false,结果 CMake 把它当成了字符串。后来发现必须写成 "FALSE""OFF" 这种 CMake 认识的字符串形式。记住,JSON 里的布尔值 true/false 和 CMake 的 ON/OFF 不是一回事。

预设模式用好了,真的能大幅提升开发效率。尤其是团队协作时,每个人都能用统一的配置,再也不用担心「在我机器上能编译」这种问题了。你试试看,把项目里的构建参数整理成预设,你会发现世界清净了很多。

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