22、构建系统与CI/CD集成:Jenkins/GitLab CI配置、编译脚本自动化、构建产物管理
说实话,AOSP 构建系统本身已经够复杂了。但真正让它发挥威力的,是把它接入到 CI/CD 流水线里。我见过太多团队,本地编译跑得飞起,一上 CI 就各种翻车。说白了,构建系统与 CI/CD 的集成,考验的不是你会不会配 Jenkins,而是你对整个构建流程的理解深度。
这一章,我就带你走一遍我在实际项目中踩过的坑,以及最终沉淀下来的最佳实践。
22.1 为什么需要 CI/CD 集成?
你想想看,一个 AOSP 项目,少说几十个模块,多则上百。每次提交代码,如果都靠人工去编译、测试、打包,那效率太低了。更关键的是,人总会犯错——漏编译某个模块、忘记更新依赖、用了过时的工具链……
CI/CD 要解决的核心问题就三个:
- 自动化编译:代码一提交,自动触发全量或增量编译
- 持续验证:编译通过后,自动跑单元测试、静态检查、集成测试
- 产物管理:编译出来的镜像、SDK、APK 等,自动归档、版本化、可追溯
我个人习惯把 CI/CD 比作「构建系统的安全带」。平时觉得它碍事,但真出问题的时候,它能救命。
22.2 Jenkins 配置实战
Jenkins 是老牌 CI 工具了,在 AOSP 场景下依然很能打。我曾在某手机厂商维护过一套 Jenkins 集群,专门用来编译不同芯片平台的系统镜像。
22.2.1 基础环境准备
首先,Jenkins 节点上必须装好 AOSP 编译所需的所有工具链。这里有个坑:Jenkins 默认的工作空间路径不能有空格,否则 source build/envsetup.sh 会报错。
# Jenkins 节点初始化脚本
sudo apt-get install -y git-core gnupg flex bison build-essential \
zip curl zlib1g-dev gcc-multilib g++-multilib libc6-dev-i386 \
libncurses5 lib32ncurses5-dev x11proto-core-dev libx11-dev \
lib32z1-dev libgl1-mesa-dev libxml2-utils xsltproc unzip
# 配置 JDK(AOSP 12 以上需要 JDK 11)
sudo update-alternatives --config java
22.2.2 Pipeline 脚本示例
我推荐用 Declarative Pipeline,可读性好,也方便版本控制。下面是一个简化版的 AOSP 编译 Pipeline:
pipeline {
agent { label 'aosp-builder' }
parameters {
string(name: 'BRANCH', defaultValue: 'android-13.0.0_r30', description: 'AOSP 分支')
choice(name: 'TARGET', choices: ['aosp_arm64-eng', 'aosp_x86_64-userdebug'], description: '编译目标')
}
stages {
stage('Checkout') {
steps {
// 注意:AOSP 源码巨大,建议用 repo 工具
sh '''
repo init -u https://android.googlesource.com/platform/manifest -b ${BRANCH}
repo sync -j$(nproc)
'''
}
}
stage('Build') {
steps {
sh '''
source build/envsetup.sh
lunch ${TARGET}
make -j$(nproc) 2>&1 | tee build.log
'''
}
post {
failure {
// 编译失败时,保留日志
archiveArtifacts artifacts: 'build.log', allowEmptyArchive: true
}
}
}
stage('Archive') {
steps {
sh '''
# 打包产物
tar -czf out/target/product/*/system.img.tar.gz out/target/product/*/system.img
'''
archiveArtifacts artifacts: 'out/target/product/*/system.img.tar.gz'
}
}
}
post {
always {
// 清理工作空间,避免磁盘爆满
cleanWs()
}
}
}
22.3 GitLab CI 配置实战
如果你团队用的是 GitLab,那 GitLab CI 是更自然的选择。它的 Runner 可以跑在 Docker 容器里,隔离性更好。
22.3.1 .gitlab-ci.yml 示例
variables:
AOSP_BRANCH: "android-13.0.0_r30"
TARGET_PRODUCT: "aosp_arm64"
TARGET_BUILD_VARIANT: "eng"
stages:
- checkout
- build
- test
- deploy
checkout:
stage: checkout
script:
- repo init -u https://android.googlesource.com/platform/manifest -b $AOSP_BRANCH
- repo sync -j$(nproc)
artifacts:
paths:
- .repo/
expire_in: 1 day
build:
stage: build
needs: [checkout]
script:
- source build/envsetup.sh
- lunch ${TARGET_PRODUCT}-${TARGET_BUILD_VARIANT}
- make -j$(nproc)
artifacts:
paths:
- out/target/product/${TARGET_PRODUCT}/
expire_in: 1 week
test:
stage: test
needs: [build]
script:
- echo "Running CTS tests..."
- ./cts-tradefed run cts --plan CTS
artifacts:
reports:
junit: out/cts/test_result.xml
deploy:
stage: deploy
needs: [test]
script:
- echo "Uploading images to artifact server..."
- scp out/target/product/${TARGET_PRODUCT}/*.img deploy@artifacts:/data/images/
only:
- tags
22.4 编译脚本自动化
不管是 Jenkins 还是 GitLab CI,核心都是编译脚本。我见过最糟糕的做法是把所有逻辑都写在 Pipeline 的 script 块里。一旦脚本超过 50 行,维护起来就是噩梦。
我的建议是:把编译逻辑抽成独立的 Shell 脚本,放在 AOSP 源码的 vendor/yourcompany/build/ 目录下。
22.4.1 自动化编译脚本示例
#!/bin/bash
# vendor/yourcompany/build/auto_build.sh
# 用途:自动化编译 AOSP,支持增量编译和产物打包
set -e
AOSP_ROOT=$(cd "$(dirname "$0")/../../.."; pwd)
BUILD_LOG="${AOSP_ROOT}/build_$(date +%Y%m%d_%H%M%S).log"
PRODUCT_OUT="${AOSP_ROOT}/out/target/product"
echo "[INFO] Starting AOSP build at $(date)" | tee -a ${BUILD_LOG}
# 1. 环境检查
if [ ! -f "${AOSP_ROOT}/build/envsetup.sh" ]; then
echo "[ERROR] Not in AOSP root directory!" | tee -a ${BUILD_LOG}
exit 1
fi
# 2. 设置编译环境
source ${AOSP_ROOT}/build/envsetup.sh
lunch ${TARGET_PRODUCT}-${TARGET_BUILD_VARIANT}
# 3. 增量编译(如果上次编译产物还在)
if [ -d "${PRODUCT_OUT}/${TARGET_PRODUCT}" ]; then
echo "[INFO] Incremental build..." | tee -a ${BUILD_LOG}
make -j$(nproc) 2>&1 | tee -a ${BUILD_LOG}
else
echo "[INFO] Full build..." | tee -a ${BUILD_LOG}
make -j$(nproc) 2>&1 | tee -a ${BUILD_LOG}
fi
# 4. 检查编译结果
if [ ${PIPESTATUS[0]} -ne 0 ]; then
echo "[ERROR] Build failed!" | tee -a ${BUILD_LOG}
exit 1
fi
# 5. 打包产物
echo "[INFO] Packaging artifacts..." | tee -a ${BUILD_LOG}
cd ${PRODUCT_OUT}/${TARGET_PRODUCT}
tar -czf ${AOSP_ROOT}/out/aosp_${TARGET_PRODUCT}_$(date +%Y%m%d).tar.gz \
system.img boot.img vendor.img userdata.img
echo "[INFO] Build completed at $(date)" | tee -a ${BUILD_LOG}
22.5 构建产物管理
产物管理是 CI/CD 里最容易忽视、但后果最严重的一环。我曾经遇到过:编译出来的镜像没打版本号,结果 QA 拿错版本测了一周,最后发现是两周前的旧镜像。
22.5.1 产物命名规范
| 产物类型 | 命名格式 | 示例 |
|---|---|---|
| 系统镜像 | aosp_{product}_{variant}_{date}_{build_id}.tar.gz | aosp_arm64_eng_20250315_12345.tar.gz |
| SDK | sdk_{api_level}_{date}_{build_id}.zip | sdk_33_20250315_12345.zip |
| APK | {module_name}_{version}_{variant}_{date}.apk | Settings_13.0_eng_20250315.apk |
| 测试报告 | test_report_{date}_{build_id}.xml | test_report_20250315_12345.xml |
22.5.2 产物存储策略
我推荐用分层存储:
- 热存储:最近 7 天的产物,放在 CI 服务器本地或 NAS 上,方便快速取用
- 温存储:7-30 天的产物,压缩后存到对象存储(如 MinIO、S3)
- 冷存储:超过 30 天的产物,归档到磁带或低成本存储,仅保留元数据
关键点:每个产物必须附带一个 manifest 文件,记录编译时的 Git commit、环境变量、编译参数。这样出了问题,你能精确回溯到是哪一次编译、哪一行代码导致的。
22.6 知识体系总览
下面这张图,是我对本章内容的一个总结。你可以看到,CI/CD 集成不是简单地把 Jenkins 和 AOSP 拼在一起,而是一个从代码提交到产物交付的完整链路。
22.7 避坑指南
最后,分享几个我亲身踩过的坑:
- 磁盘空间:AOSP 一次全量编译需要至少 200GB 空间。我曾经因为没做磁盘监控,导致 CI 节点磁盘写满,整个集群挂了半天。解决方案是:每次编译前检查磁盘剩余空间,不足时自动清理最旧的产物。
- 并发控制:多个 Job 同时编译同一个分支,可能会因为 .repo 目录的锁冲突而失败。我建议每个分支独占一个工作空间,或者用 Docker 容器隔离。
- 超时处理:AOSP 编译偶尔会卡住(比如某个模块死锁)。一定要设置 Job 超时,并开启「超时后自动中止」。我一般设 3 小时,超过就发告警。
- 产物一致性:不同 Job 编译出来的产物,如果依赖的 commit 不同,可能会导致集成测试失败。我建议在产物 manifest 里记录所有模块的 commit hash,方便排查。
嗯,CI/CD 集成这块,说白了就是「自动化一切,记录一切」。你投入的每一分精力,都会在后续的版本迭代中加倍回报。