Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
a070a41
feat(macos): define ARM64 guest profile and emulator paths
DeathofBrain Nov 5, 2026
55f5530
chore: remove test changes from macOS profile PR
DeathofBrain Nov 5, 2026
47e52c2
Merge upstream main and retain 1080p resolution defaults
DeathofBrain Nov 5, 2026
5ad74e7
feat: implement Darwin process runtime primitives
DeathofBrain Nov 5, 2026
1432755
Merge remote-tracking branch 'origin/main' into codex/darwin-process-…
DeathofBrain Nov 5, 2026
fcefa74
refactor: separate host routing from platform backends
DeathofBrain Nov 5, 2026
41fa471
Merge main into codex/darwin-process-runtime and resolve host conflicts
DeathofBrain Nov 5, 2026
a24cd4c
Merge latest main and retain portable host test imports
DeathofBrain Nov 5, 2026
f829e44
Merge remote-tracking branch 'origin/main' into codex/macos-arm64-pro…
DeathofBrain Nov 5, 2026
acd96b5
test: sync existing platform expectations for ARM64 profiles
DeathofBrain Nov 5, 2026
36197b7
Merge remote-tracking branch 'origin/main' into codex/macos-arm64-pro…
DeathofBrain Nov 5, 2026
e1ddfd2
Merge latest main and retain host routing for compact and logcat
DeathofBrain Nov 5, 2026
81ef060
Merge implemented macOS stages 1 and 2
DeathofBrain Nov 5, 2026
5adf4c7
feat: add macOS host and ARM64 boot preflight checks
DeathofBrain Nov 5, 2026
6f92a12
feat: fetch pinned Darwin ARM64 SDK components
DeathofBrain Nov 5, 2026
d22e58a
Merge remote-tracking branch 'origin/main' into codex/macos-1-api35-r…
DeathofBrain Nov 6, 2026
22e3dbf
fix: select Android API level from the host platform table
DeathofBrain Nov 6, 2026
71bb88c
Merge current main and preserve host routing for new upstream calls
DeathofBrain Nov 6, 2026
489f193
Merge branch 'codex/macos-1-api35-review' into codex/macos-review-base
DeathofBrain Nov 6, 2026
d8e8a46
Merge branch 'codex/macos-review-base' into codex/macos-4-review
DeathofBrain Nov 6, 2026
dd5463c
fix: check the platform API level in Darwin image metadata
DeathofBrain Nov 6, 2026
f32641c
Merge branch 'codex/macos-4-review' into codex/macos-6-review
DeathofBrain Nov 6, 2026
5c9f495
fix: pin Darwin system image to API 35 revision 9
DeathofBrain Nov 6, 2026
e7baf75
fix: identify Darwin SDK busy paths by directory identity
DeathofBrain Nov 6, 2026
4531acb
fix: use named macOS system image constant
DeathofBrain Nov 6, 2026
8c61f79
Merge branch 'codex/macos-1-api35-review' into codex/macos-review-base
DeathofBrain Nov 6, 2026
b8769d4
Merge branch 'codex/macos-review-base' into codex/macos-4-review
DeathofBrain Nov 6, 2026
1f7835f
Merge branch 'codex/macos-4-review' into codex/macos-6-review
DeathofBrain Nov 6, 2026
702f23b
Merge main into macOS preflight checks
DeathofBrain Nov 6, 2026
3d6c561
Merge commit '702f23b1bb60c944dc8e80a405642b395a5539cd' into codex/ma…
DeathofBrain Nov 6, 2026
7a315db
fix: align macOS component consent with manager download sources
DeathofBrain Nov 6, 2026
ea308bf
Merge main and preserve platform-specific component installation
DeathofBrain Nov 6, 2026
447c956
feat: prepare Google Play ARM64 components on macOS
DeathofBrain Nov 6, 2026
ab43103
Merge main and resolve macOS Google Play component conflicts
DeathofBrain Nov 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ modavd [--root <dir>] [--json] [--quiet] [--verbose] <command> [args]

**Google Play Store** (`components fetch --play-store`, `components import-playstore`, `playstore`, ModAVD#125):

On Apple Silicon macOS, downloads and imports use Android 15 Google Play `arm64-v8a` r9: the official `arm64-v8a-35_r09.zip` archive (about 1.8 GB). After license consent, download it or import your own archive/five files; their pinned sizes and hashes are checked before publication to `sdk/playstore/`. These files are separate from the Windows API34 x86_64 profile described below. macOS currently only prepares the component; installation into an instance and removal remain unavailable, and no execution bridge is required to prepare the files.

- The instances' Google APIs image has Google Play services but no store. The store is five files of Google's official `system-images;android-34;google_apis_playstore;x86_64` r14, the same release as the instances' image: the store, the Google-signed Play services, GSF and Partner Setup, and the store's fs-verity certificate. The root folder keeps only these five files (`sdk\playstore\`), and each instance gets them when asked; new instances never do.
- The root gets the five files one of two ways, both optional (`components status` and `check` never count them as required):
- Download: `components fetch --play-store` (in the manager, 「补全 Google Play 商店服务」 in the component dialog) downloads that image (about 1.5 GB) after the license agreement, reads the five files out of its `system.img` read-only, each checked against its built-in size and SHA-256, and deletes the unpacked `system.img` and the downloaded zip (the zip stays with `--keep-downloads`). Skipped when the files are there already.
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ modavd [--root <目录>] [--json] [--quiet] [--verbose] <命令> [参数]

**Google Play 商店**(`components fetch --play-store`、`components import-playstore`、`playstore`,ModAVD#125):

Apple Silicon macOS 的下载与导入使用 Android 15 Google Play `arm64-v8a` r9:官方归档为 `arm64-v8a-35_r09.zip`(约 1.8 GB)。许可确认后下载,或导入自己的归档/五个文件,按该版本的大小和摘要校验后保存到 `sdk/playstore/`。它与下面的 Windows API34 x86_64 文件不混用。当前 macOS 仅准备组件,实例内安装与移除尚未开放,不要求安装执行桥。

- 实例用的 Google APIs 镜像有 Play 服务、没有商店。商店取自 Google 官方的 `system-images;android-34;google_apis_playstore;x86_64` r14(和实例的镜像同一期)里的五个文件:商店、Google 签名的 Play 服务、GSF、合作伙伴设置和商店的 fs-verity 证书。根目录只留这五个文件(`sdk\playstore\`),每台实例要的时候再装进去;新建的实例不带。
- 根目录的五个文件有两种来法,都是可选的,`components status`、`check` 不把它们算进必需组件:
- 下载:`components fetch --play-store`(管理器「补全组件」里勾选「补全 Google Play 商店服务」)同意许可协议后下载这个镜像(约 1.5 GB),从里面的 `system.img` 只读取出五个文件,每个都核对写死的大小和 SHA-256,然后删掉解出的 `system.img` 和下载的压缩包(`--keep-downloads` 时压缩包留着)。已经有了就跳过。
Expand Down
6 changes: 4 additions & 2 deletions internal/cli/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ var (
"不询问,直接视为同意《Android 软件开发套件许可协议》(自动化用;协议全文照样取回并打印,取不到照样退出);--json 时必须给"}
optKeepDownloads = flagSpec{"keep-downloads", kindBool, "", "装好后保留 downloads 里的 zip(默认装好即删,省 1.5 GB)"}
optFetchStore = flagSpec{"play-store", kindBool, "",
"也补全可选的 Google Play 商店:下 Google Play 版系统镜像(约 1.5 GB),只取出商店等 5 个文件(约 230 MB)"}
playStoreFetchHelp()}
optMirror = flagSpec{"mirror", kindString, "ID", "只用一个内置镜像:googledownloads_cn / tencent / google"}
optMirrorURL = flagSpec{"mirror-url", kindStrings, "URL",
"自建或测试用:用这个目录地址代替内置的三个镜像,可写多次或用逗号隔开,按顺序试;给了就不看 --mirror"}
Expand Down Expand Up @@ -133,7 +133,9 @@ func init() {
"默认依次试 googledownloads.cn、腾讯云镜像、dl.google.com,下载约 1.8 GB,中断后再次运行会接着下;" +
"校验归档、版本和平台后装进 sdk/,保留执行权限。已经齐全的跳过;不完整的旧目录改名为 <目录>.incomplete-<时间> 留着,不删。" +
"SDK 使用中会拒绝补全,请先自行关闭使用它的进程。组件补全后,当前 macOS 版本仍未开放实例创建与启动。" +
"--play-store 和 import-playstore 目前仅支持 Windows。" +
"--play-store 可选下载 Android 15 Google Play arm64-v8a r9 镜像(约 1.8 GB),校验后只保留商店等五个文件。" +
"import-playstore 可导入官方 arm64-v8a-35_r09.zip 或五个文件,按固定大小和摘要检查,不下载。" +
"这两项仅准备文件,当前 macOS 版本尚未开放实例内安装与移除。" +
"source 查看或修改记住的下载源(auto、googledownloads_cn、tencent、google);--mirror 或 --mirror-url 可覆盖本次选择。" +
"--json 时必须给 --accept-license(仅 fetch),表示已同意待下载组件的全部许可协议。" +
"退出码:未同意许可是 2(license_declined),未就绪或预检失败是 4,SDK 使用中或正在补全是 5,下载不可用或校验失败是 9。"
Expand Down
18 changes: 16 additions & 2 deletions internal/cli/playstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ package cli
import (
"fmt"
"io"
"path/filepath"
"runtime"
"strings"
"time"

"modavd/internal/components"
"modavd/internal/errs"
"modavd/internal/playstore"
)

// importPlayStore is components.ImportPlayStore (a variable for the tests).
Expand All @@ -28,7 +31,7 @@ func runImportPlayStore(c *cmdCtx) (any, error) {
}
paths := c.inv.pos[1:]
if len(paths) == 0 {
return nil, errs.New(errs.Usage, "components import-playstore 要给文件或文件夹:Google Play 版镜像的压缩包 x86_64-34_r14.zip,或者商店的五个文件").
return nil, errs.New(errs.Usage, "components import-playstore 要给文件或文件夹:Google Play 版镜像的压缩包 "+filepath.Base(playstore.CurrentProfile().ImageURL)+",或者商店的五个文件").
WithAdvice(findCommand("components").usageAdvice())
}
files, err := components.PlayStoreImportFiles(paths)
Expand Down Expand Up @@ -92,5 +95,16 @@ func formatImportPlayStore(w io.Writer, r *components.PlayStoreImportResult) {
if r.SetAside != "" {
fmt.Fprintf(w, " 原来不完整的目录改名为 %s,确认没用后可以自己删掉\n", r.SetAside)
}
fmt.Fprintln(w, "给实例装上:modavd playstore <实例编号> install")
if runtime.GOOS == "darwin" {
fmt.Fprintln(w, "ARM64 商店文件已准备;当前 macOS 版本尚未开放实例内安装与移除")
} else {
fmt.Fprintln(w, "给实例装上:modavd playstore <实例编号> install")
}
}

func playStoreFetchHelp() string {
if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
return "也准备可选的 Google Play 商店:下载 API35 ARM64 r9 镜像(约 1.8 GB),校验后只保留五个文件;实例内装卸尚未开放"
}
return "也补全可选的 Google Play 商店:下 Google Play 版系统镜像(约 1.5 GB),只取出商店等 5 个文件(约 230 MB)"
}
3 changes: 0 additions & 3 deletions internal/components/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,9 +333,6 @@ func Fetch(ctx context.Context, r *root.Root, o Options) (*Result, error) {
if fetchGOOS != "windows" && !(fetchGOOS == "darwin" && nativeDarwinComponents) {
return nil, errs.New(errs.Unsupported, "这个系统还没有组件清单").WithAdvice("目前支持 Windows 和 Apple Silicon macOS")
}
if o.PlayStore && fetchGOOS != "windows" {
return nil, errs.New(errs.Unsupported, "目前只有 Windows 能补全 Google Play 商店组件")
}
if r == nil {
return nil, errs.New(errs.NotFound, "找不到魔改 AVD 根目录")
}
Expand Down
77 changes: 59 additions & 18 deletions internal/components/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,16 @@ func (f *fetcher) installPlayStore(ctx context.Context, c root.SDKComponent, zip
if err := f.playStoreFromZip(ctx, c, zipPath, staging, out); err != nil {
return "", err
}
return f.finishPlayStore(c, out, PlayStoreFromDownload)
return f.finishPlayStore(ctx, c, out, PlayStoreFromDownload)
}

// playStoreStaging is a fresh sdk\.staging\<id> and the directory in it the five files are gathered in.
func (f *fetcher) playStoreStaging(c root.SDKComponent) (staging, out string, err error) {
if f.darwin() {
if err := f.platformPaths(); err != nil {
return "", "", err
}
}
staging = filepath.Join(f.stagingRoot(), c.ID)
f.removeStaging(staging)
out = filepath.Join(staging, root.PlayStoreDir)
Expand All @@ -131,9 +136,10 @@ func (f *fetcher) playStoreStaging(c root.SDKComponent) (staging, out string, er
// staging\system.img, read without mounting anything (playStoreExtract checks each file's size and SHA-256) and
// deleted again, whether that worked or not.
func (f *fetcher) playStoreFromZip(ctx context.Context, c root.SDKComponent, zipPath, staging, out string) error {
f.where = "从 " + filepath.Base(zipPath) + " 解出 " + playstore.ImageMember
member := playstore.CurrentProfile().ImageMember
f.where = "从 " + filepath.Base(zipPath) + " 解出 " + member
img := filepath.Join(staging, "system.img")
err := f.extractMember(ctx, c, zipPath, playstore.ImageMember, img)
err := f.extractMember(ctx, c, zipPath, member, img)
if err == nil {
f.where = "从 system.img 取出 Google Play 商店的文件"
f.say(" 从系统镜像里取出商店、Play 服务、GSF、合作伙伴设置…")
Expand All @@ -149,14 +155,30 @@ func (f *fetcher) playStoreFromZip(ctx context.Context, c root.SDKComponent, zip

// finishPlayStore writes out's source.properties (with where the files came from), checks out as a whole (Complete:
// every pin and the revision) and moves it into sdk\playstore.
func (f *fetcher) finishPlayStore(c root.SDKComponent, out, source string) (setAside string, err error) {
func (f *fetcher) finishPlayStore(ctx context.Context, c root.SDKComponent, out, source string) (setAside string, err error) {
if f.darwin() {
if err := f.platformPaths(); err != nil {
return "", err
}
}
props := c.Properties + playStoreSourceKey + "=" + source + "\n"
if err := os.WriteFile(filepath.Join(out, "source.properties"), []byte(props), 0o644); err != nil {
return "", errs.Wrap(errs.Internal, err, "写不了 source.properties:"+err.Error())
}
if !c.Complete(out) {
return "", rejected("%s 的文件校验失败", c.FetchName)
}
if ctx.Err() != nil {
return "", canceled(ctx)
}
if f.darwin() {
if err := f.platformPaths(); err != nil {
return "", err
}
if err := f.platformIdle(); err != nil {
return "", err
}
}
return f.place(c, out)
}

Expand All @@ -168,28 +190,47 @@ func (f *fetcher) extractMember(ctx context.Context, c root.SDKComponent, zipPat
return rejected("压缩包打不开:%v", err)
}
defer zr.Close()
if f.darwin() && len(zr.File) > 20000 {
return rejected("压缩包条目过多")
}
var member *zip.File
for _, e := range zr.File {
if strings.ReplaceAll(e.Name, `\`, "/") != name {
continue
}
if e.Flags&1 != 0 {
return rejected("压缩包里的 %s 是加密的", e.Name)
if member != nil {
return rejected("压缩包里的 %s 出现了两次", name)
}
if e.Method != zip.Store && e.Method != zip.Deflate {
return rejected("压缩包里 %s 用了不支持的压缩方式 %d", e.Name, e.Method)
}
m := newMeter(f.o.Progress, "extracting", c.FetchName, max(int64(e.UncompressedSize64), 1), 0)
n, err := f.extractFile(ctx, e, dest, make([]byte, 1<<20), 0, m)
if err != nil {
return err
member = e
if !f.darwin() {
break
}
if n != int64(e.UncompressedSize64) {
return rejected("解压出的 %s 大小不对", e.Name)
}
if member == nil {
return rejected("压缩包里没有 %s", name)
}
if f.darwin() {
if member.Name != name || !member.Mode().IsRegular() ||
member.UncompressedSize64 != uint64(playstore.CurrentProfile().ImageMemberSize) {
return rejected("压缩包里的 %s 类型、路径或大小与组件清单不符", name)
}
m.finish(n)
return nil
}
return rejected("压缩包里没有 %s", name)
if member.Flags&1 != 0 {
return rejected("压缩包里的 %s 是加密的", name)
}
if member.Method != zip.Store && member.Method != zip.Deflate {
return rejected("压缩包里 %s 用了不支持的压缩方式 %d", name, member.Method)
}
m := newMeter(f.o.Progress, "extracting", c.FetchName, max(int64(member.UncompressedSize64), 1), 0)
n, err := f.extractFile(ctx, member, dest, make([]byte, 1<<20), 0, m)
if err != nil {
return err
}
if n != int64(member.UncompressedSize64) {
return rejected("解压出的 %s 大小不对", name)
}
m.finish(n)
return nil
}

// place moves an unpacked, checked component directory (extracted) into sdk\<target> (the end of the script's
Expand Down
11 changes: 10 additions & 1 deletion internal/components/platform_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,18 @@ func (f *fetcher) platformPaths() error {
filepath.Join(f.r.Dir, "downloads", licenseFile),
filepath.Join(f.r.Dir, "downloads", "android-sdk-arm-dbt-license.txt"),
}
for _, c := range componentCatalogue() {
specs := componentCatalogue()
if f.o.PlayStore {
specs = append(append([]root.SDKComponent(nil), specs...), playStoreCatalogue())
}
for _, c := range specs {
paths = append(paths, c.Dir(f.sdk))
files = append(files, f.zipPath(c), f.zipPath(c)+".part")
if c.ID == playStoreCatalogue().ID {
staging := filepath.Join(f.stagingRoot(), c.ID)
paths = append(paths, staging, filepath.Join(staging, root.PlayStoreDir))
files = append(files, filepath.Join(staging, "source.zip"), filepath.Join(staging, "system.img"))
}
}
for _, p := range paths {
rel, err := filepath.Rel(f.r.Dir, p)
Expand Down
Loading
Loading