5 min read

用 Apple Container + Rosetta 在 Mac 上跑 AOSP Module Build

Table of Contents

macOS 26 推出了 Apple Container。我想知道它能不能讓 MacBook 跑 AOSP module build,減少對 CI 的依賴。結論:可以,但有幾個地方要處理。


背景

我的 AOSP 開發流程一直有個痛點:MacBook 只能改 code,build 必須推到遠端 CI。每次改動的迭代週期是「push → 等 CI build → 看結果」,一個 typo 就要等 30 分鐘才知道。

macOS 原生不能 build AOSP——build system 假設 Linux,prebuilt toolchain 是 x86_64 binary,加上 case-sensitive filesystem 的要求。所以一直都是推 code 然後等。

Apple Container 出來之後,我想試看看能不能用它在本地跑 module build,至少在推 CI 之前先驗證一次。


環境

  • MacBook Pro M3 Max,128 GB RAM
  • macOS 26,Apple Container v1.0.0
  • AOSP Android 14

Apple Container 是 Apple 基於 Virtualization.framework 做的 Linux 容器 runtime,幾個跟這次相關的特性:

  • ARM64 Linux 容器原生跑在 Apple Silicon 上
  • --rosetta flag 可以透明執行 x86_64 binary
  • virtiofs 掛載 host 目錄,I/O 接近原生
  • 支援 persistent ext4 volume
  • 容器啟動秒開

思路:用 virtiofs 掛載 AOSP source tree,ARM64 工具原生跑,x86_64 prebuilt(Go、clang、aapt2 等)走 Rosetta。

Dockerfile

Ubuntu 24.04 ARM64 加 AOSP build 依賴,再加 Rosetta 需要的 x86_64 runtime:

FROM ubuntu:24.04

# ARM64 build 依賴
RUN apt-get update && apt-get install -y \
    git-core gnupg flex bison build-essential zip curl \
    zlib1g-dev libxml2-utils xsltproc unzip fontconfig \
    libncurses-dev python3 openjdk-21-jdk \
    rsync bc cpio lz4

# Rosetta 用的 x86_64 lib(AOSP prebuilt 是 x86_64)
RUN dpkg --add-architecture amd64 \
    && echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu noble main" \
       > /etc/apt/sources.list.d/amd64.list \
    && apt-get update \
    && apt-get install -y libc6:amd64 libstdc++6:amd64 zlib1g:amd64

啟動:

container run --rm --rosetta --cap-add ALL \
    -v /path/to/aosp:/aosp \
    -v build-cache:/tmp/aosp-out \
    -e OUT_DIR=/tmp/aosp-out \
    aosp-builder:v1

到這裡環境就搭好了,但直接跑 lunch 會失敗。有三個地方要處理。


三個需要處理的相容性問題

1. macOS metadata 目錄透過 virtiofs 穿透

macOS 的 HFS+/APFS volume 上有幾個系統用的 metadata 目錄:.Spotlight-V100.DocumentRevisions-V100.TemporaryItems.fseventsd

透過 virtiofs 掛進 Linux 之後,這些目錄會出現在 readdir() 結果裡,但對它們 lstat() 會回 ENOENT——列表裡看得到,但 stat 不到。

Soong 的 module-finder 會掃描整棵 source tree 找 Android.bp,碰到這些目錄就直接 fatal error:

finder.go: error: lstat .Spotlight-V100: no such file or directory

修法是改 build/soong/ui/build/finder.goExcludeDirs,把這幾個目錄加進去:

// 修改前
ExcludeDirs: []string{".git", ".repo"},
// 修改後
ExcludeDirs: []string{".git", ".repo",
    ".Spotlight-V100", ".DocumentRevisions-V100",
    ".TemporaryItems", ".fseventsd"},

我試過其他幾種方法都不行:在那些路徑建 dummy 目錄(virtiofs mount 會覆蓋)、在 virtiofs 上面疊 overlayfs(stale NFS handle error)、建 symlink 指到 /dev/null(破壞其他路徑解析)。最後只有改 ExcludeDirs 是乾淨的。

2. Rosetta 需要 x86_64 runtime library

--rosetta 會翻譯 x86_64 instruction,但不帶 libc。第一次跑 AOSP prebuilt 工具:

$ prebuilts/go/linux-x86/bin/go version
rosetta error: failed to open elf at /lib64/ld-linux-x86-64.so.2

需要從 Ubuntu x86_64 repo 裝 libc6:amd64libstdc++6:amd64zlib1g:amd64

這裡有個坑:Ubuntu ARM64 的 ports.ubuntu.com 不帶 amd64 套件,要另外加 archive.ubuntu.com 的 source,並限制預設 source 為 Architectures: arm64 避免衝突。Dockerfile 裡已經處理了。

3. Artifact path 嚴格檢查

AOSP 的 artifact path checker 會驗證檔案不違反 partition 邊界。某些 vendor 配置下會標記 vendor/lib/libhidltransport.so 之類的檔案然後 hard-fail。

CI 上通常靠 board config 的 BUILD_BROKEN_ARTIFACT_PATH_REQUIREMENT := true 處理,但容器裡這個 flag 不一定正確傳播。

本地 build 的務實做法:在 build/make/core/artifact_path_requirements.mkmaybe-print-list-and-error 改成 maybe-print-list-and-warning。CI 不受影響。


virtiofs COW:patch 不留痕跡

三個 patch 都在容器 entrypoint 自動套用。重點是 virtiofs 的 copy-on-write 特性——所有修改只存在於容器裡,host 上的 source tree 完全不會被動到。容器一關,patch 就消失了。

這讓我可以放心在容器裡 sed build system 檔案,不用擔心弄髒 source tree。

#!/bin/bash
# entrypoint.sh — 每次容器啟動時自動套用

# 排除 macOS metadata 目錄
sed -i 's/ExcludeDirs:.*\[\]string{".git", ".repo"}/.../' \
    build/soong/ui/build/finder.go

# 停用重複的 product 定義
mv device/.../AndroidProducts.mk{,.container-disabled}

# 降級 artifact path 檢查
sed -i 's/maybe-print-list-and-error/maybe-print-list-and-warning/' \
    build/make/core/artifact_path_requirements.mk

source build/envsetup.sh
exec bash

Persistent Build Cache

AOSP build 產生的中間產物不少(Soong ninja 檔案、kati makefile、編譯中間檔)。沒有 cache 每次都是 cold build。

Apple Container 支援 persistent ext4 volume:

container volume create -s 50G build-cache
container run -v build-cache:/tmp/aosp-out ...

Volume 跨容器重啟保留。

實測數據

Cold CacheWarm Cache差異
lunch(Soong bootstrap)121 秒89 秒-26%
mmm(module build)27:3117:18-37%
Total29 分鐘19 分鐘-34%

Cache 用了約 8.6 GB(其中 Soong cache 佔 4.2 GB)。

Warm cache 還是會重新生成 1024 個 glob shard(Soong 每次都重新掃描 source tree),但跳過了 ninja 檔案生成。


Full Build 可行嗎?

技術上可行——所有編譯工具都走 Rosetta。但每一次 compiler invocation 都有翻譯開銷。Module build 約 2500 步花 19-29 分鐘,full build 50,000+ 步。

預估 4-8 小時 vs. CI 的 30-60 分鐘。不實際,full build 還是交給 CI。


適用場景

場景以前現在
驗證 resource 改動能編譯Push → CI 30 min → 看結果本地 mmm ~19 min
迭代 overlay config每次都 push本地 build,確認後一次 push
測試 build system 改動盲 push 到 CI先本地驗證
調查 build 失敗遠端讀 CI log本地重現

本地 build 不快——19 分鐘不算短。但省掉的是「push → 等 → 發現錯誤 → 改 → 再 push」的來回,這才是時間的主要浪費。


踩完坑之後的筆記

  1. virtiofs 會把 host filesystem 的怪癖穿透進來。 macOS metadata 目錄、extended attribute 之類的都會漏進 Linux,做 filesystem 掃描的 build system 會踩到。

  2. Rosetta 翻譯 instruction,不翻譯 library。 你得自己裝 x86_64 的 libc、libstdc++、zlib。

  3. virtiofs COW 讓容器裡的 patch 沒有代價。 這個特性很適合本地開發環境需要跟 CI 有差異的情況——改完容器一關就還原。

  4. Build cache 是「可用」和「煩人」的分界線。 37% 的差距,沒有 cache 的話這個工作流大概不會有人想用。

  5. Module build 是甜蜜點。 Full build 技術上可以但沒意義。每個工具做它擅長的事就好。


設定步驟

  1. macOS 26+ on Apple Silicon
  2. 安裝 Apple Container v1.0.0+
  3. container system start(一次性,下載 VM kernel)
  4. Build container image(Ubuntu 24.04 + ARM64 依賴 + amd64 Rosetta lib)
  5. 建立 persistent volume 給 build cache
  6. 寫 entrypoint 處理三個相容性問題
  7. -v /path/to/aosp:/aosp 掛載 AOSP tree
  8. lunch <target> && mmm <module>

具體的 patch 會因 AOSP 版本和 device 配置而異,但三大類(filesystem metadata、Rosetta runtime lib、build system 嚴格檢查)應該是通用的。

完整的 Containerfile、entrypoint 和腳本放在 wangchauyan/aosp-container。Clone 下來,在 config.env 設好 AOSP_ROOT./run.sh 就可以跑。


測試環境:MacBook Pro M3 Max(128 GB RAM)、macOS 26、Apple Container v1.0.0、AOSP Android 14。