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 上
--rosettaflag 可以透明執行 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.go 的 ExcludeDirs,把這幾個目錄加進去:
// 修改前
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:amd64、libstdc++6:amd64、zlib1g: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.mk 把 maybe-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 Cache | Warm Cache | 差異 | |
|---|---|---|---|
lunch(Soong bootstrap) | 121 秒 | 89 秒 | -26% |
mmm(module build) | 27:31 | 17:18 | -37% |
| Total | 29 分鐘 | 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」的來回,這才是時間的主要浪費。
踩完坑之後的筆記
-
virtiofs 會把 host filesystem 的怪癖穿透進來。 macOS metadata 目錄、extended attribute 之類的都會漏進 Linux,做 filesystem 掃描的 build system 會踩到。
-
Rosetta 翻譯 instruction,不翻譯 library。 你得自己裝 x86_64 的 libc、libstdc++、zlib。
-
virtiofs COW 讓容器裡的 patch 沒有代價。 這個特性很適合本地開發環境需要跟 CI 有差異的情況——改完容器一關就還原。
-
Build cache 是「可用」和「煩人」的分界線。 37% 的差距,沒有 cache 的話這個工作流大概不會有人想用。
-
Module build 是甜蜜點。 Full build 技術上可以但沒意義。每個工具做它擅長的事就好。
設定步驟
- macOS 26+ on Apple Silicon
- 安裝 Apple Container v1.0.0+
container system start(一次性,下載 VM kernel)- Build container image(Ubuntu 24.04 + ARM64 依賴 + amd64 Rosetta lib)
- 建立 persistent volume 給 build cache
- 寫 entrypoint 處理三個相容性問題
-v /path/to/aosp:/aosp掛載 AOSP treelunch <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。