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 -Bcmake --build分成两个步骤。这样配置失败时不会浪费构建时间。
  • 测试结果要输出ctest --output-on-failure这个参数一定要加。不然CI只告诉你测试失败,不告诉你为什么失败。

CI/CD流水线结构图

下面这张图展示了典型的CMake CI流水线流程。我把它画成了SVG,方便你理解各阶段的关系:

CMake CI/CD 流水线流程 代码检出 git checkout 安装依赖 apt / brew / vcpkg CMake 配置 cmake -B build 编译 cmake --build build 测试 ctest --output-on-failure 缓存策略 ccache / sccache actions/cache 矩阵构建 os × build_type Linux / macOS / Windows 实线:主流程 | 虚线:辅助配置

常见问题与解决方案

问题 原因 解决方案
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自己就能搞定。嗯,动手试试吧。

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