第28章 自动化构建CI:Jenkins/GitLab CI配置、多设备构建、版本发布

说实话,做嵌入式Android系统开发,最怕什么?不是代码写不出来,而是每次手动编译、打包、刷机,一套流程下来大半天就没了。我早年在一家IoT公司带队做平板固件,那时候团队就三个人,每次发版都得熬夜盯着终端,生怕哪个环节断了没人知道。后来我实在受不了了,花了两周把Jenkins流水线搭起来,从那以后,晚上十点之后我基本不看手机——构建挂了会自动发邮件,第二天早上来修就行。

这一章,我就把CI/CD在AOSP定制中的实战经验掰开揉碎讲给你听。说白了,就是让机器替你干那些重复、枯燥、容易手滑的活儿。

为什么AOSP构建特别需要CI?

你想想看,一个完整的AOSP工程,从repo sync到生成刷机包,快的也要一两个小时。如果每次改个驱动或者加个系统服务,都要手动敲命令等结果,那效率太低了。更关键的是,多人协作时,你没法保证每个人本地的编译环境完全一致。我见过最离谱的一次,同事的Ubuntu是中文版,locale没设对,编译出来的系统UI全是乱码——这种问题在CI环境里根本不会出现。

所以,CI的核心价值就两条:自动化标准化。自动化解放人力,标准化消灭玄学。

CI工具选型:Jenkins vs GitLab CI

这两个我都深度用过。简单说下我的感受:

对比维度 Jenkins GitLab CI
部署方式 需要单独服务器,安装配置较复杂 集成在GitLab中,开箱即用
插件生态 极其丰富,几乎什么都能干 相对精简,但够用
Pipeline语法 Declarative / Scripted Pipeline YAML格式,学习成本低
多设备支持 通过节点标签灵活分配 需要注册多个Runner
适合场景 大型团队,复杂构建矩阵 中小团队,与GitLab深度绑定

我个人习惯是:如果公司已经用了GitLab,那就直接用GitLab CI,省心。如果团队超过20人,或者需要同时维护多个Android版本(比如Android 12和13并行),Jenkins的矩阵构建能力会更顺手。

Jenkins Pipeline实战:AOSP多设备构建

先看一个我实际用过的Jenkinsfile片段。这个流水线支持同时构建三个设备:平板、车机、电视棒。

pipeline {
    agent any

    parameters {
        choice(name: 'DEVICE', choices: ['all', 'tablet', 'car', 'tvstick'], description: '选择目标设备')
        string(name: 'BUILD_TAG', defaultValue: 'daily', description: '构建标签')
    }

    stages {
        stage('初始化环境') {
            steps {
                sh '''
                    export PATH=/opt/android-sdk/platform-tools:$PATH
                    source build/envsetup.sh
                '''
            }
        }

        stage('并行构建') {
            parallel {
                stage('平板') {
                    when { expression { params.DEVICE == 'all' || params.DEVICE == 'tablet' } }
                    steps {
                        sh 'lunch aosp_tablet-userdebug && make -j$(nproc) 2>&1 | tee build_tablet.log'
                    }
                }
                stage('车机') {
                    when { expression { params.DEVICE == 'all' || params.DEVICE == 'car' } }
                    steps {
                        sh 'lunch aosp_car_x86-userdebug && make -j$(nproc) 2>&1 | tee build_car.log'
                    }
                }
                stage('电视棒') {
                    when { expression { params.DEVICE == 'all' || params.DEVICE == 'tvstick' } }
                    steps {
                        sh 'lunch aosp_tvstick-arm64-userdebug && make -j$(nproc) 2>&1 | tee build_tvstick.log'
                    }
                }
            }
        }

        stage('打包与发布') {
            steps {
                script {
                    def devices = ['tablet', 'car', 'tvstick']
                    devices.each { device ->
                        sh """
                            cd out/target/product/${device}
                            zip -r ${BUILD_TAG}_${device}_ota.zip *
                            cp ${BUILD_TAG}_${device}_ota.zip /mnt/nfs/releases/
                        """
                    }
                }
            }
        }
    }

    post {
        failure {
            emailext(
                subject: "AOSP构建失败: ${env.BUILD_TAG}",
                body: "设备: ${params.DEVICE}\n请查看构建日志: ${env.BUILD_URL}",
                to: 'team@company.com'
            )
        }
    }
}
我的经验:并行构建时,一定要确保Jenkins节点有足够的内存和CPU。我曾经在只有32GB内存的机器上同时跑三个lunch,结果OOM把系统都搞崩了。后来我改成每个设备独占一个节点,用标签来调度。

GitLab CI配置:轻量级方案

如果你团队用的是GitLab,那配置起来更简洁。下面是一个.gitlab-ci.yml的例子,它会在每次推送代码时自动构建指定设备,并把产物上传到Release页面。

variables:
  DEVICE_LIST: "tablet car tvstick"

stages:
  - build
  - package
  - release

before_script:
  - source build/envsetup.sh

build:tablet:
  stage: build
  script:
    - lunch aosp_tablet-userdebug
    - make -j$(nproc)
  artifacts:
    paths:
      - out/target/product/tablet/
    expire_in: 1 week
  only:
    - tags

build:car:
  stage: build
  script:
    - lunch aosp_car_x86-userdebug
    - make -j$(nproc)
  artifacts:
    paths:
      - out/target/product/car/
    expire_in: 1 week
  only:
    - tags

package:
  stage: package
  needs: ["build:tablet", "build:car"]
  script:
    - for device in tablet car; do
        cd out/target/product/$device;
        zip -r ${CI_COMMIT_TAG}_${device}_ota.zip *;
        mv *.zip ../../..;
      done
  artifacts:
    paths:
      - "*.zip"
    expire_in: 1 month

release:
  stage: release
  needs: ["package"]
  script:
    - echo "发布版本 ${CI_COMMIT_TAG}"
  release:
    name: "Release ${CI_COMMIT_TAG}"
    tag_name: ${CI_COMMIT_TAG}
    description: "自动构建的AOSP固件包"
    assets:
      links:
        - name: "平板固件"
          url: "${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/file/tablet_ota.zip"
        - name: "车机固件"
          url: "${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/file/car_ota.zip"
注意:GitLab CI的Runner默认并发数是1。如果你要同时构建多个设备,记得在Runner配置里设置concurrent=4或者更高。否则,build:tablet和build:car会排队执行,那就失去并行意义了。

多设备构建的坑与解法

我在项目中遇到过最头疼的问题,是不同设备对编译工具链的依赖冲突。比如平板用clang-14,车机必须用clang-12,但AOSP的prebuilt工具链是全局的。怎么解?

我的做法是:为每个设备创建独立的编译容器。用Docker封装好各自的环境,CI里直接拉取对应镜像。这样既隔离了依赖,又方便扩展新设备。

另一个常见问题是磁盘空间。AOSP一次完整构建能吃掉200GB以上,如果同时构建三个设备,磁盘很快就爆了。我建议在Jenkins里配置构建后清理策略:只保留最近两次的out目录,其余的自动删除。GitLab CI则可以利用artifacts的expire_in字段来控制保留时间。

版本发布:自动化签名与OTA

构建完只是第一步,怎么把固件安全地发出去才是关键。我习惯在CI流水线里集成签名步骤:

stage('签名') {
    steps {
        withCredentials([file(credentialsId: 'release-key', variable: 'KEYSTORE')]) {
            sh '''
                java -jar signapk.jar -w $KEYSTORE password \
                    out/target/product/tablet/system.img \
                    out/target/product/tablet/system_signed.img
            '''
        }
    }
}

签名用的密钥绝对不能硬编码在脚本里。Jenkins用Credentials Binding插件,GitLab CI用CI/CD Variables的File类型,都是安全的方式。

发布时,我还会自动生成OTA差分包。这样用户升级时只需要下载几十MB的增量包,而不是整个系统镜像。生成差分包的脚本我放在AOSP源码的build/tools/releasetools/下,CI里直接调用ota_from_target_files即可。

知识体系总览

下面这张图是我自己整理的CI/CD在AOSP定制中的核心流程,你可以把它当作搭建流水线的路线图:

AOSP CI/CD 自动化构建流程 代码推送 / PR Jenkins/GitLab CI 并行构建 平板 车机 电视棒 ... 打包 签名 版本发布 / OTA 构建失败自动通知 支持多设备并行构建、自动签名、OTA差分生成 基于Jenkins Declarative Pipeline / GitLab CI YAML

避坑指南:我踩过的三个大坑

  • 缓存问题:我曾经因为ccache没配置好,导致每次构建都全量编译,白白浪费了三个小时。后来我在CI脚本里加了export USE_CCACHE=1 && ccache -M 50G,构建时间直接缩短了60%。
  • 并发冲突:多个构建同时写同一个out目录,会出现奇怪的链接错误。我的解决方案是让每个构建使用独立的输出目录,比如make OUT_DIR=out_${DEVICE}_${BUILD_ID}
  • 密钥泄露:有一次我不小心把签名密钥打印到了构建日志里,虽然马上撤销了,但想想都后怕。从那以后,所有敏感信息我都用CI的secret变量管理,日志里用set +x关闭命令回显。

总结

自动化构建CI这件事,投入产出比非常高。你花一周时间搭好流水线,后面每个月能省下几十个小时的手动操作。而且,标准化流程能大幅减少「在我机器上能跑啊」这种扯皮问题。

我个人建议,先从单设备CI开始跑通,再逐步扩展到多设备并行。别一上来就想搞个大而全的矩阵,容易把自己绕晕。记住,CI的核心是稳定可靠,不是花里胡哨。

核心要点回顾:

  • Jenkins适合复杂矩阵和多节点调度,GitLab CI适合与代码仓库深度集成
  • 多设备构建一定要做环境隔离(Docker或独立输出目录)
  • 签名密钥永远不要硬编码,用CI的凭据管理功能
  • 构建产物要设置自动清理策略,防止磁盘写满
  • OTA差分包能大幅降低用户升级成本,值得集成到流水线中

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