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

这个流水线做了三件事:

  1. 编译:用arm-none-eabi-gcc镜像,执行cmake + make
  2. 测试:只在main和develop分支跑单元测试
  3. 归档:把编译产物带上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 核心流程:从代码提交到编译完成

不管用哪种方案,整个流程的逻辑是一样的。我画了一张图,帮你理清思路:

CI/CD 自动编译流程 开发者 Push 代码 触发 CI 流水线 拉取最新代码 配置编译环境 执行编译脚本 生成 .hex/.bin 通知编译结果 归档产物到仓库 整个流程从 Push 到通知,理想情况下应控制在 5 分钟以内。超过 10 分钟,开发者就会切去做别的事。

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自动帮你搞定。省下来的时间,用来写更好的代码,或者——嗯,喝杯咖啡也挺好。