4. CI/CD平台选型与流水线骨架
固件项目的CI/CD与纯软件项目有一个根本差异:构建产物不是可执行文件或容器镜像,而是需要烧录到真实芯片上的二进制固件。这意味着流水线不仅要完成编译,还要处理交叉编译工具链、签名密钥、硬件在环测试等环节。平台选型的第一步,是判断哪个系统能最自然地承载这些环节。
三大平台的核心差异
Jenkins、GitLab CI、GitHub Actions三者的架构模型不同,直接决定了Runner的部署方式和流水线的可维护性。
Jenkins是自托管为主的老牌方案。它的核心是Jenkins Controller加Agent节点,所有构建逻辑通过Jenkinsfile或界面配置定义。优势在于对私有网络、自定义硬件(如连接J-Link的测试机)的接入非常灵活,插件生态覆盖嵌入式工具链。代价是Controller需要自行维护、升级和安全加固,插件版本冲突是常见故障源。
GitLab CI与代码仓库深度绑定,流水线由仓库根目录的.gitlab-ci.yml定义,Runner通过注册令牌接入。它的强项是环境隔离和缓存机制成熟,适合已经在用GitLab管理固件源码的团队。Runner可以部署在Docker、Shell或Kubernetes执行器中,其中Shell执行器最适合需要直接访问USB调试器的固件测试场景。
GitHub Actions采用托管Runner加自托管Runner混合模式,工作流由.github/workflows/下的YAML定义。托管Runner开箱即用,但无法访问本地硬件;固件构建通常需要自托管Runner,并配合runs-on标签路由到带工具链的机器。它的Marketplace Action复用度高,但第三方Action的供应链安全需要额外审查——这对固件安全课程而言是一个必须纳入考量的风险点。
Runner部署的关键决策
Runner是流水线真正执行构建的机器。固件项目对Runner有三个硬性要求:能访问交叉编译工具链、能访问签名密钥、能连接目标硬件。这决定了Runner不能随意使用公共托管资源。
以GitLab Runner为例,注册命令如下:
gitlab-runner register \
--url https://gitlab.example.com/ \
--registration-token $RUNNER_TOKEN \
--executor shell \
--tag-list "firmware,arm,jlink" \
--description "firmware-build-runner-01"
这里选择shell执行器而非docker,原因是固件测试阶段需要直接访问宿主机的USB设备。如果使用Docker执行器,必须额外配置--device参数挂载J-Link,且容器内需要安装对应的udev规则,复杂度显著上升。标签firmware,arm,jlink用于在流水线中精确路由任务,避免普通软件构建占用固件构建机。
Jenkins的Agent接入方式类似,通过java -jar agent.jar -jnlpUrl ...启动,但需要在Controller端预先配置节点标签和工具路径。GitHub Actions的自托管Runner则通过仓库Settings中的Runner注册流程获取令牌,执行./config.sh --url ... --token ...完成接入,之后在workflow中用runs-on: [self-hosted, firmware]指定。
流水线即代码的落地方式
“流水线即代码”意味着构建逻辑必须与源码一起版本控制,而不是在Web界面里点击配置。三个平台都支持这一模式,但文件格式和表达能力不同。
Jenkins使用Jenkinsfile,支持声明式和脚本式两种语法。声明式更适合固件团队,因为它的stages结构清晰,便于审查。GitLab CI使用.gitlab-ci.yml,通过stages、script、artifacts等关键字定义。GitHub Actions使用workflow YAML,以jobs和steps为核心组织单元。
无论哪个平台,固件流水线的骨架都应包含以下阶段:源码检出(含子模块)、工具链准备、编译构建、签名与哈希、产物归档。下面是一个可直接参考的GitLab CI示例:
stages:
- checkout
- build
- sign
- package
variables:
TOOLCHAIN: arm-none-eabi-
BUILD_DIR: build/release
checkout:
stage: checkout
script:
- git submodule update --init --recursive
build:
stage: build
tags: [firmware, arm]
script:
- cmake -B $BUILD_DIR -DCMAKE_TOOLCHAIN_FILE=toolchain.cmake
- cmake --build $BUILD_DIR -j$(nproc)
artifacts:
paths:
- $BUILD_DIR/*.bin
- $BUILD_DIR/*.hex
- $BUILD_DIR/*.map
expire_in: 30 days
sign:
stage: sign
tags: [firmware, arm]
script:
- python3 tools/sign_firmware.py $BUILD_DIR/firmware.bin
- sha256sum $BUILD_DIR/firmware_signed.bin > $BUILD_DIR/sha256sum.txt
artifacts:
paths:
- $BUILD_DIR/firmware_signed.bin
- $BUILD_DIR/sha256sum.txt
这个骨架中有几个固件特有的设计点。第一,checkout阶段显式执行子模块更新,因为固件项目常把HAL库、RTOS、加密库作为子模块管理。第二,build阶段通过CMAKE_TOOLCHAIN_FILE指定交叉编译工具链,而不是依赖Runner的全局环境变量,这样流水线在不同Runner上行为一致。第三,sign阶段独立成阶段,签名密钥通过CI/CD变量注入,不写入代码仓库。第四,产物中保留.map文件,便于后续分析固件体积和符号布局。
创建首个固件构建流水线的实操要点
首次搭建时,建议先用一个最小可运行版本验证链路,再逐步增加签名和硬件测试。最小版本只需要checkout和build两个阶段,确认Runner能正确调用交叉编译工具链并产出.bin文件。
验证时重点观察三件事:编译日志中工具链路径是否正确、产物文件是否出现在artifacts中、流水线耗时是否在可接受范围。如果编译失败,优先检查Runner上的工具链版本与项目要求是否一致——这是固件CI中最常见的环境问题。
arm-none-eabi-gcc安装到/opt/toolchain/10.3-2021.10/并在流水线中显式引用该路径,可以避免因Runner系统升级导致工具链漂移,从而保证固件构建的可复现性。
当最小流水线跑通后,再引入签名阶段和硬件在环测试阶段。签名阶段需要提前在CI/CD变量中配置密钥路径和口令,硬件测试阶段则需要为Runner打上jlink标签并确保测试脚本能通过pyocd或JLinkExe烧录固件。每一步都应保持“先验证、再扩展”的节奏,避免一次性引入过多变量导致排障困难。