第29章:持续集成:GitHub Actions、Jenkins跨平台构建、矩阵测试
持续集成,说白了就是让机器替你跑那些重复的活儿。
我早年做C++跨平台开发时,最头疼的就是「在我机器上能跑啊」这句话。Windows上编译通过,Linux上链接失败,macOS上符号找不到……每次手动测一遍,半天就没了。后来我下定决心,必须上CI。
这一章,我就把GitHub Actions和Jenkins这两套方案掰开揉碎讲清楚。尤其是跨平台构建和矩阵测试,这是咱们C++项目的刚需。
29.1 为什么C++项目特别需要CI?
C++的跨平台坑,比其他语言多得多。你想想看:
- 编译器不同:MSVC、GCC、Clang,各自有各自的方言
- 标准库实现不同:libstdc++、libc++、微软STL
- 构建系统不同:CMake、Makefile、Visual Studio解决方案
- 第三方库的ABI兼容性:一不小心就符号冲突
我在项目中遇到过最离谱的一次:Linux上编译通过,但运行时崩溃,原因是某个结构体的内存对齐方式在两个平台上不一致。这种问题,没有CI矩阵测试,根本发现不了。
核心原则:CI不是「跑一下测试」那么简单。对于C++跨平台项目,CI是你的「兼容性哨兵」。每次提交,都应该在目标平台上完整编译+运行测试。
29.2 GitHub Actions:轻量级跨平台CI
如果你的项目托管在GitHub上,GitHub Actions是最省心的选择。它原生支持Windows、Linux、macOS三大平台,而且免费额度对个人项目完全够用。
29.2.1 基础工作流结构
一个典型的C++跨平台CI工作流,长这样:
name: C++ Cross-Platform CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
compiler: [gcc, clang, msvc]
build_type: [Debug, Release]
exclude:
- os: windows-latest
compiler: gcc
- os: windows-latest
compiler: clang
- os: macos-latest
compiler: msvc
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Install dependencies (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y cmake ninja-build libboost-all-dev
- name: Install dependencies (macOS)
if: runner.os == 'macOS'
run: |
brew install cmake ninja boost
- name: Configure CMake
run: |
cmake -B build \
-DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \
-DCMAKE_CXX_COMPILER=${{ matrix.compiler }}
- name: Build
run: cmake --build build --config ${{ matrix.build_type }}
- name: Run tests
run: ctest --test-dir build --output-on-failure
这里有个关键点:矩阵策略。我通过strategy.matrix定义了操作系统、编译器、构建类型的组合。然后用exclude排除了不合理的组合——比如Windows上跑GCC,这在实际项目中很少见。
我的习惯:矩阵组合不要贪多。我一般只保留3-4个核心组合:Ubuntu+GCC、Ubuntu+Clang、Windows+MSVC、macOS+Clang。组合太多,排队时间太长,反而影响开发效率。
29.2.2 缓存策略
C++编译慢,这是共识。没有缓存,每次CI都要从头编译,等得让人抓狂。
- name: Cache CMake build
uses: actions/cache@v3
with:
path: |
build
~/.cache/ccache
key: ${{ runner.os }}-${{ matrix.compiler }}-${{ hashFiles('**/CMakeLists.txt', '**/*.cmake') }}
restore-keys: |
${{ runner.os }}-${{ matrix.compiler }}-
我建议用ccache或sccache来加速重复编译。第一次构建可能慢,但后续的增量构建能快3-5倍。
29.3 Jenkins:企业级跨平台构建
GitHub Actions虽好,但有些场景它搞不定:
- 项目代码在私有GitLab或Bitbucket上
- 需要连接内部硬件设备(比如嵌入式开发板)
- 对构建环境有严格的安全合规要求
这时候,Jenkins就派上用场了。我曾在某通信设备公司搭建过一套Jenkins集群,管理着20多个C++跨平台项目。
29.3.1 Jenkins Pipeline脚本
Jenkins的Pipeline即代码,用Groovy写。一个跨平台构建的Pipeline大概长这样:
pipeline {
agent none
stages {
stage('Matrix Build') {
matrix {
axes {
axis {
name 'OS'
values 'linux', 'windows', 'macos'
}
axis {
name 'COMPILER'
values 'gcc', 'clang', 'msvc'
}
axis {
name 'BUILD_TYPE'
values 'Debug', 'Release'
}
}
// 排除无效组合
exclude {
axis {
name 'OS'
values 'windows'
}
axis {
name 'COMPILER'
values 'gcc', 'clang'
}
}
exclude {
axis {
name 'OS'
values 'macos'
}
axis {
name 'COMPILER'
values 'msvc'
}
}
agent { label "${OS}-agent" }
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Configure') {
steps {
sh """
cmake -B build \
-DCMAKE_BUILD_TYPE=${BUILD_TYPE} \
-DCMAKE_CXX_COMPILER=${COMPILER}
"""
}
}
stage('Build') {
steps {
sh "cmake --build build --config ${BUILD_TYPE}"
}
}
stage('Test') {
steps {
sh "ctest --test-dir build --output-on-failure"
}
}
}
}
}
}
post {
always {
junit 'build/test-results/**/*.xml'
archiveArtifacts artifacts: 'build/**/*.log', allowEmptyArchive: true
}
failure {
emailext(
subject: "构建失败: ${env.JOB_NAME} - ${env.BUILD_NUMBER}",
body: "请查看构建日志: ${env.BUILD_URL}",
to: 'team@example.com'
)
}
}
}
注意:Jenkins的矩阵语法和GitHub Actions不同。它用matrix块包裹axes,而不是用strategy.matrix。另外,Jenkins的agent标签要提前配置好——你得有Linux、Windows、macOS的构建节点。
29.3.2 分布式构建节点
Jenkins的强项在于分布式。我当年搭建的集群是这样的:
| 节点名称 | 操作系统 | 编译器 | 用途 |
|---|---|---|---|
| linux-builder-01 | Ubuntu 22.04 | GCC 12, Clang 16 | Linux平台构建与测试 |
| win-builder-01 | Windows Server 2022 | MSVC 2022 | Windows平台构建 |
| mac-builder-01 | macOS Ventura | Apple Clang 15 | macOS/iOS构建 |
| arm64-builder-01 | Ubuntu 22.04 ARM64 | GCC 12 (交叉编译) | ARM平台交叉编译 |
每个节点上我都装了ccache和distcc,进一步加速编译。嗯,这里要注意:distcc在Windows上不太好使,所以Windows节点我单独配了Incredibuild。
29.4 矩阵测试:不只是编译
很多人以为矩阵测试就是「在不同平台上编译通过」。其实远不止如此。真正的矩阵测试,应该覆盖:
- 编译器版本矩阵:GCC 11、12、13,Clang 14、15、16
- 标准库矩阵:libstdc++、libc++
- C++标准矩阵:C++17、C++20、C++23
- 架构矩阵:x86_64、ARM64、RISC-V
- 构建配置矩阵:Debug、Release、RelWithDebInfo、MinSizeRel
我曾经踩过一个坑:项目在GCC 11上编译通过,但在GCC 12上因为std::format的实现差异导致编译错误。如果没有编译器版本矩阵,这个问题会一直潜伏到用户反馈。
29.4.1 合理的矩阵策略
矩阵不是越大越好。我建议按「风险等级」来设计:
- 核心矩阵(每次提交必跑):Ubuntu+GCC 12、Windows+MSVC 2022、macOS+Clang 15
- 扩展矩阵(每日或每周跑):GCC 11/13、Clang 14/16、ARM64交叉编译
- 边界矩阵(版本发布前跑):C++17模式、C++23模式、MinGW、MSVC 2019
这样既保证了快速反馈,又覆盖了兼容性风险。
29.5 跨平台构建的常见坑
说几个我实际遇到过的坑,你们可以提前避开:
坑1:路径分隔符
Windows用反斜杠\,Linux/macOS用正斜杠/。在CMake里用/是安全的,因为CMake会自动转换。但如果你在脚本里硬编码路径,比如./build\bin\test.exe,那在Linux上就炸了。
解决方案:永远用CMake的${CMAKE_BINARY_DIR}和${CMAKE_SOURCE_DIR},不要自己拼路径。
坑2:动态库搜索路径
Windows上DLL要放在exe同目录或PATH里。Linux上要用LD_LIBRARY_PATH或rpath。macOS上则是DYLD_LIBRARY_PATH或install_name_tool。
解决方案:在CMake里设置CMAKE_INSTALL_RPATH,或者用BUILD_RPATH让构建目录下的库能被找到。
坑3:编译器特有的警告
MSVC的/W4和GCC的-Wall -Wextra不是一回事。有些代码在GCC上零警告,在MSVC上能报几十个。
解决方案:在CI里对每个编译器都开启最高警告级别,并且把-Werror或/WX加上。警告就是潜在的bug,别放过。
29.6 知识体系总览
我把这一章的核心逻辑画成了图,方便你理解:
29.7 总结
持续集成不是锦上添花,而是C++跨平台开发的必需品。我个人习惯是:项目初始化时就把CI配好,哪怕只有一个平台、一个编译器。因为CI一旦跑起来,后续的矩阵扩展就是加几行配置的事。
GitHub Actions适合中小型项目,配置简单,开箱即用。Jenkins适合大型企业,灵活可控,但需要专人维护。无论选哪个,矩阵测试的核心思路是一样的:用自动化覆盖所有目标平台,把兼容性问题扼杀在提交阶段。
嗯,这一章就到这里。记住:CI不是终点,而是起点。它帮你省下的时间,可以用来做更有价值的事——比如优化算法、重构代码、或者早点下班。