一、构建系统与CI/CD:为什么它们必须在一起
说实话,我见过太多团队把构建系统和CI/CD割裂开来对待。他们觉得CMake是CMake,CI是CI,各管各的。结果呢?每次新成员入职,光配环境就要花一整天。我个人习惯是,从项目第一天就把构建系统和CI/CD当成一个整体来设计。
你想想看,构建系统解决的是「怎么编译」,CI/CD解决的是「自动编译、测试、部署」。这两者天然就是搭档。今天我们就聊聊,怎么把CMake和主流的CI平台——GitLab CI、GitHub Actions——深度集成起来。
核心观点:构建系统是CI/CD的「内功」,CI/CD是构建系统的「外功」。两者配合好了,团队效率翻倍。
1.1 GitLab CI 集成 CMake:一个实战案例
我在一个嵌入式项目中用过GitLab CI + CMake的组合。那个项目有3个目标平台:x86_64 Linux、ARM Cortex-A、还有Windows。如果没有CI,每次手动切换工具链,我估计得疯掉。
下面是一个典型的.gitlab-ci.yml配置,它展示了如何用CMake做多平台构建:
stages:
- build
- test
- package
.build_template: &build_definition
stage: build
before_script:
- apt-get update && apt-get install -y cmake ninja-build
script:
- mkdir -p build
- cd build
- cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release
- ninja
artifacts:
paths:
- build/
expire_in: 1 week
build:linux:
<<: *build_definition
tags:
- linux
build:arm:
<<: *build_definition
tags:
- arm-runner
before_script:
- apt-get update && apt-get install -y cmake ninja-build gcc-arm-linux-gnueabihf
script:
- mkdir -p build
- cd build
- cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=../arm-toolchain.cmake
- ninja
这里有个小技巧:我用YAML的锚点(&build_definition)把公共部分抽出来了。这样每个平台的构建只需要覆盖差异部分。嗯,这个做法我用了好几年,维护成本极低。
我的建议:在GitLab CI中,尽量把CMake的配置参数(比如工具链文件路径、构建类型)通过CI变量传递,而不是硬编码在脚本里。这样换平台时只需要改变量,不用改CI配置。
1.2 GitHub Actions 多平台构建:矩阵才是王道
GitHub Actions的矩阵构建(matrix)是我最喜欢的特性之一。说白了,就是一次定义,自动生成多个构建任务。我曾经维护过一个库,需要同时支持Ubuntu、macOS、Windows,还有Debug和Release两种配置。
用矩阵配置,代码量直接减半:
name: Multi-platform Build
on: [push, pull_request]
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
build_type: [Debug, Release]
include:
- os: ubuntu-latest
cmake_generator: "Unix Makefiles"
- os: macos-latest
cmake_generator: "Unix Makefiles"
- os: windows-latest
cmake_generator: "Visual Studio 17 2022"
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Configure CMake
run: cmake -B build -G "${{ matrix.cmake_generator }}" -DCMAKE_BUILD_TYPE=${{ matrix.build_type }}
- name: Build
run: cmake --build build --config ${{ matrix.build_type }}
- name: Test
run: ctest --test-dir build -C ${{ matrix.build_type }}
你看,6个构建任务(3个OS × 2个构建类型)就这么搞定了。每个任务都是独立的,并行执行。我算过,原来手写6个job需要200多行YAML,现在不到40行。
注意:矩阵配置虽然方便,但别滥用。如果你的矩阵有4个维度(比如OS、架构、构建类型、编译器),那组合数会爆炸。我一般控制在2-3个维度,超过的话,考虑用动态矩阵或者分阶段构建。
1.3 缓存策略:ccache 和 sccache 的正确用法
说到CI,最让人头疼的就是构建时间。尤其是C/C++项目,每次全量编译动辄半小时。我早期在GitHub Actions上跑一个项目,每次push都要等25分钟。后来加了ccache,直接降到8分钟。
ccache的原理很简单:它缓存编译器的中间产物(.o文件)。如果源文件没变,就直接从缓存拿。sccache是Mozilla出的,支持分布式缓存,适合大型团队。
下面是在GitHub Actions中使用ccache的配置:
steps:
- name: Cache ccache
uses: actions/cache@v3
with:
path: ~/.ccache
key: ${{ runner.os }}-ccache-${{ hashFiles('**/*.cpp', '**/*.h') }}
restore-keys: |
${{ runner.os }}-ccache-
- name: Configure CMake with ccache
run: |
cmake -B build \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache
这里有个坑:缓存key的设计很关键。我用hashFiles对源文件做哈希,这样只要源文件变了,缓存就失效。但如果你只改了一个注释,哈希也会变,缓存就浪费了。我个人习惯是,把哈希粒度放宽一点,比如只对src/目录下的文件做哈希。
对于sccache,配置稍微复杂一点,但支持跨机器共享缓存:
steps:
- name: Setup sccache
run: |
cargo install sccache
echo "SCCACHE_BUCKET=my-cache-bucket" >> $GITHUB_ENV
echo "SCCACHE_REGION=us-east-1" >> $GITHUB_ENV
- name: Build with sccache
run: |
export CC="sccache gcc"
export CXX="sccache g++"
cmake -B build
cmake --build build
避坑指南:我曾经在sccache上踩过一个坑——缓存命中率很低。后来发现是因为编译器版本不一致。本地用GCC 11,CI用GCC 12,缓存永远不命中。所以,确保所有环境的编译器版本一致,这是缓存生效的前提。
1.4 构建矩阵配置:不只是OS和架构
很多人以为构建矩阵就是「不同操作系统 + 不同架构」。其实远不止这些。我常用的矩阵维度包括:
- 编译器版本:GCC 11、GCC 12、Clang 16
- 构建类型:Debug、Release、RelWithDebInfo
- 标准版本:C++17、C++20、C++23
- 特性开关:WITH_SSL、WITH_GPU、WITH_AVX2
下面是一个更复杂的矩阵配置示例:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
compiler: [gcc, clang]
standard: [17, 20]
exclude:
- os: windows-latest
compiler: clang
include:
- os: ubuntu-latest
compiler: gcc
cc: gcc-12
cxx: g++-12
- os: ubuntu-latest
compiler: clang
cc: clang-16
cxx: clang++-16
这里用了exclude来排除不支持的组合(Windows + Clang),用include来添加每个组合的具体编译器路径。这种配置方式,说白了就是「先定义规则,再处理例外」。
1.5 知识体系总览
下面这张图,是我对本章知识体系的总结。它展示了构建系统、CI平台、缓存策略和矩阵配置之间的关系:
1.6 一些实战心得
最后,分享几个我踩过的坑和总结的经验:
- 缓存不是银弹。ccache对头文件修改很敏感。如果你频繁改头文件,缓存命中率会很低。我建议把公共头文件单独放一个目录,减少变动频率。
- 矩阵配置要「先窄后宽」。刚开始只跑2-3个关键组合,等稳定了再扩展。我见过一个团队上来就配了20个组合,结果CI排队排到天荒地老。
- 本地和CI的环境要一致。我吃过最大的亏就是本地用Ubuntu 20.04,CI用22.04,结果链接器版本不同,链接失败。现在我都用Docker镜像来统一环境。
- 善用CI的并行能力。CMake的
--parallel选项配合CI的多核机器,构建时间能再降30%。
一句话总结:构建系统和CI/CD的集成,本质上是在「自动化」和「灵活性」之间找平衡。CMake给了你灵活性,CI给了你自动化,而缓存和矩阵配置,就是让这个平衡更高效的工具。