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 其实很简单,核心就是两步:
- 在仓库根目录放好
CMakePresets.json - 在 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
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
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 安装路径。
$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 中的角色
一些实战经验
最后分享几个我在 CI 集成 Presets 时踩过的坑:
- 版本兼容性: CMake 3.20 才开始支持 Presets,3.23 才支持
--build --preset。如果你的 CI 镜像用的是 Ubuntu 20.04 自带的 CMake 3.16,那就得手动升级。我建议在 CI 脚本里加一步 CMake 版本检查。 - 环境变量传递: Preset 文件里可以用
$env{VAR}引用环境变量。但不同 CI 平台的环境变量命名可能不同(比如CIvsGITLAB_CI)。我习惯在 Preset 里定义一个ci条件,然后在 CI 脚本里 export 对应的变量。 - 缓存策略: 如果你用 CCache,记得在 Preset 里把
CMAKE_C_COMPILER_LAUNCHER和CMAKE_CXX_COMPILER_LAUNCHER设置好。CI 上每次都是干净构建,没有缓存的话编译时间会翻倍。 - 测试报告: CTest 支持输出 JUnit 格式的测试报告,很多 CI 平台可以直接解析。在 Preset 里加上
--output-junit test-results.xml,CI 页面上就能直接看到测试通过率。
嗯,差不多就这些。Presets 在 CI/CD 里的用法其实不复杂,但细节挺多。你只要记住一个原则:Preset 文件是唯一真相来源。所有构建参数都写在 Preset 里,CI 脚本只负责调用。这样不管换什么 CI 平台,你的构建逻辑都不会变。
ci-release,跑通了再慢慢加 ci-debug、ci-sanitizer 这些。一步到位容易出问题,迭代着来更稳。
公众号:蓝海资料掘金营,微信deep3321