45. 12.1 CMake生态系统工具链

引言:施工队长的”标准作业手册”

在前面的十一章中,我们的 CMake “施工队长”已经身经百战:从看图纸(Target)到运材料(Source),从调工艺(Compile/Link)到办交房(Install),从海外工程(交叉编译)到迎接各路监理(IDE 集成),可谓是十八般武艺样样精通。但不知道你有没有发现一个问题:当队长面对不同的客户时,他每次都要重新口述一遍施工方案。

比如,对本地开发团队要说:”用 Ninja 生成器,开 Debug 模式,开测试,开 Sanitizer”;对 CI 机器人要说:”用 Unix Makefiles,开 Release 模式,跑全量测试,生成覆盖率报告”;对交叉编译环境又要说:”加载 ARM 工具链,关闭测试,开启 LTO”。这些命令行长到能绕工地三圈,一旦记错一个参数,整栋楼就可能盖歪。

到了 2020 年,CMake 官方终于给队长发了一本标准作业手册——CMakePresets.json。这本手册用 JSON 格式把各种常用配置写成”预设”(Preset),从此队长不再需要靠记性,只要翻开手册点一道菜,整个施工流程就能按标准化方案自动执行。更重要的是,这本手册是跨 IDE、跨平台、跨人员的,无论是 VS Code、CLion、Visual Studio,还是 GitHub Actions,都能读得懂。

这一节,我们就来学习这本手册的编写与使用。

12.1 CMakePresets.json 的作用

简单来说,CMakePresets.json 是位于项目根目录的一个 JSON 文件,它把原本需要在命令行里敲的长串参数,封装成了一个个有名字的配置卡片。它的核心价值可以概括为三点:

  • 团队一致性:新成员 clone 仓库后,不需要打听”咱们项目怎么配”,直接 cmake --preset=dev 就能拿到和资深工程师完全一致的配置。
  • IDE 无关性:VS Code、CLion、Visual Studio 2022+、Qt Creator 都原生支持读取 Preset,配置一次,到处使用。
  • CI/CD 友好:自动化流水线可以直接引用预设,避免在 YAML 里写冗长的 -D 参数,让构建脚本更易读、易维护。

CMake 预设分为四大类,分别对应施工流程的四个阶段:

  1. Configure Presets(配置预设):相当于施工前的图纸会审,决定生成器、工具链、缓存变量。
  2. Build Presets(构建预设):相当于正式开工,决定编译目标、并发数、构建目录。
  3. Test Presets(测试预设):相当于质检验收,决定 CTest 的运行策略。
  4. Workflow Presets(工作流预设):相当于总控台,把上面三步串成”一键交钥匙”流程。

配置预设:configurePresets

配置预设是最基础、最常用的预设类型。一个最小可用的 CMakePresets.json 长这样:

{
  "version": 6,
  "cmakeMinimumRequired": {
    "major": 3,
    "minor": 23,
    "patch": 0
  },
  "configurePresets": [
    {
      "name": "dev",
      "displayName": "开发调试配置",
      "description": "本地开发使用:Ninja + Debug + 测试开启",
      "generator": "Ninja Multi-Config",
      "binaryDir": "${sourceDir}/build/${presetName}",
      "cacheVariables": {
        "CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
        "BUILD_TESTING": "ON",
        "CMAKE_CXX_STANDARD": "20"
      },
      "environment": {
        "CXXFLAGS": "-fdiagnostics-color=always"
      }
    }
  ]
}

我们来拆解几个关键字段:

  • name:预设的唯一标识符,命令行里要用它。displayNamedescription 是给 IDE 展示用的。
  • generator:指定生成器,比如 NinjaUnix MakefilesVisual Studio 17 2022
  • binaryDir:构建目录。这里用了宏 ${sourceDir}${presetName},CMake 会自动展开为源码根目录和预设名,避免硬编码路径。
  • cacheVariables:等价于命令行的 -D 参数,直接写入 CMakeCache.txt
  • environment:设置环境变量,仅在该预设的配置阶段生效。

继承与复用

如果”开发配置”和”发布配置”只有少量差异(比如 CMAKE_BUILD_TYPE 不同),可以用 inherits 字段避免重复:

{
  "configurePresets": [
    {
      "name": "base",
      "hidden": true,
      "generator": "Ninja Multi-Config",
      "binaryDir": "${sourceDir}/build/${presetName}",
      "cacheVariables": {
        "CMAKE_CXX_STANDARD": "20"
      }
    },
    {
      "name": "dev",
      "inherits": "base",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug",
        "BUILD_TESTING": "ON"
      }
    },
    {
      "name": "release",
      "inherits": "base",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Release",
        "BUILD_TESTING": "OFF"
      }
    }
  ]
}

注意 hidden: true 表示该预设不会出现在 IDE 列表里,专门作为”基类”被继承。这种写法让手册既整洁又遵循 DRY 原则。

工具链预设

交叉编译时,可以把工具链文件路径直接写在预设里,省得每次手动 --toolchain

{
  "name": "cross-arm",
  "inherits": "base",
  "toolchainFile": "${sourceDir}/cmake/arm-linux-gnueabihf.cmake",
  "cacheVariables": {
    "CMAKE_BUILD_TYPE": "MinSizeRel"
  }
}

构建预设:buildPresets

配置完成后,构建阶段也可以预设。Build Preset 必须关联一个 Configure Preset,告诉 CMake”我用哪套图纸,盖哪几层楼”。

{
  "buildPresets": [
    {
      "name": "dev-build",
      "configurePreset": "dev",
      "configuration": "Debug",
      "targets": ["myapp", "myapp_tests"],
      "jobs": 8,
      "verbose": false
    },
    {
      "name": "release-build",
      "configurePreset": "release",
      "configuration": "Release",
      "jobs": 0
    }
  ]
}

关键字段说明:

  • configurePreset:必填,指明基于哪个配置预设。CMake 会自动去对应的 binaryDir 里执行构建。
  • configuration:对于多配置生成器(如 Visual Studio、Ninja Multi-Config),指定本次构建用 Debug 还是 Release。
  • targets:只构建指定目标,而不是默认目标(通常是 ALL_BUILD)。这在大型项目里能节省大量时间。
  • jobs:并行编译数。0 表示让 CMake 自动根据 CPU 核心数决定。

命令行用法:

cmake --preset=dev          # 执行配置预设
cmake --build --preset=dev-build  # 执行构建预设

测试预设:testPresets

测试预设让 CTest 的命令行参数也能”卡片化”。这对于需要反复跑不同策略的测试场景(如快速冒烟测试 vs. 全量回归测试)非常有用。

{
  "testPresets": [
    {
      "name": "dev-test",
      "configurePreset": "dev",
      "configuration": "Debug",
      "output": {
        "outputOnFailure": true,
        "verbosity": "extra"
      },
      "execution": {
        "jobs": 4,
        "timeout": 60,
        "noTestsAction": "error"
      },
      "filter": {
        "include": {
          "name": "^smoke_"
        }
      }
    },
    {
      "name": "ci-test",
      "configurePreset": "release",
      "configuration": "Release",
      "execution": {
        "jobs": 8,
        "timeout": 300
      },
      "output": {
        "outputJUnitFile": "${sourceDir}/build/junit.xml"
      }
    }
  ]
}

这段配置定义了两套测试策略:

  • dev-test:本地开发时只跑名字以 smoke_ 开头的冒烟测试,输出详细日志,失败时立即打印详情。
  • ci-test:CI 环境下跑全量测试,生成 JUnit 格式的 XML 报告,方便 GitLab/GitHub 解析展示。

命令行执行:

ctest --preset=dev-test
ctest --preset=ci-test

工作流预设:workflowPresets

如果每次都要手动先 configure、再 build、再 test,那预设的便利性就打了折扣。CMake 3.23+ 引入了 Workflow Preset,可以把多个步骤编排成一个流水线:

{
  "workflowPresets": [
    {
      "name": "dev-workflow",
      "displayName": "完整开发流程",
      "steps": [
        {
          "type": "configure",
          "name": "dev"
        },
        {
          "type": "build",
          "name": "dev-build"
        },
        {
          "type": "test",
          "name": "dev-test"
        }
      ]
    }
  ]
}

一条命令跑完全程:

cmake --workflow --preset=dev-workflow

如果其中任何一步失败,整个工作流会立即终止并返回非零退出码,非常适合在 Git Hook 或本地提交前检查中使用。

条件预设与宏扩展

真正的大型项目往往需要在不同操作系统上启用不同的预设。CMake Preset 支持 condition 字段,实现”见机行事”:

{
  "configurePresets": [
    {
      "name": "win-dev",
      "condition": {
        "type": "equals",
        "lhs": "${hostSystemName}",
        "rhs": "Windows"
      },
      "generator": "Visual Studio 17 2022",
      "architecture": {
        "value": "x64",
        "strategy": "set"
      }
    },
    {
      "name": "unix-dev",
      "condition": {
        "type": "notEquals",
        "lhs": "${hostSystemName}",
        "rhs": "Windows"
      },
      "generator": "Ninja Multi-Config"
    }
  ]
}

常用的宏变量包括:

  • ${sourceDir}CMakePresets.json 所在的目录。
  • ${sourceParentDir}:源码目录的父目录。
  • ${presetName}:当前预设的名字。
  • ${generator}:当前使用的生成器名称。
  • ${hostSystemName}:宿主系统(Windows、Linux、Darwin)。
  • ${fileDir}:当前 JSON 文件所在目录(对于 CMakeUserPresets.json 特别有用)。

这些宏在 binaryDirtoolchainFilecacheVariables 等字段中都能使用,极大提升了配置的灵活性。

CMakeUserPresets.json

有时候,个别开发者有一些本地专属的偏好(比如特定的 ccache 路径或私有的 SDK 路径),但又不方便提交到仓库。CMake 支持在项目根目录放置一个 CMakeUserPresets.json,它继承并覆盖 CMakePresets.json 的内容,且通常被 .gitignore 忽略。这相当于每个工程师可以在标准手册旁边放一本”私人笔记”。

Preset 的版本演进

Preset 并非一日长成。了解它的版本历史,能帮你判断当前 CMake 版本支持哪些特性:

  • CMake 3.19:首次引入 CMakePresets.json,仅支持 configurePresets
  • CMake 3.20:新增 buildPresetstestPresets
  • CMake 3.21:支持 macroExpansion 和更多的条件判断。
  • CMake 3.23:新增 workflowPresets,实现配置-构建-测试的编排。
  • CMake 3.24:支持 packagePresets(配合 CPack)。
  • CMake 3.25:增强条件表达式,支持 matches(正则匹配)等更复杂的逻辑。

JSON 文件顶部的 version 字段必须与当前使用的特性集匹配。如果 IDE 或命令行报 “Unsupported preset version”,通常就是因为 version 写高了,而本地 CMake 版本太旧。建议团队统一 CMake 版本,或者把 cmakeMinimumRequired 写清楚。

与 CI/CD 系统的深度集成

Preset 最大的革命性意义,在于它抹平了”本地开发”与”自动化构建”之间的鸿沟。以前,开发者在本地用 CLion 图形界面配置一套参数,CI 里的 YAML 又要写另一套,两者很容易脱节。现在,双方可以读同一本手册。

GitHub Actions 示例

name: CI
on: [push, pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Configure
        run: cmake --preset=ci
      - name: Build
        run: cmake --build --preset=ci-build
      - name: Test
        run: ctest --preset=ci-test
      - name: Upload JUnit
        uses: actions/upload-artifact@v4
        with:
          name: test-results
          path: build/junit.xml

可以看到,YAML 里没有任何 -D 或长路径,所有细节都藏在仓库的 CMakePresets.json 里。这意味着:如果以后要调整编译选项,开发者只需要改一个 JSON 文件并提交 PR,而不需要去各个 CI 脚本里翻找。

GitLab CI 示例

build-job:
  stage: build
  script:
    - cmake --preset=ci
    - cmake --build --preset=ci-build
  artifacts:
    paths:
      - build/

test-job:
  stage: test
  needs: [build-job]
  script:
    - ctest --preset=ci-test
  artifacts:
    reports:
      junit: build/junit.xml

矩阵构建

对于需要在多个平台上测试的项目,可以结合 CI 的矩阵功能与 Preset 的命名规范:

strategy:
  matrix:
    preset: [linux-clang, linux-gcc, macos-arm, windows-msvc]
steps:
  - run: cmake --preset=${{ matrix.preset }}
  - run: cmake --build --preset=${{ matrix.preset }}-build
  - run: ctest --preset=${{ matrix.preset }}-test

只要预设命名遵循统一规范,CI 脚本就能写得极其优雅,真正做到”增删平台只需改 JSON,不动 YAML”。

小结

在这一节中,我们学习了 CMake 生态系统中最重要的现代化工具之一——Preset(预设)。它通过 CMakePresets.json 这本”标准作业手册”,把原本散落在命令行、IDE 配置和 CI 脚本中的构建参数,统一收拢到版本控制之下。

我们掌握了:

  • configurePresets:定义生成器、工具链、缓存变量和环境变量,支持继承与复用。
  • buildPresets:绑定配置预设,控制构建目标、并发数和配置类型。
  • testPresets:封装 CTest 的过滤、超时、输出和并行策略。
  • workflowPresets:把配置、构建、测试编排成一键执行的工作流。
  • 条件与宏:利用 condition 和内置宏实现跨平台的灵活配置。
  • CI/CD 集成:让本地开发与自动化流水线共用同一套配置,消除”在我机器上能跑”的隐患。

下一节,我们将把目光投向 CMake 与现代 C++ 包管理生态的整合——vcpkg、Conan 2.x 等工具如何与 CMake 协同工作,进一步完善我们的构建工具链。

请登录后发表评论

    没有回复内容

正在唤醒异次元光景……