
前阵子业务上需要在一台 Ubuntu 20.04 机器上把 AOSP 9.0.0 完整编一遍用来验证一个 framework 补丁在 Android Pie 上的表现。说实话这个组合听起来有点复古AOSP 9.0.0 发布的时候Ubuntu 20.04 还没有发布官方文档里推荐的宿主系统是 16.04 / 18.04 那一代等到了 20.04系统源里的软件已经把它当成老古董。结果就是整个过程中依赖不兼容、磁盘爆满、内存被 OOM killer 点名这些问题轮着来。这篇文章把整个流程和踩过的坑原原本本记下来给还需要在老版本 AOSP 上做 ROM、做系统级开发验证的人一个参考。1. 从零开始规划为什么 AOSP 9 配 Ubuntu 20.04 会踩这么多坑1.1 AOSP 9.0.0 在当下的真实处境AOSP 9.0.0 对应 Android Pie2018 年发布构建系统已经全面转向 Soong Ninja虽然还保留了大量 Makefile 兼容层但真正的构建图是由 Kati 转成 Ninja 文件执行的。这个变化决定了它的编译过程和 Android 6、7 那种纯 make 时代不一样很多“老教程”里教的命令已经不完全适用但也有不少老依赖仍然存在。真正麻烦的是宿主系统。Ubuntu 20.04 的 glibc、gcc、OpenJDK 都比 AOSP 9 时代新了大版本AOSP 自带的 prebuilt 工具链大多还停留在旧版本上。编译时很多错误并不是代码问题而是构建脚本在“新系统 旧构建链”的组合下找不到正确的解释器、库文件或 Java 版本。所以你对这个组合要有个预期环境准备阶段花的时间很可能比编译本身还长。另外 AOSP 9 的官方构建文档虽然简单但它写的是 Ubuntu 16.04/18.04 时代的命令直接复制到 20.04 上会当场翻车。比如官方推荐的 lib32ncurses5-dev 这个包在 20.04 里已经不存在了官方要求 openjdk-820.04 默认源里也没有。这些都是要提前解决的问题而不是等到编译报错再去搜。1.2 硬件规划别在第一步就把自己劝退编译 AOSP 是典型的资源饥饿型任务硬件准备不足会浪费大量时间。我的建议是内存最低 16GB32GB 会比较轻松。内存不够时编译过程会出现莫名其妙的中断报错信息往往只有一行 “Killed” 或 “ninja: build stopped”后面讲排查坑的时候会详细说。磁盘AOSP 9 完整源码同步完大概 50GB 左右out 目录在编完一个 target 后也要几十到上百 GB加上 git 对象、ccache 缓存你至少要给 300GB 以上的空间。我见过有人把源码放在 256GB 硬盘上同步完就只剩 20GB编到一半直接 No space left on device。文件系统用 ext4 这类大小写敏感的 Linux 文件系统别放在 NTFS 移动硬盘或者某些挂载参数有问题的目录上。AOSP 构建过程会生成大量符号链接和大小写敏感的文件文件系统不支持就会产生一堆诡异错误。我把目标目录放在 /home 下的专用分区里而不是根目录。原因很简单根分区通常比较小一旦 out 目录把根分区塞满连系统日志都写不了恢复起来非常麻烦。如果你的 home 和根分区没有分开那至少单独准备一块数据盘挂载到专门目录下再开始操作。1.3 明确要编的 targetuser、userdebug、eng第一次接触 AOSP 的人容易忽略 lunch 后面那个后缀的含义导致编译完了才发现不是自己想要的版本。三种变体的区别还是很明显的变体root 能力调试信息体积适合场景user无少小接近正式版验证跑 CTS/GTSuserdebug支持 adb root中等中日常开发调试系统 app 修改验证eng完整 root shell多大底层调试、功能扩展我这次选的是 userdebug因为要验证 framework patch需要 adb root 权限同时又不希望体积膨胀得太夸张。如果你需要跑兼容性测试建议 user如果只是自己折腾内核或者做驱动验证eng 也没问题只是编出来的镜像更大刷机后首次开机也更慢。另外目标产品也会直接影响你 lunch 的参数。只跑模拟器用 aosp_x86_64 就够了真机则要根据设备代号选比如 Pixel 一代对应 aosp_sailfish。选错 target 编出的 boot 和设备树不匹配刷进真机基本起不来。2. 环境准备Ubuntu 20.04 上补齐老版本依赖的完整清单2.1 基础编译链一次性安装先装基础工具。在 Ubuntu 20.04 上执行sudo apt update sudo apt install -y \ git gnupg flex bison build-essential zip curl \ zlib1g-dev gcc-multilib g-multilib libc6-dev-i386 \ lib32z1-dev libgl1-mesa-dev libxml2-utils xsltproc \ unzip fontconfig ccache这里面 gcc-multilib 和 g-multilib 是为了编译 32 位 host 工具准备的。AOSP 9 的某些 host 工具仍然以 32 位存在缺了 multilib会在编译早期报找不到 gcc 的头文件。ccache 后面会用到建议提前装好二次构建速度的提升非常可观。有些教程会另外装 lib32readline-dev、lib32ncurses-dev 之类的包20.04 上暂时不需要因为 libncurses5 我们要单独处理。2.2 OpenJDK 8、Python 2 与 libncurses5 的“老三样”这三个组件是 Ubuntu 20.04 和 AOSP 9 之间最大的坑。OpenJDK 820.04 默认没有 openjdk-8 包。AOSP 9 的 Java 相关工具链要求的是 JDK 8如果你用系统自带的 JDK 11 或者更高版本构建过程中会报 Unsupported class file major version 或者 javac 参数不识别。我的做法是添加 openjdk-r PPAsudo add-apt-repository ppa:openjdk-r/ppa sudo apt update sudo apt install -y openjdk-8-jdk然后确认默认 java 是 1.8 开头的版本。如果不想用 PPA也可以从旧版 Ubuntu 的归档仓库拉取 openjdk-8 的 deb 包手动安装原理一样只是依赖处理要自己多花点时间。Python 2.7AOSP 9 的构建脚本还有相当一部分依赖 Python 2。Ubuntu 20.04 默认只有 Python 3.8必须手动把 python2 装上并且让 /usr/bin/python 指向 python2。直接执行sudo apt install -y python2 sudo ln -s /usr/bin/python2.7 /usr/local/bin/python如果你机器上有多个 python 版本建议用 update-alternatives 管理避免以后其他项目需要 python3 时被这个软链接搞乱。libncurses520.04 的软件源里默认只有 libncurses6但 AOSP 9 的 prebuilt 工具链里很多二进制是链接到 libncurses5 的。你可以在 20.04 的源里碰碰运气如果没有就去 Ubuntu 18.04 的软件包仓库下载 libncurses5 的 deb 安装。装完之后用 ldconfig -p | grep ncurses 验证一下 libncurses.so.5 是否存在。2.3 repo 工具与 git 初始化repo 是 AOSP 用来管理上百个 git 仓库的工具。Ubuntu 20.04 的软件源里直接有 repo 包sudo apt install -y repo如果源里没有或者你想用别的来源也可以手动把 repo 脚本放到 ~/bin 下加 PATH。repo 本质上是一个 Python 脚本后续源码同步时会再自举更新只要你机器上 Python 环境正确差别不大。然后设置 git 身份这一步很多人容易忘等到第一次同步代码时才报错git config --global user.name Your Name git config --global user.email youexample.com还可以把 git 的自动换行关掉避免仓库内文件被错误转换git config --global core.autocrlf false2.4 swap 文件花十分钟换回一晚上的安稳AOSP 编译过程中链接阶段会有多个进程同时占用大量内存物理内存一旦用完Linux 会发生两件事一是直接 OOM kill 掉正在编译的进程报一个让人摸不着头脑的 Killed二是开始疯狂 swap整个机器卡成 PPT。两者都要避免。最省事的做法就是提前准备一个大 swap 文件sudo fallocate -l 16G /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile sudo swapon /swapfile如果想要重启后依然生效把它加到 /etc/fstabecho /swapfile none swap sw 0 0 | sudo tee -a /etc/fstab如果机器本身有 32GB 内存我也建议保留 16GB swap。别小看编译时那几分钟的峰值内存多一层兜底总比 OOM 好。SSD 上 swap 性能虽然比内存慢但至少不会让编译直接中断。3. 同步源码分支选择、repo init 与下载时的细节3.1 选择 android-9.0.0_r1 还是其它安全补丁分支AOSP 的版本标签格式是 android-9.0.0_rNN 越大一般代表越靠后的安全补丁版本。如果你只是做一次系统镜像验证选 android-9.0.0_r1 这个初始发布版本就够了分支干净历史简单。如果你要适配某个厂商或者 Pixel 的特定安全补丁等级需要去 AOSP 的 manifest 仓库查看都有哪些标签然后选对应设备官方发布时用的那个 tag。这里有个容易犯的错误只留意大版本不核对设备代号。AOSP 9 的同一大版本下不同 tag 可能包含不同设备的预编译产物差异。为了保证编译出来的镜像能正常刷入最好选设备厂商在 release note 里指明的 tag。3.2 repo init 与 repo sync 实操创建源码目录并初始化mkdir -p ~/aosp9 cd ~/aosp9 repo init -u https://android.googlesource.com/platform/manifest -b android-9.0.0_r1-u 指定 manifest 仓库地址-b 指定分支。manifest 文件里记录的是几百个 git 仓库各自的提交点这样无论谁同步拿到的都是同一套代码保证可复现。如果官方仓库访问速度不理想可以把 -u 参数换成国内外维护的 AOSP 镜像地址常见的是高校或云厂商长期维护的镜像站。替换地址后后续所有操作完全一致。同步命令是repo sync -j8 -c-j8 指用 8 个并发任务拉取数字最好和 CPU 核数相当。-c 表示只同步当前分支能省掉大量不必要的历史分支数据。第一次同步耗时取决于网速快则一两个小时慢则半天同步过程中断了直接重新执行这条命令repo 会断点续传这点不用慌。如果有人指导你“先 repo init 再 repo sync”但同步过程中卡住可以先检查网络和磁盘空间。磁盘不足时 repo sync 的表现往往是某个仓库下载失败重试多次也没用。提前用 df -h 看好剩余容量比反复重试更有效。3.3 同步完成后的自检清单同步结束后先别急着 source envsetup.sh花几分钟检查一下源码根目录下应该有 build/、art/、frameworks/、packages/ 等目录缺一不可。.repo 目录存在并且在目录列表里能看到 manifest 相关文件。执行 repo status 不会有大量异常输出。用 du -sh 看一下源码总体积是否在预期范围内。我最常犯的错误是同步完就立刻开始 build结果早期阶段报“缺少 build/envsetup.sh”一查才发现某个仓库没同步完整。repo sync 虽然断点续传但如果本地仓库损坏它可能不会自动发现。遇到这种情况删掉出问题的仓库目录再重新 repo sync 是更快的办法。4. 构建流程envsetup、lunch、make 的每一步都在干什么4.1 envsetup 与 lunch 目标选择源码同步完成后进入源码目录source build/envsetup.sh这个脚本会加载大量 shell 函数包括 lunch、m、mm、mmm 这些快捷命令。接下来选择编译目标lunch aosp_x86_64-userdebuglunch 会做两件事一是设置 TARGET_PRODUCT、TARGET_BUILD_VARIANT 等环境变量二是把这些配置写入 out/ 目录后续所有构建步骤都读取这套配置。如果你不确定当前选的什么可以 echo $TARGET_PRODUCT 和 echo $TARGET_BUILD_VARIANT 检查。lunch 之后可以先跑一下相关命令确认环境没问题比如 env | grep TARGET。如果 lunch 提示找不到目标大概率是 target 名字写错了或者 envsetup.sh 没有 source 成功。4.2 make 的线程数与编译时间正式编译make -j8这里的 -j8 表示并行任务数。AOSP 9 的构建系统会把大量编译任务拆给 ninja并行太多会导致内存峰值飙升并行太少又浪费 CPU。我的经验是16GB 内存配 -j832GB 内存配 -j16超过 32GB 再往上加。如果虚拟机分配了 16 核但只有 16GB 内存别用 -j16一定会 OOM。首次全量编译的时长因硬件而异。SSD 16GB 内存 8 核的机器编 aosp_x86_64-userdebug 大约需要 2 到 3 个小时机械硬盘会成倍增加因为 AOSP 小文件极多I/O 很容易成为瓶颈。开始编译后别急着干别的先盯一下前五分钟的日志。最开始的配置阶段就会暴露很多环境问题比如找不到 Python 2 的 shebang 错误、缺少某个库文件等。这些问题如果不管可能跑半个小时后才以更复杂的报错形式出现。4.3 输出产物解读编译结束后产物都在 out/target/product/ / 下最常用的文件是boot.img内核 ramdisk决定设备能否开机system.img系统分区镜像vendor.img厂商分区镜像userdata.img用户数据分区镜像ramdisk.img初始内存盘对 AOSP 9 来说如果目标产品没有启用动态分区out 目录里不会出现 super.img而是传统的 system.img、vendor.img、userdata.img 分开的镜像。如果你的产品树里配置了动态分区产物里会有 super.img那是 Android 10 以后成为主流的机制AOSP 9 可选。另外out/host/linux-x86/bin 下会生成 adb、fastboot、mkbootimg 等主机工具这些工具是编译过程中自动生成的可以直接拿来刷机使用。很多时候排查问题只需要用新编译的 boot.img 和 system.img而不是整套镜像。5. 我实际撞上的三个坑报错信息与排查链路5.1 No space left on deviceinode 和 /tmp 双重埋伏第一次编译过程中报错信息是 write error (No space left on device)。我第一反应是 df -h 查容量结果发现根分区和 out 所在分区都还有几十 GB 剩余容量根本不是问题。接着用 df -i 一看inode 使用率已经 100%。原因很简单AOSP 源码加编译产物的小文件数量极其庞大对于默认 inode 数量不够的分区文件系统会在没用完容量的时候就拒绝创建新文件。这种情况在分区较小、或者格式化参数用了 -i 指定了小 inode 比例的文件系统上特别常见。解决办法要么是换一块更大的分区要么是重新格式化时用默认参数让 inode 数量按容量自动分配。另一个隐蔽的坑是 /tmp。如果 out 目录所在分区没问题但 /tmp 挂载在独立 tmpfs 上空间很小编译过程中同样会报 No space left on device而且日志里不会明确指出是哪个目录写不下了。遇到这种情况先把 TMPDIR 指到源码目录下一个专门的临时目录mkdir -p ~/aosp9/tmp export TMPDIR~/aosp9/tmp这个经验帮了大忙因为我编译时 /tmp 只有 2GB而构建过程会往里面写大量临时文件。5.2 ninja 被 OOM killer 干掉第二次遇到的报错更迷惑make 跑了大半个小时突然输出 Killed然后整个终端回到 shell。日志里没有明确的编译失败信息倒回去看也只看到 ninja: build stopped: subcommand failed.。这时候要做的第一件事是 dmesg | tail -100 | grep -i kill看内核是不是真的杀了进程。如果看到 Out of memory: Killed process … 这类信息说明内存不够是根因而不是代码或者依赖有问题。我的处理方式是先给系统加 swap见 2.4然后把并行任务数降下去从 -j16 改成 -j8。如果还压不住继续降到 -j4。AOSP 编译的峰值内存通常出现在链接阶段而不是编译阶段所以并行任务过多很容易在同一时间点触发多个链接器内存瞬间被打满。另外ccache 虽然对重复编译很有帮助但它本身也会占一些内存内存极度紧张的时候可以临时关掉。5.3 Java 环境不干净导致的诡异 setup 失败AOSP 9 的构建链里还保留着 Jack 的兼容层如果你的构建日志里出现 jack server 相关的字样说明部分 Java 模块仍然走的是 Jack 流程。在 Ubuntu 20.04 下面最常见的报错是 Communication error with Jack server或者 Jack server 启动后马上退出。排查链路是这样的先看 java -version确认是 1.8.0_xxx而不是 11 或 17。如果默认 Java 版本不对jdk 相关工具就会以不兼容的方式运行。再看 echo $JAVA_HOME 是否指向 openjdk-8 的目录。最后看 out/host/linux-x86/bin 下是否生成了 jack 可执行文件。如果确实是 Jack 的问题我用的解决方式是手动重启 Jack serverexport ANDROID_JACK_VM_ARGS-Xmx4096m -Dfile.encodingUTF-8 ./out/host/linux-x86/bin/jack-admin kill-server ./out/host/linux-x86/bin/jack-admin start-server这里 -Xmx4096m 表示给 Jack server 4GB 堆内存。内存只有 16GB 的话可以设成 -Xmx2048m同时建议别把 make 的并行任务开太大Jack 并发和 ninja 并行同时吃内存会更紧张。如果确认环境变量没问题但仍然反复挂把 out/ 目录下跟 jack 相关的状态目录删掉rm -rf out/host/linux-x86/jack然后重新 make让构建系统重新生成 Jack 状态。这个操作不影响已经编译好的产物但会重新跑一遍依赖的 setup 阶段。6. 编译完成后怎么验证与刷机6.1 先用 emulator 验证最快反馈回路如果编的是 x86_64 这种模拟器友好目标最简单的方式是直接启动模拟器source build/envsetup.sh lunch aosp_x86_64-userdebug emulatoremulator 会自动读取当前 out 目录中的镜像启动模拟器。没有图形界面的话可以加 -no-window但那样只能通过 adb 操作看不到实际 UI。启动后先用 adb wait-for-device 等待设备起来再用 adb shell getprop ro.build.fingerprint 确认系统版本。模拟器的好处是反馈周期短不用每次改动都刷真机。我验证 framework patch 时基本都是在模拟器上确认行为没问题再刷真机做最终兼容性检查。6.2 在实体设备上刷入产物与 fastboot 命令真机刷机要确认目标产品选对了。以 aosp_sailfish-userdebug 为例编译完成后 out/target/product/sailfish/ 下的 boot.img、system.img、vendor.img、userdata.img 就是需要刷入的文件。先重启到 bootloader用 fastboot 执行fastboot flash boot boot.img fastboot flash system system.img fastboot flash vendor vendor.img fastboot flash userdata userdata.img fastboot reboot需要注意的是刷机前备份用户数据userdata.img 刷入会清空 /data。如果是 A/B 分区设备命令可能要加上 _a 或 _b 槽位后缀或者用 fastboot flashall 配合分区脚本具体看设备本身的分区配置。第一次刷入自编译镜像后设备可能启动很慢甚至出现开机动画循环几轮的情况别急着下结论。自编译镜像没有厂商的完整优化和 SELinux 策略调整首次启动时间比正式版长是正常现象。如果超过十分钟还卡在开机动画再用 adb logcat 慢慢排查。6.3 构建缓存与二次编译提速首编完成只是第一步后续针对同一个源码树做二次构建时ccache 的作用会非常明显。我配的是export USE_CCACHE1 export CCACHE_EXEC$(which ccache) ccache -M 100G100GB 是一个相对合理的缓存上限能覆盖大部分系统 Java 和 C/C 编译产物。注意 ccache 缓存也是占磁盘空间的如果磁盘紧张把它设成 30G 也行。我实测下来改一个 framework 的 Java 文件后再次 makeccache 命中率高的时候全量编译流程几分钟就能完成。当然这指的是增量编译不是重新 clean 后全量编译。每次都 clean 再编的人等于把前面所有经验都扔掉了。整个项目走下来我最大的体会是AOSP 编译本质上是个环境工程源码和构建链只是其中一环。AOSP 9.0.0 在 Ubuntu 20.04 下编译真正难的不是 make 过程本身而是把老版本依赖在 20.04 上重新拼凑齐。如果你也准备做这件事先按文中的环境准备清单把 JDK 8、Python 2、libncurses5、swap 都配好再去碰源码和构建。最后再分享一个小技巧把每次报错前的最后 200 行日志存下来。很多看似玄学的编译失败重新翻日志后都能找到先兆比如某个头文件缺失之前往往会有一串 warning。这个习惯跟着我从 AOSP 9 用到了后续版本每次都能救急。