第28章 CI/CD集成:GitHub Actions,GitLab CI,Jenkins中的CMake构建脚本编写
说实话,CMake本身写得再漂亮,如果每次构建都要手动敲命令,那跟没写差不多。我见过太多团队,本地编译好好的,一上CI就崩。为什么?因为CI环境和本地环境总有那么点不一样。这一章,我们就聊聊怎么把CMake脚本塞进主流的CI/CD流水线里。
28.1 为什么CI里的CMake需要单独操心?
你可能会想:「不就是cmake -B build && cmake --build build吗?」嗯,理论上没错。但实际项目中,你会遇到这些问题:
- 缓存问题:CI每次都是干净环境,没有缓存,编译慢得要死
- 工具链差异:本地是VS2022,CI可能是ubuntu-latest
- 依赖管理:第三方库怎么下载?vcpkg?Conan?还是git submodule?
- 并行度控制:CI机器的核心数跟你的开发机不一样
我个人习惯是,在项目根目录放一个ci/文件夹,里面按平台分脚本。这样CI配置文件和CMake逻辑解耦,维护起来清爽很多。
28.2 GitHub Actions + CMake
GitHub Actions是目前最流行的CI之一。它的配置是YAML格式,放在.github/workflows/下。我给大家看一个我常用的模板:
# .github/workflows/cmake.yml
name: CMake Build
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
env:
BUILD_TYPE: Release
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
include:
- os: ubuntu-latest
generator: "Ninja"
- os: windows-latest
generator: "Visual Studio 17 2022"
- os: macos-latest
generator: "Ninja"
steps:
- uses: actions/checkout@v4
- name: Install Ninja (Linux/macOS)
if: runner.os != 'Windows'
run: sudo apt-get install -y ninja-build
- name: Configure CMake
run: cmake -B ${{github.workspace}}/build -G "${{matrix.generator}}" -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}}
- name: Build
run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}}
- name: Test
working-directory: ${{github.workspace}}/build
run: ctest -C ${{env.BUILD_TYPE}} --output-on-failure
28.3 GitLab CI + CMake
GitLab CI的配置是.gitlab-ci.yml。它的特点是基于Docker runner,环境隔离做得很好。我一般在项目里这样写:
# .gitlab-ci.yml
stages:
- configure
- build
- test
variables:
BUILD_DIR: build
CMAKE_FLAGS: "-DCMAKE_BUILD_TYPE=Release"
before_script:
- apt-get update -qq && apt-get install -y -qq cmake ninja-build g++
configure:
stage: configure
script:
- cmake -B $BUILD_DIR -G Ninja $CMAKE_FLAGS
artifacts:
paths:
- $BUILD_DIR/
expire_in: 1 hour
build:
stage: build
script:
- cmake --build $BUILD_DIR --parallel $(nproc)
dependencies:
- configure
test:
stage: test
script:
- cd $BUILD_DIR && ctest --output-on-failure -j$(nproc)
dependencies:
- build
28.4 Jenkins + CMake
Jenkins是老牌CI工具,配置灵活但稍显繁琐。我建议用Pipeline(Declarative Pipeline)来写,可读性好:
// Jenkinsfile
pipeline {
agent any
tools {
cmake 'cmake-3.27'
ninja 'ninja-1.11'
}
environment {
BUILD_DIR = 'build'
BUILD_TYPE = 'Release'
}
stages {
stage('Configure') {
steps {
script {
def generator = isUnix() ? 'Ninja' : 'Visual Studio 17 2022'
cmakeConfigure generator: generator,
sourceDir: '.',
buildDir: BUILD_DIR,
arguments: ["-DCMAKE_BUILD_TYPE=${BUILD_TYPE}"]
}
}
}
stage('Build') {
steps {
cmakeBuild buildDir: BUILD_DIR,
arguments: ['--parallel', '--config', BUILD_TYPE]
}
}
stage('Test') {
steps {
dir(BUILD_DIR) {
sh 'ctest -C ${BUILD_TYPE} --output-on-failure -j$(nproc)'
}
}
}
}
post {
always {
junit 'build/test-results/**/*.xml'
archiveArtifacts artifacts: 'build/bin/*', fingerprint: true
}
}
}
Jenkins的坑:Jenkins的CMake插件版本可能比较老。我建议直接在Pipeline里用sh或bat步骤调用原生CMake命令,而不是依赖插件。这样你的脚本在任何Jenkins版本上都能跑。
28.5 跨CI的通用CMake脚本技巧
不管用哪个CI,有些CMake层面的技巧是通用的。我总结了几条:
| 场景 | CMake脚本写法 | 说明 |
|---|---|---|
| 检测CI环境 | if(DEFINED ENV{CI}) |
大多数CI都会设置CI环境变量 |
| 缓存第三方库 | set(CMAKE_CACHE_FILE "$ENV{CI_PROJECT_DIR}/.cmake-cache") |
利用CI的缓存机制,避免每次重新下载 |
| 控制并行度 | cmake --build . --parallel $ENV{NPROC} |
Linux用nproc,macOS用sysctl -n hw.logicalcpu |
| 生成测试报告 | ctest --output-junit test-results.xml |
JUnit格式被所有CI支持 |
28.6 避坑指南:CI里的CMake常见问题
我做过的项目里,CI上的CMake问题翻来覆去就那么几个。我列出来,你遇到了直接对照排查:
- 路径硬编码:不要在CMakeLists.txt里写死
/home/user/之类的路径。用CMAKE_SOURCE_DIR或PROJECT_SOURCE_DIR。 - 生成器不一致:本地用VS,CI用Ninja,结果某些编译器特性检测不一样。我建议在
CMakePresets.json里统一配置。 - 测试超时:CI机器通常比本地慢。在
ctest命令里加--timeout 120,或者用set_tests_properties设置单个测试的超时。 - 缓存污染:CI的缓存目录如果不清空,旧版本的编译产物可能导致奇怪的问题。我习惯在每次构建前
rm -rf build/。
我曾经踩过的坑:有一次在GitHub Actions上,Linux构建一直失败,报错说找不到std::filesystem。查了半天,发现是ubuntu-latest的GCC版本太老,不支持C++17。后来我在CMakeLists.txt里加了set(CMAKE_CXX_STANDARD 17),并且在CI里显式安装了GCC-10才解决。所以,CI上的工具链版本一定要在CMake里做检查。
28.7 知识体系总览
下面这张图,我把本章的核心逻辑梳理了一下。你看完应该能明白,CI里的CMake其实就三件事:配置、构建、测试。但每件事在不同CI里写法不同,核心是保持CMake脚本本身的可移植性。
28.8 小结
说白了,CI里的CMake脚本编写,核心就一句话:让CMake脚本本身不依赖任何CI特性。你在本地能跑通的CMakeLists.txt,放到任何CI里都应该能跑。CI配置文件只负责调用cmake和ctest,以及处理缓存、报告这些外围事情。
我个人建议,刚开始做CI集成时,先在一个CI上跑通,再扩展到其他平台。别一上来就搞矩阵构建,否则排查问题会让你怀疑人生。嗯,我就干过这种事,结果花了整整一天才发现是某个环境变量没传进去。
最后,记得在项目README里写上CI的构建状态徽章。这玩意儿看着小,但能极大提升项目的可信度。别人一看「build passing」,心里就有底了。