第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
我的经验:矩阵构建里,我建议把Linux和macOS统一用Ninja,Windows用VS生成器。为什么?因为Ninja在跨平台项目里表现最稳定,而且增量编译速度比Make快不少。Windows上用VS生成器是因为有些Windows特有的API需要MSVC编译器。

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
注意:GitLab CI里,stage之间的artifacts传递要注意大小。我曾经有一次把整个build目录(包括第三方库)都传了,结果artifact有2GB,CI直接崩了。建议只传必要的二进制和测试报告。

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里用shbat步骤调用原生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_DIRPROJECT_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脚本本身的可移植性。

CMake CI/CD 集成 GitHub Actions GitLab CI Jenkins 配置 (Configure) 构建 (Build) 测试 (Test) 通用技巧:缓存管理 · 路径处理 · 并行控制 · 报告生成 核心原则:CMake脚本保持平台无关,CI配置只负责调用

28.8 小结

说白了,CI里的CMake脚本编写,核心就一句话:让CMake脚本本身不依赖任何CI特性。你在本地能跑通的CMakeLists.txt,放到任何CI里都应该能跑。CI配置文件只负责调用cmake和ctest,以及处理缓存、报告这些外围事情。

我个人建议,刚开始做CI集成时,先在一个CI上跑通,再扩展到其他平台。别一上来就搞矩阵构建,否则排查问题会让你怀疑人生。嗯,我就干过这种事,结果花了整整一天才发现是某个环境变量没传进去。

最后,记得在项目README里写上CI的构建状态徽章。这玩意儿看着小,但能极大提升项目的可信度。别人一看「build passing」,心里就有底了。


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