持续集成:GitHub Actions, GitLab CI, Jenkins 配置跨平台编译与测试

跨平台开发最头疼的是什么?我个人觉得,不是代码怎么写,而是怎么保证代码在 Windows、Linux、macOS 上都能跑起来。你想想看,本地开发环境再完美,换台机器就翻车的情况我见得太多了。

持续集成(CI)就是来解决这个问题的。说白了,每次你提交代码,CI 自动帮你拉代码、编译、跑测试,一气呵成。今天我就聊聊三个主流工具:GitHub Actions、GitLab CI 和 Jenkins。它们各有各的脾气,但核心思路是一样的。

核心要点:CI 的本质是「自动化验证」。跨平台 CI 的核心是「矩阵构建」—— 同一份代码,在多个操作系统、多个编译器下同时跑。

跨平台 CI 核心流程 开发者提交代码 CI 自动触发 Linux (GCC/Clang) macOS (Clang) Windows (MSVC/MinGW) 生成测试报告

GitHub Actions:最省心的选择

如果你的代码托管在 GitHub 上,GitHub Actions 是最直接的选择。它不需要额外搭建服务器,直接在 GitHub 的虚拟机里跑。我个人习惯把跨平台编译写成矩阵策略,一行配置就能搞定。

来看一个实际例子。这是一个 C 语言项目的 CI 配置,支持 Linux、macOS、Windows 三平台编译:

# .github/workflows/cross-platform.yml
name: Cross-Platform Build & Test

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        compiler: [gcc, clang]
        exclude:
          - os: macos-latest
            compiler: gcc
          - os: windows-latest
            compiler: clang

    runs-on: ${{ matrix.os }}

    steps:
    - uses: actions/checkout@v4

    - name: Setup Compiler
      run: |
        if [ "${{ matrix.compiler }}" = "gcc" ]; then
          echo "CC=gcc" >> $GITHUB_ENV
          echo "CXX=g++" >> $GITHUB_ENV
        else
          echo "CC=clang" >> $GITHUB_ENV
          echo "CXX=clang++" >> $GITHUB_ENV
        fi
      shell: bash

    - name: Build
      run: |
        mkdir build && cd build
        cmake .. -DCMAKE_C_COMPILER=${{ env.CC }}
        cmake --build .
      shell: bash

    - name: Test
      run: |
        cd build
        ctest --output-on-failure
      shell: bash

小技巧:exclude 字段用来排除不合理的组合。比如 macOS 上 GCC 其实是 Clang 的别名,Windows 上 Clang 需要额外配置。我刚开始没注意这个,结果 macOS 的 GCC 跑出来一堆奇怪警告,排查了半天才发现是编译器版本问题。

GitLab CI:自托管玩家的首选

GitLab CI 和 GitHub Actions 思路类似,但配置语法略有不同。它用 .gitlab-ci.yml 文件,支持更灵活的 Runner 管理。如果你有自己的服务器,可以注册自定义 Runner,跑在物理机上性能更好。

我曾经在一个嵌入式项目里用过 GitLab CI。那项目需要在 ARM 交叉编译环境下测试,GitHub 的公共 Runner 不支持,但 GitLab 的自定义 Runner 完美解决了这个问题。

# .gitlab-ci.yml
stages:
  - build
  - test

variables:
  BUILD_DIR: build

.cross_platform_template: &cross_platform
  before_script:
    - apt-get update -qq
    - apt-get install -y -qq cmake gcc g++ clang

linux-gcc:
  stage: build
  image: ubuntu:22.04
  script:
    - mkdir -p $BUILD_DIR && cd $BUILD_DIR
    - cmake .. -DCMAKE_C_COMPILER=gcc
    - cmake --build .
  artifacts:
    paths:
      - $BUILD_DIR/
  tags:
    - linux-runner

linux-clang:
  stage: build
  image: ubuntu:22.04
  script:
    - mkdir -p $BUILD_DIR && cd $BUILD_DIR
    - cmake .. -DCMAKE_C_COMPILER=clang
    - cmake --build .
  artifacts:
    paths:
      - $BUILD_DIR/
  tags:
    - linux-runner

test:
  stage: test
  image: ubuntu:22.04
  needs: ["linux-gcc", "linux-clang"]
  script:
    - cd $BUILD_DIR
    - ctest --output-on-failure
  tags:
    - linux-runner

注意:GitLab CI 的 artifacts 机制很实用。编译产物可以传递给后续 job,避免重复编译。但要注意 artifacts 过期时间,默认是 30 天,太大会占磁盘空间。我有个项目忘了设置过期时间,结果 Runner 磁盘被撑爆了……

Jenkins:老牌劲旅,灵活但重

Jenkins 是 CI 领域的老大哥。它不绑定任何代码托管平台,什么都能接。但配置起来相对复杂,需要自己维护 Jenkins 服务器。说实话,小项目用 Jenkins 有点杀鸡用牛刀的感觉,但大型企业项目里它依然是主流。

Jenkins 的跨平台编译通常用 Pipeline 脚本(Jenkinsfile)来实现。下面是一个声明式 Pipeline 的例子:

// Jenkinsfile
pipeline {
    agent none

    stages {
        stage('Build on Linux') {
            agent {
                label 'linux-node'
            }
            steps {
                checkout scm
                sh '''
                    mkdir -p build && cd build
                    cmake .. -DCMAKE_C_COMPILER=gcc
                    cmake --build .
                '''
                stash includes: 'build/**', name: 'linux-build'
            }
        }

        stage('Build on Windows') {
            agent {
                label 'windows-node'
            }
            steps {
                checkout scm
                bat '''
                    if not exist build mkdir build
                    cd build
                    cmake .. -DCMAKE_C_COMPILER=cl
                    cmake --build .
                '''
                stash includes: 'build/**', name: 'win-build'
            }
        }

        stage('Test') {
            agent {
                label 'linux-node'
            }
            steps {
                unstash 'linux-build'
                sh '''
                    cd build
                    ctest --output-on-failure
                '''
            }
        }
    }

    post {
        always {
            junit 'build/test-results/**/*.xml'
        }
    }
}

关键区别:Jenkins 的 agent 机制允许你指定不同的执行节点。Linux 编译在 Linux 节点上跑,Windows 编译在 Windows 节点上跑。stash/unstash 用于在节点间传递文件,相当于 GitLab 的 artifacts。

跨平台编译的常见坑

配置 CI 只是第一步,真正头疼的是跨平台编译本身。我踩过不少坑,挑几个典型的说说:

  • 路径分隔符:Windows 用反斜杠 \,Linux/macOS 用正斜杠 /。代码里硬编码路径会直接炸。解决方案是用 #include <filesystem>(C++17)或 CMake 的 file(TO_CMAKE_PATH)
  • 动态库后缀:Linux 是 .so,macOS 是 .dylib,Windows 是 .dll。CMake 的 MODULESHARED 要分清。
  • 编译器差异:MSVC 对 C99/C11 的支持比较「有个性」。我遇到过 MSVC 不支持 strdup() 函数,后来用 _strdup() 加宏定义绕过去的。
  • 换行符:Windows 是 \r\n,Linux 是 \n。Git 的 autocrlf 设置要统一,否则编译时会出现莫名其妙的语法错误。

我的经验:在项目根目录放一个 CMakeLists.txt,用 CMake 管理构建。CMake 本身跨平台,能自动检测编译器、路径、库依赖。配合 CI 的矩阵构建,一套配置跑遍所有平台。我最近三个项目都是这么干的,省心不少。

测试策略:不止是编译通过

编译通过不代表程序能跑。跨平台测试要覆盖几个层面:

测试类型 说明 CI 中的实现
单元测试 验证函数级逻辑 CTest + 单元测试框架(如 Unity、Criterion)
集成测试 验证模块间交互 自定义测试脚本,模拟实际调用链
平台特性测试 验证平台相关代码 条件编译 + 平台标记,只在对应平台运行
性能回归测试 防止性能退化 基准测试框架(如 Google Benchmark),对比历史数据

举个例子,如果你的代码里有 #ifdef _WIN32 的分支,那 Windows 上的测试必须覆盖这个分支。Linux 上跑再多测试也发现不了 Windows 的 bug。所以 CI 的矩阵策略里,每个平台都要跑完整的测试套件。

曾经踩过的坑:我有一个网络库,Linux 上一切正常,Windows 上却频繁超时。排查了半天,发现是 Windows 的 socket 默认超时时间和 Linux 不一样。后来在测试代码里显式设置了超时参数,问题才解决。所以平台特性测试一定要做细。

总结一下

GitHub Actions 适合 GitHub 用户,配置简单,开箱即用。GitLab CI 适合自托管场景,Runner 灵活可控。Jenkins 适合大型企业,高度可定制但维护成本高。

不管你选哪个工具,核心思路是一样的:用矩阵构建覆盖多平台,用自动化测试保证质量。跨平台开发没有银弹,但 CI 能帮你把问题提前暴露出来,而不是等到用户反馈才手忙脚乱。

嗯,今天就聊到这里。配置 CI 的时候遇到具体问题,欢迎随时交流。

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