
如果你点进来说明你和我一样在 Ubuntu 24.04 上折腾 Android 16 的 Cuttlefish 编译。这个需求听起来就是一条命令的事实际跑起来你才会发现AOSP 构建系统中的每一个环节都可能成为拦路虎。Cuttlefish 作为 Android 官方力推的虚拟设备方案在本地编译、系统镜像验证、自动化测试这些场景里几乎是绕不开的。我这次在 Ubuntu 24.04 上从零开始编译 Android 16 的 Cuttlefish前后花了两天时间把各种报错都过了一遍今天把整个过程、决策逻辑和踩坑记录完整梳理出来希望能给后面的人省点时间。1. 搞懂 Cuttlefish 在 Android 16 开发里的定位再动手不迟很多同学上来就想直接敲make但 Cuttlefish 和普通的 AVD 模拟器不一样它更像一套可以在 Linux 主机上运行的真机硬件抽象层。谷歌设计它的初衷是替代老旧的 goldfish 模拟器给 CTS/GTS 测试、兼容性验证、系统级调试提供一个更接近真实设备的虚拟化环境。Android 16 对虚拟化框架的依赖越来越多用 Cuttlefish 验证要比用 ARM 翻译模拟器靠谱得多这也是我这次扛着编译风险也坚持用它的原因。1.1 Cuttlefish 到底是什么鱼Cuttlefish 是一个基于 crosvm 的虚拟化方案核心思路是把 Android 系统镜像跑在 Linux 的 KVM 之上。它和你在 Android Studio 里常用的 AVD 模拟器有几个关键差异AVD 模拟器面向应用开发者提供方便的可视化操作界面但底层的硬件模拟是做过简化的。Cuttlefish 面向系统工程师和测试工程师完全命令行驱动支持快照、多显示器、虚拟摄像头、虚拟传感器甚至可以通过cvd命令控制设备重启、网络配置等。Cuttlefish 的 HAL 层实现更贴近真机很多 OEM 在移植 Android 16 时会用它做早期验证。用一句话概括AVD 是给你点着玩的Cuttlefish 是给你跑自动化测试和系统验证的。1.2 Android 16 为什么要用 Cuttlefish 验证Android 16 在动态系统更新、虚拟化框架、多用户空间等方面改动不少。如果你只想跑个第三方 App普通模拟器够用但如果你想编译完system.img后又刷机、又验证vendor镜像的行为Cuttlefish 是更快的方式。它可以直接加载完整的 boot、system、vendor 镜像并且可以模拟 OTA 更新流程。另外Cuttlefish 编译产物里包含了 host 端工具和虚拟设备配置这让它可以很方便地集成到 CI 流水线里。我在本地编译完成后只需要一条cvd start就能拉起一台虚拟设备然后用 adb 连接进去做调试这比反复擦写真机不知道省了多少时间。1.3 Ubuntu 24.04 作为宿主机的优势与风险Google 官方构建文档历来对 Ubuntu LTS 版本支持最好Ubuntu 24.04 的内核升级到了 6.8KVM 相关模块的稳定性比 22.04 好不少内存管理和 cgroup v2 的支持也更成熟。但这些新也带来了新问题后面我踩的 libncurses5 缺失、crosvm 兼容性、cgroup v2 权限都和这个系统版本强相关。如果你手头有 Ubuntu 22.04可能会觉得老版本更稳但如果你为了后续跟 Android 16 的风险还是建议直接用 24.04。关键是看完这篇文章把后面这些坑提前避开。2. 编译前的宿主机环境准备磁盘、内存和工具链编译 AOSP 不是简单敲个make就行宿主机环境准备不充分编译中途出现各种诡异错误会让你怀疑人生。我把最容易翻车的几个点单独拉出来说。2.1 磁盘分区与交换空间的底线Cuttlefish 编译要跑 host 端 crosvm、虚拟设备运行时还有 AOSP 的镜像产物这些体积都很大。我这次在一个 500GB 的数据盘上做的编译最终out目录占了 120GB如果加上 ccache 和中间缓存很容易突破 200GB。Ubuntu 24.04 默认安装时根分区通常只给 50GB如果你只编译一次试试水可能勉强够但如果你要反复修改源码、多次编译磁盘空间一定是第一个瓶颈。检查磁盘空间可以用df -h如果空间紧张建议单独挂一个至少 300GB 的分区或者直接把 AOSP 源码目录和数据放在大分区下。还有一个容易忽略的点是out目录默认在源码树内如果你不想源码和产物混在一起也可以用软链接把out指向其他磁盘。内存方面16GB 是底线32GB 会更从容。如果只有 16GB编译时很可能会被 OOM 杀掉这时候加一个 8GB 的交换文件能救命sudo fallocate -l 8G /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile sudo swapon /swapfile注意swap 只是让你不崩编译速度还是会受影响所以能加内存就加内存。2.2 JDK、Python、repo 工具链的版本陷阱Android 16 的构建系统需要 OpenJDK 17。Ubuntu 24.04 软件源里默认的 OpenJDK 是 21直接编译会碰到一些 Java 版本相关的错误。我建议先显式安装 OpenJDK 17并把它设为默认sudo apt install openjdk-17-jdk sudo update-alternatives --config javaPython 版本倒还好Ubuntu 24.04 默认为 Python 3.12满足 AOSP 要求的 3.8 以上。但你要注意系统里可能同时存在 python3 与 pythonAOSP 的脚本大多调用 python3所以不要随便把 python 指向 python3。repo 工具比较关键。Ubuntu 软件源里的 repo 通常比较老建议用官方脚本安装最新版curl -sS https://storage.googleapis.com/git-repo-downloads/repo ~/bin/repo chmod ax ~/bin/repo把~/bin加到 PATH 后用repo --version验证。如果你在公司网络环境下可能还需要配置代理这个自己处理但别用不安全的工具。2.3 依赖包安装清单AOSP 编译依赖的软件包很多Ubuntu 24.04 上官方文档不一定列全我把实际用到的整理成一份命令sudo apt update sudo apt install -y git-core gnupg flex bison build-essential zip curl zlib1g-dev \ libc6-dev-i386 libx11-dev lib32z1-dev libgl1-mesa-dev \ libxml2-utils xsltproc unzip fontconfig python3 python3-pip \ libncurses-dev libssl-dev libffi-dev这里有个坑libncurses5这个老包在 Ubuntu 24.04 的软件源里已经没了但某些 Android 16 的 host 工具确实还需要它。我在第 6 节会专门讲怎么绕过去这里先不要急着下载第三方 deb 包。依赖包安装完成后可以顺手验证一下 build-essential 是否正常比如gcc --version、make --version。有时候你之前装过 Anaconda 或者自己的编译链导致系统默认的 gcc 版本不对后面编译很容易出现奇怪的段错误。3. 用 repo 拉取 Android 16 源码分支选择与断点续传源码获取是另一个大坑。Android 16 对应的 AOSP 分支如果没选对后面编译目标都找不到。我这里的做法是直接用官方 manifest 分支。3.1 初始化 manifest 分支创建源码目录并初始化 repomkdir aosp-cf cd aosp-cf repo init -u https://android.googlesource.com/platform/manifest -b android-16.0.0_r1 repo sync -c -j8android-16.0.0_r1是 Android 16 的发布标签分支随着时间推移可能会变成更靠后的r2、r3之类。如果你想要最新开发版也可以直接用main分支但 main 分支每天都有变化构建稳定性会差很多。repo 初始化的时候会提示你设置 git 用户名和邮箱不然同步代码时会报错git config --global user.name Your Name git config --global user.email youexample.com3.2 Cuttlefish 在源码树中的位置同步完成后你需要确认 Cuttlefish 相关模块是否完整。Cuttlefish 代码主要分布在几个目录device/google/cuttlefish核心设备配置与启动脚本external/crosvm虚拟化底层hardware/google/cuttlefishHAL 实现和虚拟外设检查这些目录是否存在ls -d device/google/cuttlefish external/crosvm hardware/google/cuttlefish如果某个目录是空的很可能是 manifest 分支差异导致的可以先repo sync再检查一次。如果依然缺失可能需要重新检查 manifest 是否加载完整。3.3 网络中断后的断点续拉AOSP 全量代码差不多 50GB 以上repo sync 中断是家常便饭。好在 repo 支持断点续传你只需要再次执行repo sync就能继续。但直接重跑经常会在同一个项目上反复失败我一般用这样的循环脚本for i in {1..10}; do repo sync -c -j4 break || sleep 5; done把并发数从 8 降到 4网络稳定性会好很多。如果你的网络条件较差还可以用repo sync -c --no-tags跳过 tag 下载能省不少时间。第一次同步尽量找网络空闲的时段不然拉一半又断真的非常磨人。4. Cuttlefish 编译目标解析cvd host 包与镜像产物源码拉完后真正烧时间的编译环节开始了。Cuttlefish 编译有两个组成部分Android 系统镜像和 cvd 主机工具。搞懂它们之间的关系你才知道编译完了该到哪个目录找产物。4.1 指定编译目标进入源码根目录后先设置环境source build/envsetup.sh lunch aosp_cf_x86_64_phone-trunk_staging-userdebugaosp_cf_x86_64_phone是 Cuttlefish 在 x86_64 架构下的手机形态目标trunk_staging是面向开发阶段的构建变体。Android 16 可能也会提供类似aosp_cf_x86_64_phone-userdebug的目标你可以用lunch不加参数弹出菜单里查找或者用lunch aosp_cf_x86_64_phone-userdebug如果你不确定有哪些可用 target可以执行lunch然后按 Tab 键自动补全会列出所有 Cuttlefish 相关的项。选userdebug变体是为了保留 adb root 和调试能力单纯做测试可以选user。之后开始编译m -j16这里-j16的并行数匹配我的 16 线程 CPU。如果你机器线程数不是这个值用nproc查看一下然后按第 4.3 节的方法设置合理并行度。4.2 产物结构编译完成后你会在out目录里看到两类关键产物。第一类是系统镜像通常位于out/target/product/panther/里面有system.img、vendor.img、boot.img等。注意这里的panther不是高通芯片代号而是 Cuttlefish x86_64 设备的 product 命名。第二类是 cvd 主机工具和运行时资源位于out/host/linux-x86/bin/cvd out/host/linux-x86/cuttlefish/cvd这个二进制就是你用来启动虚拟设备的命令行工具。它依赖同目录下的crosvm和各种配置文件。如果你用 older 版本的 cvd 搭配新编译的镜像或者反过来启动时会非常痛苦。我的经验是每次编译完优先用同一轮产物里的cvd避免混合版本。4.3 并行度与内存控制编译并行数不是越大越好。AOSP 编译过程中会有许多并行的 ninja 任务每个任务都会占用一定内存。16 线程 CPU 配 32GB 内存时-j16基本没问题但内存只有 16GB 时-j16很容易 OOM。我建议按这个经验设置32GB 内存 16 线程用-j1616GB 内存 16 线程用-j88GB 内存不建议尝试完整编译至少升级到 16GB另外m命令还支持 max load限制系统负载m -j16 --max-load16这样即使编译过程中你还在干别的系统也不会被完全卡死。如果你用 ccache增量编译时并行度可以稍微高一点因为 ccache 会命中很多重复任务实际 CPU 压力低一些。5. 启动 Cuttlefish 前的设备权限与内核虚拟化配置编译成功只是第一步真正启动 Cuttlefish 虚拟设备时你会碰到一堆权限和内核虚拟化相关的问题。很多时候问题不是出在 Android 镜像上而是宿主机的环境没配好。5.1 KVM 透传与用户组权限Cuttlefish 依赖 KVM 做硬件加速。Ubuntu 24.04 默认会加载 KVM 模块但需要确认你的用户是否有权限访问/dev/kvm。先检查ls -l /dev/kvm如果设备节点存在但当前用户不在kvm组cvd start会报Permission denied。解决办法sudo usermod -aG kvm $USER newgrp kvm如果/dev/kvm根本不存在那就不是权限问题而是 CPU 虚拟化没开启或者内核模块有问题。grep -E -c (vmx|svm) /proc/cpuinfo sudo modprobe kvm_intel # 或 kvm_amd如果模块加载失败可能要检查 BIOS 里的 Intel VT-x / AMD-V 是否开启。这一步不过后面所有努力都白费。5.2 udev 规则的作用Cuttlefish 启动时会创建一系列虚拟设备节点比如/dev/vhost-vsock、/dev/vhost-net、/dev/net/tun等。Ubuntu 24.04 的默认权限通常只给 root 或特定组普通用户直接运行cvd start会遇到大量Failed to open的报错。官方 deb 包会附带 udev 规则但源码编译场景需要手动处理。你可以在/etc/udev/rules.d/51-cuttlefish.rules里添加类似这样的规则KERNELvhost-vsock, MODE0660, GROUPkvm KERNELvhost-net, MODE0660, GROUPkvm KERNELtun, MODE0660, GROUPkvm写完后执行sudo udevadm control --reload-rules sudo udevadm trigger这样当前用户只要能访问 kvm 组就能正常打开这些设备节点。很多人编译成功但启动失败问题就出在这。5.3 cvd start 首次启动常见失败环境配置好之后启动命令很简单cvd start首次启动常见的报错有这么几类内存不足Cuttlefish 默认会为 Android 分配较多内存如果宿主机内存小需要减小配置。/dev/kvm权限上一节讲过的组权限问题。无法访问vhost-vsock检查 udev 规则和crosvm版本。如果启动失败别急着重复执行先看日志cvd start --report_anonymous_usage_statsno ls $HOME/.cuttlefish_runtime/ cat $HOME/.cuttlefish_runtime/cuttlefish.log日志里通常直接写明了是哪个设备打开失败定位后对症下药。6. 实测踩坑记录从编译报错到启动崩溃的完整排查链路这一部分是我最想分享的也是我花时间最多的地方。下面三个问题按我实际排查的顺序记录希望能帮你省掉几轮瞎折腾。6.1 libncurses5 缺失引发的 host 工具编译失败在 Ubuntu 24.04 上编译到某个 host 工具时我遇到了一个非常经典的问题fatal error: ncurses.h: No such file or directoryUbuntu 24.04 的软件源只提供libncurses-dev对应 ncurses6而 Android 16 的某些 host 工具还停留在 ncurses5 时代。直接安装libncurses5-dev会提示找不到包。我最后用一个稳妥的办法解决了下载 Ubuntu 22.04 时代的libncurses5-devdeb 包手动安装。注意别乱装 32 位包装错了反而会把系统库弄乱。下载后执行sudo dpkg -i libncurses5-dev_*.deb如果你的软件源里也找不到这个包可以从 Ubuntu 的旧版本的 archive 库拉取但一定要确认架构是 amd64并且不要覆盖系统已有的libncurses6。另一个折中方案是做一个软链接把 ncurses5 的头文件链接到新版本上但这样有时候会引入更隐蔽的 ABI 冲突我的建议是能下载老包就用老包。6.2 crosvm 崩溃与 cgroup v2 权限第一次cvd start时虚拟设备没有起来查看cuttlefish.log发现 crosvm 直接崩溃了。日志里有这么一行Failed to set cgroup: Permission denied原因是 Ubuntu 24.04 默认启用了 cgroup v2而当时我环境里的 crosvm 版本对 v2 的支持并不完整。这个问题的根源在于Android 16 源码里的 crosvm 可能已经适配了 v2但 host 端如果装了旧的 crosvm 包就会优先使用旧版本导致冲突。排查方法which crosvm crosvm --version如果发现使用的是/usr/bin/crosvm而源码编译出来的在out/host/linux-x86/bin/crosvm你需要把 PATH 环境变量调成优先使用 out 目录下的版本然后重新启动。如果确认用的就是新版本还报 cgroup 错误可以尝试在内核引导参数里加入systemd.unified_cgroup_hierarchy0切换到 cgroup v1 再启动。但这不是长久之计最好还是升级 crosvm 到支持 v2 的版本。Android 16 的源码树内版本基本没问题问题往往出在系统自带的旧版。6.3 磁盘被 out 目录撑爆编译一次 Cuttlefish 后磁盘占用高得惊人。我最初把源码放在 Ubuntu 24.04 的根分区编译到一半就提示No space left on device。查看后发现out目录就占了 120GBccache 也有 80GB根分区彻底满了。这其实是在准备阶段偷懒了没有预先规划大分区。我的解决方案是把 out 目录软链接到大分区mv out /data/android/out ln -s /data/android/out out这样编译产物直接写入数据盘。ccache 目录同理export CCACHE_DIR/data/android/ccache ccache -M 100G如果你已经编译到一半才发现磁盘不够可以先把out目录打包放到大分区再软链接回来但需要确保链接前目录结构正确不然增量编译会重新生成很多东西。7. 上手后的优化建议缓存、参数与日常开发流环境跑通之后你会发现 Cuttlefish 其实很顺手但还有几个优化点能大幅提升开发效率。7.1 ccache 配置提升增量编译AOSP 每次编译都会产生大量重复的 C/C 编译工作ccache 能帮你节省非常多时间。第一次编译可能慢但后续增量编译会快 3 到 5 倍。配置方法sudo apt install ccache export USE_CCACHE1 export CCACHE_EXEC$(which ccache) ccache -M 100G这几行建议写进~/.bashrc或~/.profile不然每次新开终端都要重设。注意ccache -M 100G是设置缓存上限磁盘空间小的话可以设小一点但至少 50G 才够用。增量编译时你会看到类似ccache hit rate 76%的统计。当这个命中率超过 80% 时你基本就告别长时间等待了。7.2 自定义 cvd 参数调整设备Cuttlefish 默认会以 1080p 分辨率、4GB 内存启动。如果你想调整虚拟设备的行为可以用cvd start带参数。查看所有参数cvd start --help常用的比如设定内存大小或数据分区大小可以通过运行时配置文件来改。简单粗暴的方式是直接修改~/.cuttlefish_runtime/config.json但这个文件每次启动会重新生成最好还是用 cvd 提供的--extra-args或者设置环境变量。我在实际中用的比较多的是关闭图形界面以节省资源cvd start --no-graphics配合 adb 连接后在 CI 环境里跑测试完全没问题。7.3 与 Android Studio 协同使用的场景Cuttlefish 启动后不需要 Android Studio 也能用 adb 连接默认端口通常是6520你需要先通过adb connect 127.0.0.1:6520连接。如果你配置了多个 Cuttlefish 实例可以通过cvd fleet管理。为什么说它能和 Android Studio 协同因为日常写 App 或者做 FrameWork 开发时你往往需要反复刷镜像、重启设备。Cuttlefish 支持快照snapshot可以让你快速回到某个系统状态比 Android Studio 模拟器自带的快照更稳定。它也能暴露虚拟传感器事件做自动化测试时非常方便。我个人在实际操作中的体会是Cuttlefish 更适合作为一个人的私有测试云。白天在公司改代码晚上回家在同一套源码上继续编译然后用cvd start拉起的虚拟设备继续调试整个过程完全不依赖云厂商的 ARM 服务器x86 电脑上也能跑出完整的 Android 16 系统。把这一套环境理顺之后你会发现本地编译、本地运行、本地验证这件事在 Android 系统开发里其实可以做得非常顺滑。