
干Android开发这些年身边每隔一段时间就有人问我“AOSP编译环境到底怎么搭为什么我照着教程装完Ubuntu一编译就各种报错”这个问题其实不复杂但网上教程要么太老要么只覆盖某一个Ubuntu版本初学者来回折腾几天很常见。我前前后后帮团队搭过不下十套AOSP构建环境从Ubuntu 16.04一路用到20.04从物理机到虚拟机再到Docker都试过踩过的坑基本都集中在几个点上依赖包没装全、JDK版本不对、磁盘空间不够、Jack服务起不来、Python环境缺失。这篇就把两代Ubuntu下的完整搭建流程和排障经验整理出来如果你正准备同步AOSP源码并编译出自己的ROM这篇可以直接当参考手册用。AOSPAndroid Open Source Project是Android的全面开源项目编译它跟编译普通App完全是两码事它更像是在本地完整“复刻”一条手机厂商的系统生产线。1. 动手前的关键决策机器、系统与方案取舍很多新手容易犯一个错误拿到编译文档就开始敲命令结果编到一半发现磁盘不够、内存爆掉、系统版本跟源码要求不匹配前面一个小时的下载和等待全部白费。动手之前我认为花十分钟把下方几个问题想清楚比盲目执行命令重要得多。1.1 AOSP编译的本质它到底在消耗什么资源AOSP全量编译本质上是一场I/O密集型、内存密集型和CPU密集型的“三重压力测试”。编译过程中GCC/Clang需要同时处理成千上万个源文件系统服务、框架层、Native层代码的中间产物都会被写入磁盘虚拟机或物理机的swap分区也会被反复读写。以我常用的中高端配置为例编译Android 11的AOSP全树代码时构建过程会同时产生几十个G的中间产物最终生成的system.img、vendor.img等镜像文件加起来也要占用十几个G。如果磁盘剩余空间低于一百个G编译大概率会在某个阶段因为“no space left on device”直接中断。内存方面我建议物理内存至少16GB起步如果只有8GB也不是完全不能编但必须预留足够大的swap分区否则很容易在链接阶段直接OOM。CPU核心数量直接影响编译耗时。拿一次干净的完整构建来说4核机器可能要跑四五个小时甚至更久8核可以压缩到两三个小时16核以上的机器能进一小时大关。AOSP编译对多核的支持比较成熟make命令里那个-j参数就是用来指定并行任务数的所以“核心数越多越好”在AOSP编译这件事上确实是成立的。1.2 Ubuntu 16.04 还是 20.04按源码分支选系统这是一个非常实际的问题。我见过不少同事拿着同一个编译文档有人用16.04编Android 11有人用20.04编Android 8结果双双失败最后发现是系统版本和源码版本不匹配。Google官方对AOSP各Android版本对应的Ubuntu版本有明确推荐我按自己的实操经验整理了一张对照表Android源码版本推荐系统主要依赖Android 6.0 ~ 8.1Ubuntu 16.04OpenJDK 8Python 2.7Android 9.0Ubuntu 16.04 / 18.04OpenJDK 8/11Python 2.7Android 10.0Ubuntu 18.04 / 20.04OpenJDK 11Python 3.x部分工具链仍需Python 2.7Android 11.0 / 12.0Ubuntu 18.04 / 20.04OpenJDK 11Python 3.x从这个表能看出一个规律Ubuntu 16.04的生态环境和旧版Android工具链匹配度很高尤其是Android 8.1及以下的版本很多编译脚本仍然依赖Python 2和旧版GCC在20.04上会碰到各种兼容性报错。而Android 10以后源码包对OpenJDK 11和Python 3的支持已经相当成熟再用16.04反而会因为系统自带的OpenJDK版本过低、glibc版本偏旧而出现莫名奇妙的编译失败。我的建议是如果你准备编Android 9.0及以下的旧分支老老实实用Ubuntu 16.04如果目标是Android 10及以上的新分支直接上Ubuntu 20.04省去后面大量兼容性修补工作。1.3 物理机、虚拟机还是Docker三种方案的取舍源码编译环境可以跑在三种不同的载体上物理机、虚拟机和Docker容器。这三种方案我都有过实际项目经验结论可以很快说清楚。物理机是首选也是我长期在用的方案。AOSP编译对磁盘I/O和内存带宽的要求非常苛刻物理机能直接吃满硬件性能。如果你手头有一台闲置的台式机或高性能笔记本把Ubuntu装在独立硬盘上这是最省心的路径。虚拟机适合只想尝鲜或者内存比较宽裕的情况。VMware和VirtualBox都能让Ubuntu跑起来但需要注意虚拟机的磁盘文件类型和格式会影响性能。虚拟磁盘类型建议选“预分配”而不是“动态增长”预分配的空间一次性写满物理磁盘后续编译时省去动态扩容的开销。虚拟机的磁盘I/O天然有损耗同样的代码在物理机编译一个半小时虚拟机可能要两个半小时。Docker是另一个可行选项它比虚拟机轻量创建销毁都很快适合做环境隔离和团队复用。我见过用docker run挂载宿主机目录来编译AOSP的做法但Docker在挂载目录上存在一层文件系统开销当源码量大到几百个G的I/O时这个开销会被放大得很明显。如果你是第一次接触AOSP我建议还是别选Docker老老实实用物理机或虚拟机把流程跑通之后再考虑容器化迁移。2. 系统基础环境装完Ubuntu后先补齐这些系统装好只是第一步接下来才是真正的“装修阶段”。AOSP编译需要大量的系统级依赖工具它们不会随着Ubuntu默认安装一起就位。缺一个库、少一个工具链、Java版本不对都会在编译中途以报错的形式“还债”。提前把环境捡干净后面才能睡个安稳觉。2.1 磁盘规划与分区建议磁盘是AOSP编译中最容易被低估的资源。一个完整的AOSP源码树包括.repo目录下的所有仓库数据同步完成后的体积通常在30GB到60GB之间。加上编译产物整棵源码树的体积会膨胀到100GB以上。如果还同时保留多个系统版本的分支200GB到300GB的磁盘占用是非常正常的。我在帮同事搭环境时遇到最典型的问题就是Ubuntu装完后根分区只给了50GB源码同步到一半磁盘就满了。解决思路是尽量把大目录独立分区。如果你是想在现有系统上折腾建议至少给根分区或home分区预留200GB。有条件的话我习惯单独分一个/work或/build分区专门用来放AOSP源码和编译产物。这样即使系统崩了要重装源码目录还在不用重新下载几百GB的数据。分区文件系统上ext4是最稳妥的选择。如果有条件上NVMe固态编译速度会有肉眼可见的提升因为AOSP构建过程中的大量小文件读写对随机I/O性能非常敏感。2.2 基础软件包从SSH到中文输入法系统安装完成后先更新一遍软件源并升级系统sudo apt update sudo apt upgrade -y这一步很重要尤其是内核和基础库的升级可能修复影响编译器运行的关键问题。接下来安装基础工具包我是按下面的清单装的sudo apt install -y git-core gnupg flex bison build-essential zip curl zlib1g-dev \ libc6-dev-i386 x11proto-core-dev libx11-dev lib32z1-dev libgl1-mesa-dev \ libxml2-utils xsltproc unzip fontconfig这套依赖在16.04和20.04上通用。其中lib32z1-dev和libc6-dev-i386是给32位兼容库用的AOSP里的部分预编译工具还是32位ELF没有它们会出现找不到共享库的错误。如果你不是直接在主机屏幕上操作而是像我一样习惯用SSH连到编译机上干活那要确认openssh-server已经安装并启动了sudo apt install -y openssh-server sudo systemctl enable ssh --now顺手把中文输入法也处理掉。我一般直接装ibus或fcitx配搜狗输入法在虚拟机里用中文环境调试脚本时方便很多。这个不是编译的硬性要求但相信我在终端里看到一堆中文乱码注释时你会想尽快装好的。2.3 编译依赖库安装16.04和20.04各一份这是整个环境搭建中最容易踩坑的地方。AOSP官方文档会给出依赖包列表但不同Ubuntu版本的包名有细微差异照着旧教程在20.04上执行经常出现“Unable to locate package”或者装完以后编译仍然报缺库。Ubuntu 16.04的依赖安装命令如下sudo apt install -y openjdk-8-jdk git-core gnupg flex bison gperf build-essential \ zip curl zlib1g-dev gcc-multilib g-multilib libc6-dev-i386 lib32ncurses5-dev \ x11proto-core-dev libx11-dev lib32z-dev libgl1-mesa-dev libxml2-utils xsltproc \ unzip fontconfigUbuntu 20.04的依赖包清单有两点不同。第一官方不再需要lib32ncurses5-dev转而用libncurses5-dev第二不需要再单独安装gcc-multilib和g-multilib因为新版工具链已经很好支持交叉编译了sudo apt install -y git-core gnupg flex bison build-essential zip curl zlib1g-dev \ gcc-multilib g-multilib libc6-dev-i386 libncurses5-dev x11proto-core-dev \ libx11-dev lib32z1-dev libgl1-mesa-dev libxml2-utils xsltproc unzip fontconfig \ python3 python3-pip写到这里我一定要提醒一句20.04上如果缺了libncurses5-dev编译时会出现fatal error: ncurses.h: No such file or directory这个问题在不少初学者身上反复出现建议对照自己的Ubuntu版本认真核对依赖清单别混着用。2.4 OpenJDK与Python跨版本的隐形坑Java和Python的版本问题属于“看起来不影响、实际上毁所有”的那一类。Ubuntu 16.04上编译Android 6到8.1必须用OpenJDK 8。装JDK用apt就行sudo apt install -y openjdk-8-jdk java -version如果系统里同时装了其他版本JDK用update-alternatives --config java切换默认版本。Android 9.0开始才可以用OpenJDK 11但16.04上默认源里不一定有建议通过sudo apt install openjdk-11-jdk或者用add-apt-repository ppa:openjdk-r/ppa添加源。Ubuntu 20.04上编译Android 10及以上版本OpenJDK 11是标准配置。注意这里有个容易忽略的点即使只为编译也要确保JAVA_HOME环境变量指向了正确的JDK路径。我习惯在~/.bashrc里固定写死export JAVA_HOME/usr/lib/jvm/java-11-openjdk-amd64 export PATH$JAVA_HOME/bin:$PATHPython的问题更隐蔽。Ubuntu 16.04自带Python 2.7而Ubuntu 20.04默认只有Python 3.x。Android 11之前的AOSP版本里有不少脚本硬编码了#!/usr/bin/env python在20.04上执行时会直接报/usr/bin/env: python: No such file or directory。解决方法是给Python 2.7装个软链前提是你把Python 2.7装好sudo apt install -y python2 sudo ln -s /usr/bin/python2 /usr/bin/python python --version这一步做完很多莫名其妙的脚本报错都会消失。3. 源码同步与首次编译实战环境齐了接下来进入重头戏同步AOSP源码并完成首次编译。这一步是最漫长也最容易出问题的环节网络状况、磁盘空间、资源分配都会影响结果。好在这一步后面就是黎明只要代码同步下来编译工作就成功了一半。3.1 安装repo并初始化AOSP源码不是单仓库而是由几百个Git仓库组成的巨型工程。Google提供了一个叫repo的工具来管理这些仓库它本质上是基于Git封装的一层Python脚本。安装repo有两种方式。一种是直接下载repo脚本另一种是通过apt安装。我推荐用第一种版本更新及时mkdir -p ~/bin curl https://storage.googleapis.com/git-repo-downloads/repo ~/bin/repo chmod ax ~/bin/repo export PATH~/bin:$PATH把export PATH~/bin:$PATH写进~/.bashrc避免每次新开终端都要重新设置。然后创建源码目录并初始化仓库mkdir -p ~/aosp cd ~/aosp repo init -u https://mirrors.tuna.tsinghua.edu.cn/git/AOSP/platform/manifest -b android-11.0.0_r1这里的-b参数指定分支实际使用时替换成你要编译的版本。我上面用的是清华AOSP镜像源这个源在国内开发圈子里用得非常多同步速度比官方源快很多。如果你更习惯中科大镜像也可以换成https://mirrors.ustc.edu.cn/aosp/platform/manifest。3.2 源码同步的细节与加速技巧初始化完成后直接同步所有仓库repo sync -c -j8-c表示只同步当前分支不拉取所有远程分支能省不少流量和时间-j8表示并发下载数量。并发数不建议盲目拉高下载速度取决于带宽和服务端限制我实测-j8到-j16之间比较合理。首次同步动辄几十GB的数据耗时少则一小时多则数小时。如果同步中途断了不要慌继续执行一次repo sync -c -j8就能断点续传repo会自动跳过已经完整的仓库。同步阶段我遇到过最典型的问题是“无法解析主机名”或者“连接超时”这跟本机DNS配置和防火墙策略有关。用清华镜像时建议把DNS设为公共DNS或者在~/.repo/manifests目录下检查manifest文件里的remote地址是否被正确替换成了镜像地址。还有一种可能的“加速技巧”是关闭Git的压缩git config --global core.compression 0在CPU性能较弱但网速较快的机器上这个设置能让下载更稳定不过对大多数用户来说默认配置就够了。3.3 首次编译从lunch到make -jN源码同步完成先初始化编译环境加载后续要用的函数source build/envsetup.sh然后通过lunch选择构建目标。直接输入lunch会弹出产品列表也可以指定参数比如编一个模拟器镜像lunch aosp_x86_64-engeng表示工程版适合开发调试附带完整的调试工具userdebug更适合做系统测试性能和调试能力居中user是接近正式发布的版本但很多调试工具会被裁剪。首次编译我建议选eng。接下来开始正式构建make -j8-j参数后面跟并行任务数理论经验值是CPU核心数的2倍。我拿一个8核16线程的机器举例-j16能充分发挥能力但也要看内存。并行任务太多会把内存吃满你可以根据自己的内存大小把任务数调低比如16GB内存配8核CPU用-j8其实更安全。编译过程中终端会刷出大量日志如果用的是虚拟机建议开启之前分配好的swap空间。整棵树的首次编译耗时很长我建议使用make -j8 21 | tee build.log这种方式记录日志编译失败时可以快速定位问题。3.4 ccache让增量编译更快AOSP的增量编译虽然不会重编所有代码但Java层的编译、资源打包等步骤依然很慢尤其当你频繁在Framework层改代码时每次构建都能明显感觉到卡顿。ccache是一个编译器缓存工具可以缓存C/C的编译结果命中后直接复用能显著缩短增量编译时间。安装和配置很简单sudo apt install -y ccache export USE_CCACHE1 export CCACHE_EXEC/usr/bin/ccache推荐给ccache设置一个比较大的缓存目录并固定大小ccache -M 50G ccache -o compressiontrue50G可以根据磁盘情况调整我建议最少给20G。设置完成后在~/.bashrc里加上export USE_CCACHE1这样每次编译都会自动启用。首次编译时ccache还没命中提速效果不明显但当你第二次、第三次编同一套代码时实实在在能省下不少时间。4. 编译期经典报错与排查经验我能想象到你现在最关心的是什么如果编译挂了我该怎么办。下面把我在16.04和20.04上遇到的高频问题整理出来每一条都是真实案例可以直接拿来对照排错。4.1 Ubuntu 16.04的Jack服务与内存配置Jack是Android 6到8时代的Java编译器基础设施在Ubuntu 16.04上编译老版本AOSP时Jack服务经常是头号杀手。它的经典报错是Communication error with Jack server (52)或者Out of memory error. Try: Jack server out of memory. Try increasing JACK_SERVER_VM_ARGUMENTSJack默认给服务端分配的内存是4GB当你同时多次编译或者机器内存较小时很容易爆掉。解决方法是调大Jack的服务端内存export JACK_SERVER_VM_ARGUMENTS-Xmx4096m具体数值根据你机器的内存来定16GB内存的机器我习惯设成-Xmx8192m。设置完先重启Jack服务jack-admin kill-server jack-admin start-server还有一个小坑是Jack和Java 9以上不兼容如果你用16.04但手滑装了高版本JDK编译时会直接报Unsupported class file major version。所以Admin权限下确认java -version输出确实是1.8再开编。Android 9之后Jack被移除了如果你编的是新版本这个章节可以直接跳过。4.2 Ubuntu 20.04的Python 2缺失问题20.04编译Android 11时最经典也最让人崩溃的报错就是上面提到的/usr/bin/env: python: No such file or directory这个报错来自代码里的构建脚本它们还在用#!/usr/bin/env python方式调用解释器。安装Python 2并建立软链之后问题就解决了sudo apt install -y python2 sudo ln -s /usr/bin/python2 /usr/bin/python如果你还遇到/usr/bin/env: python2: No such file or directory说明Python 2解释器的名字没有被系统识别确认python2命令能跑起来python2 --version另外一个20.04特有问题是OpenSSL版本过高导致编译某些老模块时提示找不到libssl.so.1.0.0之类的动态库。原因是20.04自带的OpenSSL已经升到1.1而AOSP的某些预编译工具链还绑定老版本so。这种情况不建议强行降级系统OpenSSL更合理的做法是把老版本so软链到/usr/lib/x86_64-linux-gnu目录下或者用容器隔离老工具链。4.3 内存不足与Swap扩展方案不管哪个Ubuntu版本内存不足都是编译中常见的硬伤。报错信息通常是clang: error: unable to execute command: Killed系统日志里同时会有大量OOM Killer的痕迹。这表示物理内存耗尽系统开始杀进程了编译器被误杀导致中止。最快的临时办法是加一个Swap文件sudo fallocate -l 16G /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile sudo swapon /swapfile要让这个Swap永久生效还得在/etc/fstab里追加一行/swapfile none swap sw 0 0这个方案我在8GB内存的虚拟机上实测过编译Android 10可以从失败变成成功但速度会很慢。更好的办法还是物理内存加到16GB以上Swap只是兜底方案不是速度方案。4.4 常见报错速查表我把过去几年遇到的编译报错整理成了一张速查表方便你在现场快速对照报错信息可能原因解决方案command not found: m4缺少编译宏处理工具sudo apt install m4flex: command not found缺少词法分析器sudo apt install flexmake: *** No rule to make target依赖包不完整或中间产物损坏检查依赖包必要时make clean后重新来Out of memory / Killed物理内存不足增加swap或加大物理内存调低-j参数/usr/bin/env: python: No such filePython 2缺失sudo apt install python2并软链到/usr/bin/pythonCould not find a supported devicelunch参数写错先敲lunch列出可用目标再精确选择external error, see build.log编译日志有更多细节用tee保存日志搜索error:关键词定位No rule to make target libart.sojemalloc或art组件问题确认磁盘空间删除out目录后重试这张表不能覆盖所有问题但能覆盖我遇到过的绝大多数初级报错。真正复杂的失败往往是一连串连锁反应建议先处理第一个报错再看下一个。5. 编译产物验证与后续日常第一次编译可能耗时很长但只要能看到make completed successfully前面所有折腾都是值得的。接下来是验证产物和日常维护的关键。5.1 产物路径与模拟器启动编译成功后的产物默认放在out/target/product/产品名/目录下。以aosp_x86_64-eng为例镜像文件在out/target/product/generic_x86_64/里能看到system.img、vendor.img、ramdisk.img等文件。最简单粗暴的验证方式是直接用模拟器启动source build/envsetup.sh lunch aosp_x86_64-eng emulator模拟器能正常进入系统界面说明编译产物基本可用。这里有一个小技巧首次启动模拟器会比较慢因为冷启动要初始化用户数据分区耐心等一会儿别急着关窗口。如果是给真机编译产物还需要用fastboot工具刷入设备。常见做法是把system.img、boot.img和vendor.img分别分区刷写或者直接打包成厂商要求的OTA包。这两种操作我都试过真机刷机比模拟器麻烦很多需要提前确认bootloader是否解锁以及分区表是否匹配。5.2 增量编译与Clean策略编译了一次之后后续修改代码再编译就是增量编译直接执行make -j8增量编译只重建改动过的模块速度和全量编译完全不是一个量级。我一般改完Framework层代码几分钟内能出包。但有一种情况你必须执行clean切换了产品分支或产品名修改了系统级全局配置出现不明原因的链接错误同步了很大的更新执行cleanmake clean会自动删除out/目录彻底干净。有些老手习惯用rm -rf out来达到同样效果也可以但make clean更安全它只删除编译产物不会误伤源码。还要提醒一件事不要为了省时间跳过make clean而直接全量编译很多莫名其妙的“No rule to make target”都是因为中间产物状态不一致造成的。我在实际项目中吃过几次亏凡是遇到这种诡异问题先clean再重新编译往往能解决。这个环境搭好之后后续维护其实很轻松。保持repo sync定期同步上游代码及时更新依赖包每次编译前检查磁盘剩余空间基本就不会再出大问题。我个人的习惯是把常用命令写成一个脚本放在~/aosp/build.sh每次新开终端直接执行脚本就进入编译状态省去敲一堆export的麻烦。编译这件事第一次最难之后就都是熟练工了。