20、CI/CD 集成:在 GitHub Actions、GitLab CI、Jenkins 中使用 Presets

说实话,CMake Presets 这个功能刚出来的时候,我第一反应是:这不就是给本地开发用的吗?直到有一次,我在公司搭 CI 流水线,发现开发机、测试机、打包机上的 CMake 参数全都不一样,每次改构建脚本都得翻半天文档。嗯,那时候我才意识到——Presets 真正的价值,其实是在 CI/CD 里。

你想想看,团队里每个人本地构建都跑得好好的,一上 CI 就崩,这种事我见过太多次了。Presets 能帮你把「本地能跑」和「CI 能跑」统一起来。说白了,就是一套配置,到处运行。

为什么 CI/CD 里要用 Presets?

我个人习惯把 CI 脚本分成三块:环境准备、构建配置、产物处理。Presets 主要解决中间那块——构建配置。

以前写 CI 脚本,你可能会这样:

# GitHub Actions 里直接写 CMake 参数
cmake -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="-O2" -DBUILD_TESTING=OFF

问题在哪?这些参数散落在各个 CI 平台的 YAML 文件里,改一个地方,其他平台全得跟着改。Presets 把参数集中到 CMakePresets.json 里,CI 脚本只需要一行:

cmake --preset ci-release

干净利落。我在项目中遇到过好几次,因为不同 CI 平台的语法差异,导致构建参数漏传或写错。Presets 直接把这个坑填平了。

GitHub Actions 集成

GitHub Actions 是最常见的 CI 平台之一。集成 Presets 其实很简单,核心就是两步:

  1. 在仓库根目录放好 CMakePresets.json
  2. 在 workflow 里调用 cmake --preset

下面是一个实际可用的例子:

# .github/workflows/build.yml
name: CMake Build with Presets

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v4

    - name: Configure with Preset
      run: cmake --preset ci-release

    - name: Build
      run: cmake --build --preset ci-release

    - name: Test
      run: ctest --preset ci-release

这里有个细节要注意:cmake --build --preset 这个命令是从 CMake 3.23 开始支持的。如果你还在用老版本,就得手动指定构建目录:

cmake --build build/ci-release
小技巧: 我建议在 Presets 里把 binaryDir 设置成 build/{presetName} 这种格式。这样不同 Preset 的构建产物不会互相干扰,排查问题也方便。

GitLab CI 集成

GitLab CI 的集成思路差不多,但有个地方容易踩坑——GitLab Runner 默认的镜像可能没有安装最新版 CMake。我曾经因为这个折腾了半天,Preset 文件解析失败,报了一堆看不懂的错误。

解决方案很简单:用官方 CMake 镜像,或者自己在 CI 脚本里安装指定版本。

# .gitlab-ci.yml
stages:
  - build
  - test

cmake-build:
  stage: build
  image: cmake:latest
  script:
    - cmake --preset ci-release
    - cmake --build --preset ci-release
  artifacts:
    paths:
      - build/

cmake-test:
  stage: test
  image: cmake:latest
  script:
    - ctest --preset ci-release
  dependencies:
    - cmake-build
注意: GitLab CI 的 artifacts 路径要和 Presets 里的 binaryDir 保持一致。否则下一个 job 找不到构建产物,会报「找不到 CMakeCache.txt」的错误。

另外,如果你在 GitLab CI 里用矩阵构建(比如同时编译 Debug 和 Release),可以这样写:

matrix:
  - PRESET: ci-debug
  - PRESET: ci-release

script:
  - cmake --preset ${PRESET}
  - cmake --build --preset ${PRESET}

这样 Preset 名称通过变量传入,一个 job 定义就能覆盖所有构建配置。我在团队里推广这个模式后,CI 配置文件从 200 行缩到了 50 行。

Jenkins 集成

Jenkins 的情况稍微复杂一点。因为 Jenkins 的 Pipeline 脚本是 Groovy 写的,而且很多老项目还在用 Freestyle job。不过核心逻辑不变:调用 cmake --preset

下面是一个 Declarative Pipeline 的例子:

// Jenkinsfile
pipeline {
    agent any

    tools {
        cmake 'cmake-3.27'
    }

    stages {
        stage('Configure') {
            steps {
                sh 'cmake --preset ci-release'
            }
        }
        stage('Build') {
            steps {
                sh 'cmake --build --preset ci-release'
            }
        }
        stage('Test') {
            steps {
                sh 'ctest --preset ci-release'
            }
        }
    }
}

这里有个坑:Jenkins 的 tools 块需要提前在全局工具配置里定义好 CMake 版本。如果你用的是 Pipeline 脚本,记得在 Jenkins 管理页面里添加 CMake 安装路径。

避坑指南: 我曾经在 Jenkins 里遇到一个问题——Preset 文件里用了 $env{CI} 这种环境变量,但 Jenkins 的某些 agent 没有设置这个变量,导致条件判断失效。后来我改成在 Preset 里用 $penv{CI} 或者直接在 Jenkinsfile 里 export 变量,问题才解决。

跨平台构建的 Preset 设计

如果你的项目需要同时在 Linux、macOS、Windows 上构建,Presets 的 include 机制就派上用场了。我一般这样组织:

{
  "version": 6,
  "include": [
    "CMakePresets-common.json",
    "CMakePresets-linux.json",
    "CMakePresets-macos.json",
    "CMakePresets-windows.json"
  ],
  "configurePresets": [
    {
      "name": "ci-release",
      "inherits": ["common-release", "linux-release"]
    }
  ]
}

这样每个平台特有的编译器路径、系统库位置都放在各自的文件里,CI 脚本里只引用一个 Preset 名称。维护起来特别清爽。

SVG 流程图:Presets 在 CI/CD 中的角色

CMake Presets 在 CI/CD 中的角色 CMakePresets.json 统一构建配置 本地开发 cmake --preset debug CI/CD 平台 cmake --preset ci-release 构建产物 二进制 / 安装包 继承配置 CI 调用 生成 核心思想 一套 Preset 配置,同时服务于本地开发、CI 构建、产物打包 避免参数散落各处,减少维护成本

一些实战经验

最后分享几个我在 CI 集成 Presets 时踩过的坑:

  • 版本兼容性: CMake 3.20 才开始支持 Presets,3.23 才支持 --build --preset。如果你的 CI 镜像用的是 Ubuntu 20.04 自带的 CMake 3.16,那就得手动升级。我建议在 CI 脚本里加一步 CMake 版本检查。
  • 环境变量传递: Preset 文件里可以用 $env{VAR} 引用环境变量。但不同 CI 平台的环境变量命名可能不同(比如 CI vs GITLAB_CI)。我习惯在 Preset 里定义一个 ci 条件,然后在 CI 脚本里 export 对应的变量。
  • 缓存策略: 如果你用 CCache,记得在 Preset 里把 CMAKE_C_COMPILER_LAUNCHERCMAKE_CXX_COMPILER_LAUNCHER 设置好。CI 上每次都是干净构建,没有缓存的话编译时间会翻倍。
  • 测试报告: CTest 支持输出 JUnit 格式的测试报告,很多 CI 平台可以直接解析。在 Preset 里加上 --output-junit test-results.xml,CI 页面上就能直接看到测试通过率。

嗯,差不多就这些。Presets 在 CI/CD 里的用法其实不复杂,但细节挺多。你只要记住一个原则:Preset 文件是唯一真相来源。所有构建参数都写在 Preset 里,CI 脚本只负责调用。这样不管换什么 CI 平台,你的构建逻辑都不会变。

我的建议: 刚开始迁移的时候,别急着把所有 Preset 都配好。先配一个 ci-release,跑通了再慢慢加 ci-debugci-sanitizer 这些。一步到位容易出问题,迭代着来更稳。

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