第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 }}-

我建议用ccachesccache来加速重复编译。第一次构建可能慢,但后续的增量构建能快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平台交叉编译

每个节点上我都装了ccachedistcc,进一步加速编译。嗯,这里要注意: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 合理的矩阵策略

矩阵不是越大越好。我建议按「风险等级」来设计:

  1. 核心矩阵(每次提交必跑):Ubuntu+GCC 12、Windows+MSVC 2022、macOS+Clang 15
  2. 扩展矩阵(每日或每周跑):GCC 11/13、Clang 14/16、ARM64交叉编译
  3. 边界矩阵(版本发布前跑):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_PATHrpath。macOS上则是DYLD_LIBRARY_PATHinstall_name_tool

解决方案:在CMake里设置CMAKE_INSTALL_RPATH,或者用BUILD_RPATH让构建目录下的库能被找到。

坑3:编译器特有的警告

MSVC的/W4和GCC的-Wall -Wextra不是一回事。有些代码在GCC上零警告,在MSVC上能报几十个。

解决方案:在CI里对每个编译器都开启最高警告级别,并且把-Werror/WX加上。警告就是潜在的bug,别放过。

29.6 知识体系总览

我把这一章的核心逻辑画成了图,方便你理解:

C++跨平台持续集成知识体系 代码提交 / PR GitHub Actions Jenkins Pipeline 矩阵测试策略 操作系统矩阵 编译器矩阵 C++标准矩阵 架构矩阵 构建类型矩阵 Linux/Win/macOS GCC/Clang/MSVC C++17/20/23 x86/ARM/RISC-V Debug/Release 构建流程:Checkout → 依赖安装 → CMake配置 → 编译 → 测试 缓存加速 (ccache) | 并行构建 | 测试报告生成 构建产物 / 测试报告 / 通知

29.7 总结

持续集成不是锦上添花,而是C++跨平台开发的必需品。我个人习惯是:项目初始化时就把CI配好,哪怕只有一个平台、一个编译器。因为CI一旦跑起来,后续的矩阵扩展就是加几行配置的事。

GitHub Actions适合中小型项目,配置简单,开箱即用。Jenkins适合大型企业,灵活可控,但需要专人维护。无论选哪个,矩阵测试的核心思路是一样的:用自动化覆盖所有目标平台,把兼容性问题扼杀在提交阶段

嗯,这一章就到这里。记住:CI不是终点,而是起点。它帮你省下的时间,可以用来做更有价值的事——比如优化算法、重构代码、或者早点下班。


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