4、CI/CD入门:搭建Jenkins/GitLab CI流水线,实现代码提交后自动编译
嵌入式开发里,编译烧录这件事,说白了就是重复劳动。你改一行代码,点一下编译,等两分钟,再烧录,再测试。一天下来,光等编译就能耗掉一两个小时。
我刚开始带项目的时候,团队五个人,每天手动编译,经常出现「我本地能编译过啊」这种经典对话。后来发现,有人忘了更新子模块,有人用了不同的工具链版本。嗯,那时候我就意识到——必须上CI。
这一章,咱们就聊聊怎么搭一套能用的CI流水线。我会用Jenkins和GitLab CI两种方案来讲,因为这两种我在项目里都用过,各有各的适用场景。
4.1 CI/CD到底解决什么问题?
先别急着装工具。你得想清楚,你为什么要上CI?
我个人习惯把CI的核心价值总结成三句话:
- 代码一提交,自动编译 —— 不用等人手动敲命令
- 编译失败,立刻通知 —— 谁搞坏的谁负责修
- 产物可追溯 —— 每个版本都能找到对应的二进制和源码
说白了,CI就是给你的代码仓库配了一个24小时不休息的「编译机器人」。你只管push,它负责编译、打包、甚至烧录。
核心指标:从代码提交到编译完成,全自动,5分钟内给出结果。超过这个时间,开发者就会开始分心去做别的事,反馈循环就断了。
4.2 两种主流方案:Jenkins vs GitLab CI
我在不同项目里用过这两种方案。给你一个直观的对比:
| 对比维度 | Jenkins | GitLab CI |
|---|---|---|
| 部署方式 | 独立服务器,需手动安装 | 集成在GitLab中,开箱即用 |
| 配置方式 | Web界面 + Jenkinsfile | .gitlab-ci.yml 文件 |
| 插件生态 | 极其丰富,几乎什么都能做 | 相对简洁,够用 |
| 学习曲线 | 中等偏陡 | 平缓,YAML文件易上手 |
| 适合场景 | 复杂流水线、多平台、已有Jenkins基础设施 | 团队已用GitLab、中小型项目 |
我个人建议:如果团队已经在用GitLab,直接上GitLab CI,省一台服务器。如果项目复杂,需要对接各种硬件、测试设备,Jenkins的插件生态会让你省很多事。
4.3 方案一:搭建GitLab CI流水线
GitLab CI的配置核心就是一个文件:.gitlab-ci.yml。放在项目根目录,GitLab会自动识别。
先看一个嵌入式项目的典型例子:
# .gitlab-ci.yml
stages:
- build
- test
- archive
build-job:
stage: build
image: arm-none-eabi-gcc:latest
script:
- mkdir -p build
- cd build
- cmake .. -DCMAKE_TOOLCHAIN_FILE=../arm-gcc-toolchain.cmake
- make -j$(nproc)
artifacts:
paths:
- build/firmware.hex
- build/firmware.bin
expire_in: 1 week
test-job:
stage: test
script:
- echo "Running unit tests..."
- ./build/test_runner
only:
- main
- develop
archive-job:
stage: archive
script:
- cp build/firmware.hex firmware-${CI_COMMIT_SHORT_SHA}.hex
- cp build/firmware.bin firmware-${CI_COMMIT_SHORT_SHA}.bin
artifacts:
paths:
- firmware-*.hex
- firmware-*.bin
expire_in: 1 month
这个流水线做了三件事:
- 编译:用arm-none-eabi-gcc镜像,执行cmake + make
- 测试:只在main和develop分支跑单元测试
- 归档:把编译产物带上commit SHA命名,保存一个月
小技巧:我习惯在artifacts里加上expire_in,不然硬盘很快会被历史产物塞满。曾经有个项目,半年没清理,CI服务器磁盘直接爆了……
4.4 方案二:搭建Jenkins流水线
Jenkins的配置稍微复杂一点,但灵活性更高。我推荐用Pipeline as Code的方式,也就是写Jenkinsfile,而不是在Web界面里点来点去。
为什么?因为Jenkinsfile可以跟代码一起版本管理,改流水线就像改代码一样,有历史记录,能回滚。
来看一个嵌入式项目的Jenkinsfile:
// Jenkinsfile
pipeline {
agent any
environment {
TOOLCHAIN_PATH = '/opt/arm-gcc'
BUILD_DIR = 'build'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
steps {
sh '''
mkdir -p ${BUILD_DIR}
cd ${BUILD_DIR}
cmake .. -DCMAKE_TOOLCHAIN_FILE=${TOOLCHAIN_PATH}/arm-gcc-toolchain.cmake
make -j$(nproc)
'''
}
post {
success {
archiveArtifacts artifacts: 'build/*.hex,build/*.bin'
}
failure {
emailext(
subject: "Build Failed: ${env.JOB_NAME} - ${env.BUILD_NUMBER}",
body: "Check build at ${env.BUILD_URL}",
to: "${env.CHANGE_AUTHOR_EMAIL}"
)
}
}
}
stage('Unit Test') {
when {
branch 'main'
}
steps {
sh './build/test_runner'
}
}
}
post {
always {
cleanWs()
}
}
}
这里有个细节:post块里的failure,我配置了邮件通知。谁提交的代码导致编译失败,邮件直接发到他的邮箱。这招很管用,团队里没人敢随便push不编译的代码了。
注意:Jenkins默认的工作空间不会自动清理。时间长了,每个构建的临时文件会越积越多。我建议在post块里加上cleanWs(),每次构建完清理工作空间。不然你可能会在某天早上发现服务器硬盘满了。
4.5 核心流程:从代码提交到编译完成
不管用哪种方案,整个流程的逻辑是一样的。我画了一张图,帮你理清思路:
4.6 避坑指南:我踩过的几个坑
CI这东西,看着简单,真用起来全是细节。分享几个我亲身踩过的坑:
- 工具链版本不一致:CI服务器上装的arm-gcc版本跟本地不一样,编译出来的二进制行为不同。我后来强制在Docker镜像里固定工具链版本,才彻底解决。
- 缓存问题:Jenkins默认不缓存编译中间文件,每次全量编译,一个项目要等15分钟。加上
ccache之后,增量编译只需要2分钟。 - 权限问题:CI用户没有烧录设备的权限,流水线卡在烧录那一步。这个我折腾了一下午才发现是udev规则没配好。
- 产物过期:GitLab CI的artifacts默认30天过期。有一次客户要半年前的固件版本,我翻了好久才从备份里找到。现在我会把正式发布的产物单独存到长期存储里。
我的建议:刚开始搭CI的时候,别追求一步到位。先让流水线能编译通过,再慢慢加测试、加通知、加部署。迭代着来,比一次搞个大而全的流水线靠谱得多。
4.7 小结
这一章我们聊了CI/CD的核心价值,对比了Jenkins和GitLab CI两种方案,给出了具体的配置示例,还画了完整的流程示意图。
你想想看,以前每次改完代码都要手动编译、等结果、再手动烧录。现在只要push一下,CI自动帮你搞定。省下来的时间,用来写更好的代码,或者——嗯,喝杯咖啡也挺好。