99、CI/CD集成:在GitHub Actions/GitLab CI中配置CMake构建
说实话,很多C++项目在本地能编译通过,一上CI就翻车。我见过太多次了——开发机上是macOS,CI跑的是Ubuntu,编译器版本不一样,CMakeLists.txt里写的路径也不对。嗯,今天我们就来聊聊怎么把CMake构建塞进CI/CD流水线里。
为什么要在CI里用CMake?
你想想看,团队里每个人环境都不一样。有人用VS2022,有人用Clang,还有人用MinGW。CMake本身就是跨平台的构建工具,天然适合CI场景。我个人习惯是:本地开发用Ninja,CI里也用Ninja,这样构建行为高度一致。
核心原则:CI中的CMake配置应该与本地开发保持一致,只改变工具链和缓存策略。
GitHub Actions 配置实战
先看一个我常用的GitHub Actions工作流。这个配置覆盖了Linux、macOS、Windows三个平台:
name: CMake Build
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
build_type: [Debug, Release]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Configure CMake
run: cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.build_type }}
- name: Build
run: cmake --build build --config ${{ matrix.build_type }}
- name: Test
run: ctest --test-dir build --output-on-failure
这里有个细节——--config参数。我在项目中遇到过一个问题:Windows上多配置生成器(比如Visual Studio)必须传--config,而单配置生成器(比如Unix Makefiles)会忽略它。所以统一加上这个参数,跨平台就安全了。
小技巧:在GitHub Actions中,可以用ccache加速重复构建。我一般这样加:
- name: Cache ccache
uses: actions/cache@v3
with:
path: ~/.ccache
key: ${{ runner.os }}-ccache-${{ github.sha }}
restore-keys: ${{ runner.os }}-ccache-
GitLab CI 配置实战
GitLab CI的配置思路类似,但语法不同。我个人更喜欢GitLab的include机制,可以把公共配置抽出来:
variables:
BUILD_DIR: build
stages:
- configure
- build
- test
.configure_template: &configure
stage: configure
script:
- cmake -B $BUILD_DIR -DCMAKE_BUILD_TYPE=$BUILD_TYPE
artifacts:
paths:
- $BUILD_DIR/
.build_template: &build
stage: build
script:
- cmake --build $BUILD_DIR --config $BUILD_TYPE
dependencies:
- configure
.test_template: &test
stage: test
script:
- ctest --test-dir $BUILD_DIR --output-on-failure
dependencies:
- build
linux-debug:
image: ubuntu:22.04
variables:
BUILD_TYPE: Debug
before_script:
- apt-get update && apt-get install -y cmake g++
<<: *configure
<<: *build
<<: *test
嗯,这里要注意一点:GitLab CI的artifacts默认只保留最近一次。如果你需要调试构建产物,记得设置expire_in。我曾经因为没设过期时间,CI跑了几百次后,GitLab的存储直接爆了……
避坑指南:我曾经在GitLab CI里忘记安装build-essential,结果cmake配置成功,但编译时报找不到标准库头文件。建议在before_script里显式安装所有依赖,不要依赖镜像自带的工具链。
CI中的CMake最佳实践
总结几条我踩过坑后总结出来的经验:
- 使用预设(Presets):CMake Presets可以统一CI和本地的配置。我在
CMakePresets.json里定义好所有配置,CI直接cmake --preset ci就行。 - 控制并行度:CI环境通常有多个核心,但别用
-j$(nproc)。我建议用cmake --build . -j2,避免把CI runner跑死。 - 分离配置和构建:把
cmake -B和cmake --build分成两个步骤。这样配置失败时不会浪费构建时间。 - 测试结果要输出:
ctest --output-on-failure这个参数一定要加。不然CI只告诉你测试失败,不告诉你为什么失败。
CI/CD流水线结构图
下面这张图展示了典型的CMake CI流水线流程。我把它画成了SVG,方便你理解各阶段的关系:
常见问题与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| Windows上路径包含空格 | CMake未正确处理带空格的路径 | 使用cmake -B build -S .,避免在路径中使用空格 |
| macOS上找不到OpenMP | Apple Clang默认不包含OpenMP | 安装libomp,或使用brew install llvm的clang |
| CI中测试超时 | 测试用例在CI环境中运行较慢 | 设置ctest --timeout 60,或优化测试用例 |
| 缓存未命中导致构建慢 | 缓存key设计不合理 | 使用${{ runner.os }}-${{ hashFiles('**/CMakeLists.txt') }}作为key |
我的个人习惯:在CI中,我会额外加一个cmake --build build --target install步骤。这样能提前发现安装规则的问题,而不是等到发布时才发现install()命令写错了。
好了,CI/CD集成这块就聊这么多。说白了,核心就是让CMake在CI里跑得跟本地一样顺畅。你只要把依赖装好、缓存配好、矩阵策略设对,剩下的CMake自己就能搞定。嗯,动手试试吧。