From a070a41aeb172e29e32ba05f90986b2f6cec7bb7 Mon Sep 17 00:00:00 2001 From: DeathofBrain <109609173+DeathofBrain@users.noreply.github.com> Date: Thu, 5 Nov 2026 14:18:24 +0800 Subject: [PATCH 1/6] feat(macos): define ARM64 guest profile and emulator paths --- internal/avd/avd.go | 10 ++-- internal/avd/platform_test.go | 89 ++++++++++++++++++++++++++++++++ internal/root/components.go | 5 +- internal/root/components_test.go | 2 +- internal/root/platform.go | 46 +++++++++++------ internal/root/platform_test.go | 70 +++++++++++++++++++++++-- internal/root/root.go | 2 + 7 files changed, 200 insertions(+), 24 deletions(-) create mode 100644 internal/avd/platform_test.go diff --git a/internal/avd/avd.go b/internal/avd/avd.go index 5eee38b..1d14ee0 100644 --- a/internal/avd/avd.go +++ b/internal/avd/avd.go @@ -39,7 +39,8 @@ const ( // Display is fixed at 720p (user 10-04), written into config.ini before every boot, never `wm size`. ScreenWidth, ScreenHeight, ScreenDensity = 1280, 720, 240 TargetAPI = "android-34" - // image.sysdir.1, relative to the SDK root, with the trailing backslash AUTO-MAS writes. + // SystemImageSysdir is the Windows/AUTO-MAS value of image.sysdir.1. + // Create uses the host platform's system-image directory and path separator. SystemImageSysdir = `system-images\android-34\google_apis\x86_64\` ) @@ -605,6 +606,9 @@ func Create(r *root.Root, i int, opt CreateOptions) (*Instance, error) { for _, kv := range ConfigTemplate { cfg.Set(kv[0], kv[1]) } + p := root.CurrentPlatform() + cfg.Set("abi.type", p.GuestABI) + cfg.Set("hw.cpu.arch", p.GuestArch) for _, kv := range [][2]string{ {"avd.id", name}, {"avd.name", name}, @@ -616,14 +620,14 @@ func Create(r *root.Root, i int, opt CreateOptions) (*Instance, error) { {"hw.lcd.height", strconv.Itoa(ScreenHeight)}, {"hw.lcd.density", strconv.Itoa(ScreenDensity)}, {"hw.lcd.vsync", strconv.Itoa(fps)}, - {"image.sysdir.1", SystemImageSysdir}, + {"image.sysdir.1", p.SystemImageDir + string(filepath.Separator)}, } { cfg.Set(kv[0], kv[1]) } ini := NewIni() ini.Set("avd.ini.encoding", "UTF-8") ini.Set("path", dir) - ini.Set("path.rel", root.AvdDir+`\`+name+".avd") + ini.Set("path.rel", filepath.Join(root.AvdDir, name+".avd")) ini.Set("target", TargetAPI) if err := os.MkdirAll(r.AvdHome(), 0o755); err != nil { diff --git a/internal/avd/platform_test.go b/internal/avd/platform_test.go new file mode 100644 index 0000000..07a6e01 --- /dev/null +++ b/internal/avd/platform_test.go @@ -0,0 +1,89 @@ +package avd + +import ( + "os" + "path/filepath" + "runtime" + "slices" + "testing" + + "modavd/internal/root" +) + +func TestCreateHostGuestProfile(t *testing.T) { + var abi, arch, imageDir string + switch { + case runtime.GOOS == "windows" && runtime.GOARCH == "amd64": + abi, arch, imageDir = "x86_64", "x86_64", `system-images\android-34\google_apis\x86_64` + case runtime.GOOS == "darwin" && runtime.GOARCH == "arm64": + abi, arch, imageDir = "arm64-v8a", "arm64", "system-images/android-34/google_apis/arm64-v8a" + case runtime.GOOS == "linux" && runtime.GOARCH == "amd64": + abi, arch, imageDir = "x86_64", "x86_64", "system-images/android-34/google_apis/x86_64" + default: + t.Skip("no guest profile for this host") + } + for _, localSDK := range []bool{false, true} { + t.Run(map[bool]string{false: "packaged", true: "sdkRoot"}[localSDK], func(t *testing.T) { + r, err := root.New(t.TempDir()) + if err != nil { + t.Fatal(err) + } + if localSDK { + if err := os.WriteFile(r.MetadataPath(), []byte(`{"sdkRoot":"runtime sdk"}`), 0o644); err != nil { + t.Fatal(err) + } + } + imagePath := filepath.Join(r.RuntimeSDK(), imageDir, "system.img") + if err := os.MkdirAll(filepath.Dir(imagePath), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(imagePath, []byte("host image fixture"), 0o644); err != nil { + t.Fatal(err) + } + in, err := Create(r, 1, CreateOptions{Title: "平台配置", MemoryMB: 4096, CPU: 4, DataPartitionGB: 16, FPS: 60}) + if err != nil { + t.Fatal(err) + } + cfg, err := ReadIni(ConfigPath(r, in.Index)) + if err != nil { + t.Fatal(err) + } + for key, want := range map[string]string{ + "abi.type": abi, "hw.cpu.arch": arch, + "image.sysdir.1": imageDir + string(filepath.Separator), + "hw.cpu.ncore": "4", "hw.ramSize": "4096M", "avd.ini.displayname": "平台配置", + } { + if got := cfg.Value(key); got != want { + t.Errorf("%s = %q; want %q", key, got, want) + } + } + // Resolve the actual written image.sysdir.1 against the selected SDK, including sdkRoot overrides. + image, err := os.ReadFile(filepath.Join(r.RuntimeSDK(), cfg.Value("image.sysdir.1"), "system.img")) + if err != nil || string(image) != "host image fixture" { + t.Fatalf("written system-image path does not resolve: %q, %v", image, err) + } + ini, err := ReadIni(IniPath(r, in.Index)) + if err != nil { + t.Fatal(err) + } + if ini.Value("path") != in.Dir || ini.Value("path.rel") != filepath.Join("avd", "mas_1.avd") { + t.Fatalf("AVD directory paths: path=%q path.rel=%q", ini.Value("path"), ini.Value("path.rel")) + } + if _, err := os.Stat(filepath.Join(r.Dir, ini.Value("path.rel"), "config.ini")); err != nil { + t.Fatalf("written path.rel does not resolve to the AVD: %v", err) + } + if !slices.Equal(ini.Keys(), []string{"avd.ini.encoding", "path", "path.rel", "target"}) { + t.Fatalf("AVD .ini key order changed: %v", ini.Keys()) + } + keys := cfg.Keys() + if len(keys) < len(ConfigTemplate) { + t.Fatalf("config has fewer keys than its template: %v", keys) + } + for n, kv := range ConfigTemplate { + if keys[n] != kv[0] { + t.Fatalf("template key %d moved: got %s; want %s", n, keys[n], kv[0]) + } + } + }) + } +} diff --git a/internal/root/components.go b/internal/root/components.go index c9679cc..af40e09 100644 --- a/internal/root/components.go +++ b/internal/root/components.go @@ -96,10 +96,11 @@ func SDKComponents() []SDKComponent { if runtime.GOOS == "windows" { return WindowsComponents() } + p := CurrentPlatform() return []SDKComponent{ {ID: "platform-tools", Name: PlatformToolsName, FetchName: PlatformToolsName, Target: "platform-tools", - StatusFiles: []string{CurrentPlatform().ADB}}, - {ID: "system-image", Name: SystemImageName, FetchName: SystemImageName, Target: SystemImageDir, + StatusFiles: []string{p.ADB}}, + {ID: "system-image", Name: p.SystemImageName, FetchName: p.SystemImageName, Target: p.SystemImageDir, StatusFiles: systemImageFiles()}, } } diff --git a/internal/root/components_test.go b/internal/root/components_test.go index c841e17..6f79923 100644 --- a/internal/root/components_test.go +++ b/internal/root/components_test.go @@ -211,7 +211,7 @@ func TestComponentsUseTable(t *testing.T) { } } comps := r.Components() - if !comps[0].Installed || !comps[2].Installed || comps[0].Name != PlatformToolsName || comps[2].Name != SystemImageName { + if !comps[0].Installed || !comps[2].Installed || comps[0].Name != PlatformToolsName || comps[2].Name != SDKComponents()[1].Name { t.Fatalf("%+v", comps) } os.Remove(filepath.Join(SDKComponents()[1].Dir(r.SDK()), SDKComponents()[1].StatusFiles[0])) diff --git a/internal/root/platform.go b/internal/root/platform.go index 4dda25b..59652a8 100644 --- a/internal/root/platform.go +++ b/internal/root/platform.go @@ -16,17 +16,21 @@ func platformKeyFiles() map[string][]string { } } -// Platform describes host-specific executable names and update identifiers. -// Callers use this table instead of embedding Windows names in shared logic. +// Platform describes host executable names, guest images and update identifiers. +// Callers use this table instead of embedding Windows or x86_64 values in shared logic. type Platform struct { - Emulator string - ADB string - QEMU string - QEMUHeadless string - QemuDir string - PackageSuffix string - MirrorOS string - MirrorArch string + Emulator string + ADB string + QEMU string + QEMUHeadless string + QemuDir string + PackageSuffix string + MirrorOS string + MirrorArch string + GuestABI string + GuestArch string + SystemImageDir string + SystemImageName string } // Supported reports whether the host matches one of the package's platform rows. @@ -41,13 +45,25 @@ func (p Platform) Windows() bool { return p.QemuDir == "windows-x86_64" && p.Mir func CurrentPlatform() Platform { switch { case runtime.GOOS == "darwin" && runtime.GOARCH == "arm64": - // The qemu directory on macOS is expected to be qemu/darwin-aarch64; - // #8 will verify the package layout before changing this table. - return Platform{Emulator: "emulator", ADB: "adb", QEMU: "qemu-system-aarch64", QEMUHeadless: "qemu-system-aarch64", PackageSuffix: "macos-arm64", MirrorOS: "macos", MirrorArch: "arm64"} + return Platform{ + Emulator: "emulator", ADB: "adb", QEMU: "qemu-system-aarch64", QEMUHeadless: "qemu-system-aarch64-headless", QemuDir: "darwin-aarch64", + PackageSuffix: "macos-arm64", MirrorOS: "macos", MirrorArch: "arm64", + GuestABI: "arm64-v8a", GuestArch: "arm64", + SystemImageDir: "system-images/android-34/google_apis/arm64-v8a", SystemImageName: "系统镜像 Android 14 Google APIs arm64-v8a", + } case runtime.GOOS == "linux" && runtime.GOARCH == "amd64": - return Platform{Emulator: "emulator", ADB: "adb", QEMU: "qemu-system-x86_64", QEMUHeadless: "qemu-system-x86_64", PackageSuffix: "linux-x64", MirrorOS: "linux", MirrorArch: "x64"} + return Platform{ + Emulator: "emulator", ADB: "adb", QEMU: "qemu-system-x86_64", QEMUHeadless: "qemu-system-x86_64", + PackageSuffix: "linux-x64", MirrorOS: "linux", MirrorArch: "x64", + GuestABI: "x86_64", GuestArch: "x86_64", + SystemImageDir: "system-images/android-34/google_apis/x86_64", SystemImageName: SystemImageName, + } case runtime.GOOS == "windows" && runtime.GOARCH == "amd64": - return Platform{Emulator: "emulator.exe", ADB: "adb.exe", QEMU: "qemu-system-x86_64.exe", QEMUHeadless: "qemu-system-x86_64-headless.exe", QemuDir: "windows-x86_64", PackageSuffix: "win-x64", MirrorOS: "win", MirrorArch: "x64"} + return Platform{ + Emulator: "emulator.exe", ADB: "adb.exe", QEMU: "qemu-system-x86_64.exe", QEMUHeadless: "qemu-system-x86_64-headless.exe", QemuDir: "windows-x86_64", + PackageSuffix: "win-x64", MirrorOS: "win", MirrorArch: "x64", + GuestABI: "x86_64", GuestArch: "x86_64", SystemImageDir: SystemImageDir, SystemImageName: SystemImageName, + } default: // An unlisted host must not inherit Windows names or MirrorChyan ids. return Platform{} diff --git a/internal/root/platform_test.go b/internal/root/platform_test.go index 855a7b0..b643e98 100644 --- a/internal/root/platform_test.go +++ b/internal/root/platform_test.go @@ -1,6 +1,8 @@ package root import ( + "os" + "path/filepath" "runtime" "testing" ) @@ -9,17 +11,17 @@ func TestCurrentPlatformMatchesHost(t *testing.T) { p := CurrentPlatform() switch { case runtime.GOOS == "windows" && runtime.GOARCH == "amd64": - want := Platform{Emulator: "emulator.exe", ADB: "adb.exe", QEMU: "qemu-system-x86_64.exe", QEMUHeadless: "qemu-system-x86_64-headless.exe", QemuDir: "windows-x86_64", PackageSuffix: "win-x64", MirrorOS: "win", MirrorArch: "x64"} + want := Platform{Emulator: "emulator.exe", ADB: "adb.exe", QEMU: "qemu-system-x86_64.exe", QEMUHeadless: "qemu-system-x86_64-headless.exe", QemuDir: "windows-x86_64", PackageSuffix: "win-x64", MirrorOS: "win", MirrorArch: "x64", GuestABI: "x86_64", GuestArch: "x86_64", SystemImageDir: `system-images\android-34\google_apis\x86_64`, SystemImageName: "系统镜像 Android 14 Google APIs x86_64"} if p != want { t.Fatalf("Windows platform: %+v", p) } case runtime.GOOS == "darwin" && runtime.GOARCH == "arm64": - want := Platform{Emulator: "emulator", ADB: "adb", QEMU: "qemu-system-aarch64", QEMUHeadless: "qemu-system-aarch64", PackageSuffix: "macos-arm64", MirrorOS: "macos", MirrorArch: "arm64"} + want := Platform{Emulator: "emulator", ADB: "adb", QEMU: "qemu-system-aarch64", QEMUHeadless: "qemu-system-aarch64-headless", QemuDir: "darwin-aarch64", PackageSuffix: "macos-arm64", MirrorOS: "macos", MirrorArch: "arm64", GuestABI: "arm64-v8a", GuestArch: "arm64", SystemImageDir: "system-images/android-34/google_apis/arm64-v8a", SystemImageName: "系统镜像 Android 14 Google APIs arm64-v8a"} if p != want { t.Fatalf("darwin platform: %+v", p) } case runtime.GOOS == "linux" && runtime.GOARCH == "amd64": - want := Platform{Emulator: "emulator", ADB: "adb", QEMU: "qemu-system-x86_64", QEMUHeadless: "qemu-system-x86_64", PackageSuffix: "linux-x64", MirrorOS: "linux", MirrorArch: "x64"} + want := Platform{Emulator: "emulator", ADB: "adb", QEMU: "qemu-system-x86_64", QEMUHeadless: "qemu-system-x86_64", PackageSuffix: "linux-x64", MirrorOS: "linux", MirrorArch: "x64", GuestABI: "x86_64", GuestArch: "x86_64", SystemImageDir: "system-images/android-34/google_apis/x86_64", SystemImageName: "系统镜像 Android 14 Google APIs x86_64"} if p != want { t.Fatalf("linux platform: %+v", p) } @@ -28,4 +30,66 @@ func TestCurrentPlatformMatchesHost(t *testing.T) { t.Fatalf("unsupported platform inherited values: %+v", p) } } + wantSupported := runtime.GOOS == "windows" && runtime.GOARCH == "amd64" || + runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" || + runtime.GOOS == "linux" && runtime.GOARCH == "amd64" + if p.Supported() != wantSupported || p.Windows() != (runtime.GOOS == "windows" && runtime.GOARCH == "amd64") { + t.Fatalf("host %s/%s has incorrect platform capabilities: %+v", runtime.GOOS, runtime.GOARCH, p) + } +} + +func TestWindowsImageConstantsStayCompatible(t *testing.T) { + // WindowsComponents is also inspected on Unix hosts; these constants must not follow the host profile. + if SystemImageDir != `system-images\android-34\google_apis\x86_64` || + SystemImageName != "系统镜像 Android 14 Google APIs x86_64" { + t.Fatalf("Windows image constants changed: %q, %q", SystemImageDir, SystemImageName) + } + if (Platform{}).Supported() || (Platform{}).Windows() { + t.Fatal("an unlisted platform must not inherit a supported host") + } +} + +func TestPlatformRuntimeSDKPaths(t *testing.T) { + p := CurrentPlatform() + if !p.Supported() { + t.Skip("no executable layout for this host") + } + for _, localSDK := range []bool{false, true} { + t.Run(map[bool]string{false: "packaged", true: "sdkRoot"}[localSDK], func(t *testing.T) { + r, err := New(t.TempDir()) + if err != nil { + t.Fatal(err) + } + sdk := filepath.Join(r.Dir, "sdk") + if localSDK { + if err := os.WriteFile(r.MetadataPath(), []byte(`{"sdkRoot":"runtime sdk"}`), 0o644); err != nil { + t.Fatal(err) + } + sdk = filepath.Join(r.Dir, "runtime sdk") + } + if r.RuntimeSDK() != sdk || r.EmulatorExe() != filepath.Join(sdk, "emulator", p.Emulator) || + r.AdbExe() != filepath.Join(sdk, "platform-tools", p.ADB) || + r.QemuHeadlessExe() != filepath.Join(sdk, "emulator", "qemu", p.QemuDir, p.QEMUHeadless) { + t.Fatalf("runtime SDK layout: sdk=%q emulator=%q adb=%q qemu=%q", r.RuntimeSDK(), r.EmulatorExe(), r.AdbExe(), r.QemuHeadlessExe()) + } + qemu := filepath.Join(sdk, "emulator", "qemu", p.QemuDir, p.QEMU) + for _, path := range []string{r.EmulatorExe(), qemu} { + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte("fixture"), 0o644); err != nil { + t.Fatal(err) + } + } + if !r.EmulatorPresent() { + t.Fatal("component detection did not find the host's emulator layout") + } + if err := os.Remove(qemu); err != nil { + t.Fatal(err) + } + if r.EmulatorPresent() { + t.Fatal("emulator reported present without its host QEMU binary") + } + }) + } } diff --git a/internal/root/root.go b/internal/root/root.go index 8a0e197..235e744 100644 --- a/internal/root/root.go +++ b/internal/root/root.go @@ -192,6 +192,7 @@ func jsonOneLine(v *jsonord.Value) string { // ---- layout ------------------------------------------------------------------------------------------------------ +// SystemImageDir retains the Windows/AUTO-MAS layout; other hosts use Platform.SystemImageDir. const ( SDKDir = "sdk" AvdDir = "avd" @@ -741,6 +742,7 @@ type ComponentStatus struct { } // Component names and fixed data (AUTO-MAS constants.py). +// SystemImageName retains the Windows name; other hosts use Platform.SystemImageName. const ( PlatformToolsName = "平台工具(adb)" EmulatorName = "Android 模拟器(emulator)" From 55f5530d5af4a7cd2dfd5cd326b06a2f07d250cf Mon Sep 17 00:00:00 2001 From: DeathofBrain <109609173+DeathofBrain@users.noreply.github.com> Date: Thu, 5 Nov 2026 18:02:30 +0800 Subject: [PATCH 2/6] chore: remove test changes from macOS profile PR --- internal/avd/platform_test.go | 89 -------------------------------- internal/root/components_test.go | 2 +- internal/root/platform_test.go | 70 ++----------------------- 3 files changed, 4 insertions(+), 157 deletions(-) delete mode 100644 internal/avd/platform_test.go diff --git a/internal/avd/platform_test.go b/internal/avd/platform_test.go deleted file mode 100644 index 07a6e01..0000000 --- a/internal/avd/platform_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package avd - -import ( - "os" - "path/filepath" - "runtime" - "slices" - "testing" - - "modavd/internal/root" -) - -func TestCreateHostGuestProfile(t *testing.T) { - var abi, arch, imageDir string - switch { - case runtime.GOOS == "windows" && runtime.GOARCH == "amd64": - abi, arch, imageDir = "x86_64", "x86_64", `system-images\android-34\google_apis\x86_64` - case runtime.GOOS == "darwin" && runtime.GOARCH == "arm64": - abi, arch, imageDir = "arm64-v8a", "arm64", "system-images/android-34/google_apis/arm64-v8a" - case runtime.GOOS == "linux" && runtime.GOARCH == "amd64": - abi, arch, imageDir = "x86_64", "x86_64", "system-images/android-34/google_apis/x86_64" - default: - t.Skip("no guest profile for this host") - } - for _, localSDK := range []bool{false, true} { - t.Run(map[bool]string{false: "packaged", true: "sdkRoot"}[localSDK], func(t *testing.T) { - r, err := root.New(t.TempDir()) - if err != nil { - t.Fatal(err) - } - if localSDK { - if err := os.WriteFile(r.MetadataPath(), []byte(`{"sdkRoot":"runtime sdk"}`), 0o644); err != nil { - t.Fatal(err) - } - } - imagePath := filepath.Join(r.RuntimeSDK(), imageDir, "system.img") - if err := os.MkdirAll(filepath.Dir(imagePath), 0o755); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(imagePath, []byte("host image fixture"), 0o644); err != nil { - t.Fatal(err) - } - in, err := Create(r, 1, CreateOptions{Title: "平台配置", MemoryMB: 4096, CPU: 4, DataPartitionGB: 16, FPS: 60}) - if err != nil { - t.Fatal(err) - } - cfg, err := ReadIni(ConfigPath(r, in.Index)) - if err != nil { - t.Fatal(err) - } - for key, want := range map[string]string{ - "abi.type": abi, "hw.cpu.arch": arch, - "image.sysdir.1": imageDir + string(filepath.Separator), - "hw.cpu.ncore": "4", "hw.ramSize": "4096M", "avd.ini.displayname": "平台配置", - } { - if got := cfg.Value(key); got != want { - t.Errorf("%s = %q; want %q", key, got, want) - } - } - // Resolve the actual written image.sysdir.1 against the selected SDK, including sdkRoot overrides. - image, err := os.ReadFile(filepath.Join(r.RuntimeSDK(), cfg.Value("image.sysdir.1"), "system.img")) - if err != nil || string(image) != "host image fixture" { - t.Fatalf("written system-image path does not resolve: %q, %v", image, err) - } - ini, err := ReadIni(IniPath(r, in.Index)) - if err != nil { - t.Fatal(err) - } - if ini.Value("path") != in.Dir || ini.Value("path.rel") != filepath.Join("avd", "mas_1.avd") { - t.Fatalf("AVD directory paths: path=%q path.rel=%q", ini.Value("path"), ini.Value("path.rel")) - } - if _, err := os.Stat(filepath.Join(r.Dir, ini.Value("path.rel"), "config.ini")); err != nil { - t.Fatalf("written path.rel does not resolve to the AVD: %v", err) - } - if !slices.Equal(ini.Keys(), []string{"avd.ini.encoding", "path", "path.rel", "target"}) { - t.Fatalf("AVD .ini key order changed: %v", ini.Keys()) - } - keys := cfg.Keys() - if len(keys) < len(ConfigTemplate) { - t.Fatalf("config has fewer keys than its template: %v", keys) - } - for n, kv := range ConfigTemplate { - if keys[n] != kv[0] { - t.Fatalf("template key %d moved: got %s; want %s", n, keys[n], kv[0]) - } - } - }) - } -} diff --git a/internal/root/components_test.go b/internal/root/components_test.go index 6f79923..c841e17 100644 --- a/internal/root/components_test.go +++ b/internal/root/components_test.go @@ -211,7 +211,7 @@ func TestComponentsUseTable(t *testing.T) { } } comps := r.Components() - if !comps[0].Installed || !comps[2].Installed || comps[0].Name != PlatformToolsName || comps[2].Name != SDKComponents()[1].Name { + if !comps[0].Installed || !comps[2].Installed || comps[0].Name != PlatformToolsName || comps[2].Name != SystemImageName { t.Fatalf("%+v", comps) } os.Remove(filepath.Join(SDKComponents()[1].Dir(r.SDK()), SDKComponents()[1].StatusFiles[0])) diff --git a/internal/root/platform_test.go b/internal/root/platform_test.go index b643e98..855a7b0 100644 --- a/internal/root/platform_test.go +++ b/internal/root/platform_test.go @@ -1,8 +1,6 @@ package root import ( - "os" - "path/filepath" "runtime" "testing" ) @@ -11,17 +9,17 @@ func TestCurrentPlatformMatchesHost(t *testing.T) { p := CurrentPlatform() switch { case runtime.GOOS == "windows" && runtime.GOARCH == "amd64": - want := Platform{Emulator: "emulator.exe", ADB: "adb.exe", QEMU: "qemu-system-x86_64.exe", QEMUHeadless: "qemu-system-x86_64-headless.exe", QemuDir: "windows-x86_64", PackageSuffix: "win-x64", MirrorOS: "win", MirrorArch: "x64", GuestABI: "x86_64", GuestArch: "x86_64", SystemImageDir: `system-images\android-34\google_apis\x86_64`, SystemImageName: "系统镜像 Android 14 Google APIs x86_64"} + want := Platform{Emulator: "emulator.exe", ADB: "adb.exe", QEMU: "qemu-system-x86_64.exe", QEMUHeadless: "qemu-system-x86_64-headless.exe", QemuDir: "windows-x86_64", PackageSuffix: "win-x64", MirrorOS: "win", MirrorArch: "x64"} if p != want { t.Fatalf("Windows platform: %+v", p) } case runtime.GOOS == "darwin" && runtime.GOARCH == "arm64": - want := Platform{Emulator: "emulator", ADB: "adb", QEMU: "qemu-system-aarch64", QEMUHeadless: "qemu-system-aarch64-headless", QemuDir: "darwin-aarch64", PackageSuffix: "macos-arm64", MirrorOS: "macos", MirrorArch: "arm64", GuestABI: "arm64-v8a", GuestArch: "arm64", SystemImageDir: "system-images/android-34/google_apis/arm64-v8a", SystemImageName: "系统镜像 Android 14 Google APIs arm64-v8a"} + want := Platform{Emulator: "emulator", ADB: "adb", QEMU: "qemu-system-aarch64", QEMUHeadless: "qemu-system-aarch64", PackageSuffix: "macos-arm64", MirrorOS: "macos", MirrorArch: "arm64"} if p != want { t.Fatalf("darwin platform: %+v", p) } case runtime.GOOS == "linux" && runtime.GOARCH == "amd64": - want := Platform{Emulator: "emulator", ADB: "adb", QEMU: "qemu-system-x86_64", QEMUHeadless: "qemu-system-x86_64", PackageSuffix: "linux-x64", MirrorOS: "linux", MirrorArch: "x64", GuestABI: "x86_64", GuestArch: "x86_64", SystemImageDir: "system-images/android-34/google_apis/x86_64", SystemImageName: "系统镜像 Android 14 Google APIs x86_64"} + want := Platform{Emulator: "emulator", ADB: "adb", QEMU: "qemu-system-x86_64", QEMUHeadless: "qemu-system-x86_64", PackageSuffix: "linux-x64", MirrorOS: "linux", MirrorArch: "x64"} if p != want { t.Fatalf("linux platform: %+v", p) } @@ -30,66 +28,4 @@ func TestCurrentPlatformMatchesHost(t *testing.T) { t.Fatalf("unsupported platform inherited values: %+v", p) } } - wantSupported := runtime.GOOS == "windows" && runtime.GOARCH == "amd64" || - runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" || - runtime.GOOS == "linux" && runtime.GOARCH == "amd64" - if p.Supported() != wantSupported || p.Windows() != (runtime.GOOS == "windows" && runtime.GOARCH == "amd64") { - t.Fatalf("host %s/%s has incorrect platform capabilities: %+v", runtime.GOOS, runtime.GOARCH, p) - } -} - -func TestWindowsImageConstantsStayCompatible(t *testing.T) { - // WindowsComponents is also inspected on Unix hosts; these constants must not follow the host profile. - if SystemImageDir != `system-images\android-34\google_apis\x86_64` || - SystemImageName != "系统镜像 Android 14 Google APIs x86_64" { - t.Fatalf("Windows image constants changed: %q, %q", SystemImageDir, SystemImageName) - } - if (Platform{}).Supported() || (Platform{}).Windows() { - t.Fatal("an unlisted platform must not inherit a supported host") - } -} - -func TestPlatformRuntimeSDKPaths(t *testing.T) { - p := CurrentPlatform() - if !p.Supported() { - t.Skip("no executable layout for this host") - } - for _, localSDK := range []bool{false, true} { - t.Run(map[bool]string{false: "packaged", true: "sdkRoot"}[localSDK], func(t *testing.T) { - r, err := New(t.TempDir()) - if err != nil { - t.Fatal(err) - } - sdk := filepath.Join(r.Dir, "sdk") - if localSDK { - if err := os.WriteFile(r.MetadataPath(), []byte(`{"sdkRoot":"runtime sdk"}`), 0o644); err != nil { - t.Fatal(err) - } - sdk = filepath.Join(r.Dir, "runtime sdk") - } - if r.RuntimeSDK() != sdk || r.EmulatorExe() != filepath.Join(sdk, "emulator", p.Emulator) || - r.AdbExe() != filepath.Join(sdk, "platform-tools", p.ADB) || - r.QemuHeadlessExe() != filepath.Join(sdk, "emulator", "qemu", p.QemuDir, p.QEMUHeadless) { - t.Fatalf("runtime SDK layout: sdk=%q emulator=%q adb=%q qemu=%q", r.RuntimeSDK(), r.EmulatorExe(), r.AdbExe(), r.QemuHeadlessExe()) - } - qemu := filepath.Join(sdk, "emulator", "qemu", p.QemuDir, p.QEMU) - for _, path := range []string{r.EmulatorExe(), qemu} { - if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(path, []byte("fixture"), 0o644); err != nil { - t.Fatal(err) - } - } - if !r.EmulatorPresent() { - t.Fatal("component detection did not find the host's emulator layout") - } - if err := os.Remove(qemu); err != nil { - t.Fatal(err) - } - if r.EmulatorPresent() { - t.Fatal("emulator reported present without its host QEMU binary") - } - }) - } } From acd96b5d4dae4810c5f4e9fe5e8f45aeb20420ab Mon Sep 17 00:00:00 2001 From: DeathofBrain <109609173+DeathofBrain@users.noreply.github.com> Date: Thu, 5 Nov 2026 21:09:10 +0800 Subject: [PATCH 3/6] test: sync existing platform expectations for ARM64 profiles --- internal/root/components_test.go | 2 +- internal/root/platform_test.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/root/components_test.go b/internal/root/components_test.go index ec699e3..6ef6bd3 100644 --- a/internal/root/components_test.go +++ b/internal/root/components_test.go @@ -213,7 +213,7 @@ func TestComponentsUseTable(t *testing.T) { } } comps := r.Components() - if !comps[0].Installed || !comps[2].Installed || comps[0].Name != PlatformToolsName || comps[2].Name != SystemImageName { + if !comps[0].Installed || !comps[2].Installed || comps[0].Name != PlatformToolsName || comps[2].Name != CurrentPlatform().SystemImageName { t.Fatalf("%+v", comps) } os.Remove(filepath.Join(SDKComponents()[1].Dir(r.SDK()), SDKComponents()[1].StatusFiles[0])) diff --git a/internal/root/platform_test.go b/internal/root/platform_test.go index 855a7b0..a5c18d7 100644 --- a/internal/root/platform_test.go +++ b/internal/root/platform_test.go @@ -9,17 +9,17 @@ func TestCurrentPlatformMatchesHost(t *testing.T) { p := CurrentPlatform() switch { case runtime.GOOS == "windows" && runtime.GOARCH == "amd64": - want := Platform{Emulator: "emulator.exe", ADB: "adb.exe", QEMU: "qemu-system-x86_64.exe", QEMUHeadless: "qemu-system-x86_64-headless.exe", QemuDir: "windows-x86_64", PackageSuffix: "win-x64", MirrorOS: "win", MirrorArch: "x64"} + want := Platform{Emulator: "emulator.exe", ADB: "adb.exe", QEMU: "qemu-system-x86_64.exe", QEMUHeadless: "qemu-system-x86_64-headless.exe", QemuDir: "windows-x86_64", PackageSuffix: "win-x64", MirrorOS: "win", MirrorArch: "x64", GuestABI: "x86_64", GuestArch: "x86_64", SystemImageDir: SystemImageDir, SystemImageName: SystemImageName} if p != want { t.Fatalf("Windows platform: %+v", p) } case runtime.GOOS == "darwin" && runtime.GOARCH == "arm64": - want := Platform{Emulator: "emulator", ADB: "adb", QEMU: "qemu-system-aarch64", QEMUHeadless: "qemu-system-aarch64", PackageSuffix: "macos-arm64", MirrorOS: "macos", MirrorArch: "arm64"} + want := Platform{Emulator: "emulator", ADB: "adb", QEMU: "qemu-system-aarch64", QEMUHeadless: "qemu-system-aarch64-headless", QemuDir: "darwin-aarch64", PackageSuffix: "macos-arm64", MirrorOS: "macos", MirrorArch: "arm64", GuestABI: "arm64-v8a", GuestArch: "arm64", SystemImageDir: "system-images/android-34/google_apis/arm64-v8a", SystemImageName: "系统镜像 Android 14 Google APIs arm64-v8a"} if p != want { t.Fatalf("darwin platform: %+v", p) } case runtime.GOOS == "linux" && runtime.GOARCH == "amd64": - want := Platform{Emulator: "emulator", ADB: "adb", QEMU: "qemu-system-x86_64", QEMUHeadless: "qemu-system-x86_64", PackageSuffix: "linux-x64", MirrorOS: "linux", MirrorArch: "x64"} + want := Platform{Emulator: "emulator", ADB: "adb", QEMU: "qemu-system-x86_64", QEMUHeadless: "qemu-system-x86_64", PackageSuffix: "linux-x64", MirrorOS: "linux", MirrorArch: "x64", GuestABI: "x86_64", GuestArch: "x86_64", SystemImageDir: "system-images/android-34/google_apis/x86_64", SystemImageName: SystemImageName} if p != want { t.Fatalf("linux platform: %+v", p) } From 22e3dbf111c094c0f4ee4c89c2616695ab5e9170 Mon Sep 17 00:00:00 2001 From: DeathofBrain <109609173+DeathofBrain@users.noreply.github.com> Date: Fri, 6 Nov 2026 18:19:47 +0800 Subject: [PATCH 4/6] fix: select Android API level from the host platform table --- internal/avd/avd.go | 3 ++- internal/root/platform.go | 9 +++++---- internal/root/platform_test.go | 6 +++--- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/internal/avd/avd.go b/internal/avd/avd.go index a75fc9e..4924829 100644 --- a/internal/avd/avd.go +++ b/internal/avd/avd.go @@ -632,6 +632,7 @@ func Create(r *root.Root, i int, opt CreateOptions) (*Instance, error) { p := root.CurrentPlatform() cfg.Set("abi.type", p.GuestABI) cfg.Set("hw.cpu.arch", p.GuestArch) + cfg.Set("target", p.TargetAPI) for _, kv := range [][2]string{ {"avd.id", name}, {"avd.name", name}, @@ -651,7 +652,7 @@ func Create(r *root.Root, i int, opt CreateOptions) (*Instance, error) { ini.Set("avd.ini.encoding", "UTF-8") ini.Set("path", dir) ini.Set("path.rel", filepath.Join(root.AvdDir, name+".avd")) - ini.Set("target", TargetAPI) + ini.Set("target", p.TargetAPI) if err := os.MkdirAll(r.AvdHome(), 0o755); err != nil { return nil, errs.Wrap(errs.Internal, err, fmt.Sprintf("建不了目录 %s:%v", r.AvdHome(), err)) diff --git a/internal/root/platform.go b/internal/root/platform.go index 59652a8..28bc557 100644 --- a/internal/root/platform.go +++ b/internal/root/platform.go @@ -29,6 +29,7 @@ type Platform struct { MirrorArch string GuestABI string GuestArch string + TargetAPI string SystemImageDir string SystemImageName string } @@ -48,21 +49,21 @@ func CurrentPlatform() Platform { return Platform{ Emulator: "emulator", ADB: "adb", QEMU: "qemu-system-aarch64", QEMUHeadless: "qemu-system-aarch64-headless", QemuDir: "darwin-aarch64", PackageSuffix: "macos-arm64", MirrorOS: "macos", MirrorArch: "arm64", - GuestABI: "arm64-v8a", GuestArch: "arm64", - SystemImageDir: "system-images/android-34/google_apis/arm64-v8a", SystemImageName: "系统镜像 Android 14 Google APIs arm64-v8a", + GuestABI: "arm64-v8a", GuestArch: "arm64", TargetAPI: "android-35", + SystemImageDir: "system-images/android-35/google_apis/arm64-v8a", SystemImageName: "系统镜像 Android 15 Google APIs arm64-v8a", } case runtime.GOOS == "linux" && runtime.GOARCH == "amd64": return Platform{ Emulator: "emulator", ADB: "adb", QEMU: "qemu-system-x86_64", QEMUHeadless: "qemu-system-x86_64", PackageSuffix: "linux-x64", MirrorOS: "linux", MirrorArch: "x64", - GuestABI: "x86_64", GuestArch: "x86_64", + GuestABI: "x86_64", GuestArch: "x86_64", TargetAPI: "android-34", SystemImageDir: "system-images/android-34/google_apis/x86_64", SystemImageName: SystemImageName, } case runtime.GOOS == "windows" && runtime.GOARCH == "amd64": return Platform{ Emulator: "emulator.exe", ADB: "adb.exe", QEMU: "qemu-system-x86_64.exe", QEMUHeadless: "qemu-system-x86_64-headless.exe", QemuDir: "windows-x86_64", PackageSuffix: "win-x64", MirrorOS: "win", MirrorArch: "x64", - GuestABI: "x86_64", GuestArch: "x86_64", SystemImageDir: SystemImageDir, SystemImageName: SystemImageName, + GuestABI: "x86_64", GuestArch: "x86_64", TargetAPI: "android-34", SystemImageDir: SystemImageDir, SystemImageName: SystemImageName, } default: // An unlisted host must not inherit Windows names or MirrorChyan ids. diff --git a/internal/root/platform_test.go b/internal/root/platform_test.go index a5c18d7..91d73ed 100644 --- a/internal/root/platform_test.go +++ b/internal/root/platform_test.go @@ -9,17 +9,17 @@ func TestCurrentPlatformMatchesHost(t *testing.T) { p := CurrentPlatform() switch { case runtime.GOOS == "windows" && runtime.GOARCH == "amd64": - want := Platform{Emulator: "emulator.exe", ADB: "adb.exe", QEMU: "qemu-system-x86_64.exe", QEMUHeadless: "qemu-system-x86_64-headless.exe", QemuDir: "windows-x86_64", PackageSuffix: "win-x64", MirrorOS: "win", MirrorArch: "x64", GuestABI: "x86_64", GuestArch: "x86_64", SystemImageDir: SystemImageDir, SystemImageName: SystemImageName} + want := Platform{Emulator: "emulator.exe", ADB: "adb.exe", QEMU: "qemu-system-x86_64.exe", QEMUHeadless: "qemu-system-x86_64-headless.exe", QemuDir: "windows-x86_64", PackageSuffix: "win-x64", MirrorOS: "win", MirrorArch: "x64", GuestABI: "x86_64", GuestArch: "x86_64", TargetAPI: "android-34", SystemImageDir: SystemImageDir, SystemImageName: SystemImageName} if p != want { t.Fatalf("Windows platform: %+v", p) } case runtime.GOOS == "darwin" && runtime.GOARCH == "arm64": - want := Platform{Emulator: "emulator", ADB: "adb", QEMU: "qemu-system-aarch64", QEMUHeadless: "qemu-system-aarch64-headless", QemuDir: "darwin-aarch64", PackageSuffix: "macos-arm64", MirrorOS: "macos", MirrorArch: "arm64", GuestABI: "arm64-v8a", GuestArch: "arm64", SystemImageDir: "system-images/android-34/google_apis/arm64-v8a", SystemImageName: "系统镜像 Android 14 Google APIs arm64-v8a"} + want := Platform{Emulator: "emulator", ADB: "adb", QEMU: "qemu-system-aarch64", QEMUHeadless: "qemu-system-aarch64-headless", QemuDir: "darwin-aarch64", PackageSuffix: "macos-arm64", MirrorOS: "macos", MirrorArch: "arm64", GuestABI: "arm64-v8a", GuestArch: "arm64", TargetAPI: "android-35", SystemImageDir: "system-images/android-35/google_apis/arm64-v8a", SystemImageName: "系统镜像 Android 15 Google APIs arm64-v8a"} if p != want { t.Fatalf("darwin platform: %+v", p) } case runtime.GOOS == "linux" && runtime.GOARCH == "amd64": - want := Platform{Emulator: "emulator", ADB: "adb", QEMU: "qemu-system-x86_64", QEMUHeadless: "qemu-system-x86_64", PackageSuffix: "linux-x64", MirrorOS: "linux", MirrorArch: "x64", GuestABI: "x86_64", GuestArch: "x86_64", SystemImageDir: "system-images/android-34/google_apis/x86_64", SystemImageName: SystemImageName} + want := Platform{Emulator: "emulator", ADB: "adb", QEMU: "qemu-system-x86_64", QEMUHeadless: "qemu-system-x86_64", PackageSuffix: "linux-x64", MirrorOS: "linux", MirrorArch: "x64", GuestABI: "x86_64", GuestArch: "x86_64", TargetAPI: "android-34", SystemImageDir: "system-images/android-34/google_apis/x86_64", SystemImageName: SystemImageName} if p != want { t.Fatalf("linux platform: %+v", p) } From 4531acb86f0f9f4ae611e5e04a08a9116de1f29b Mon Sep 17 00:00:00 2001 From: DeathofBrain <109609173+DeathofBrain@users.noreply.github.com> Date: Fri, 6 Nov 2026 18:32:06 +0800 Subject: [PATCH 5/6] fix: use named macOS system image constant --- internal/root/platform.go | 2 +- internal/root/root.go | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/internal/root/platform.go b/internal/root/platform.go index 28bc557..3196e18 100644 --- a/internal/root/platform.go +++ b/internal/root/platform.go @@ -50,7 +50,7 @@ func CurrentPlatform() Platform { Emulator: "emulator", ADB: "adb", QEMU: "qemu-system-aarch64", QEMUHeadless: "qemu-system-aarch64-headless", QemuDir: "darwin-aarch64", PackageSuffix: "macos-arm64", MirrorOS: "macos", MirrorArch: "arm64", GuestABI: "arm64-v8a", GuestArch: "arm64", TargetAPI: "android-35", - SystemImageDir: "system-images/android-35/google_apis/arm64-v8a", SystemImageName: "系统镜像 Android 15 Google APIs arm64-v8a", + SystemImageDir: "system-images/android-35/google_apis/arm64-v8a", SystemImageName: MacOSSystemImageName, } case runtime.GOOS == "linux" && runtime.GOARCH == "amd64": return Platform{ diff --git a/internal/root/root.go b/internal/root/root.go index 56195f8..d3706bb 100644 --- a/internal/root/root.go +++ b/internal/root/root.go @@ -765,13 +765,14 @@ type ComponentStatus struct { // Component names and fixed data (AUTO-MAS constants.py). // SystemImageName retains the Windows name; other hosts use Platform.SystemImageName. const ( - PlatformToolsName = "平台工具(adb)" - EmulatorName = "Android 模拟器(emulator)" - SystemImageName = "系统镜像 Android 14 Google APIs x86_64" - LauncherName = "轻量桌面 µLauncher" - LauncherVersion = "0.2.12" - LauncherAPKSize = 4099556 - ImeName = "宿主输入法 org.modavd.ime" + PlatformToolsName = "平台工具(adb)" + EmulatorName = "Android 模拟器(emulator)" + SystemImageName = "系统镜像 Android 14 Google APIs x86_64" + MacOSSystemImageName = "系统镜像 Android 15 Google APIs arm64-v8a" + LauncherName = "轻量桌面 µLauncher" + LauncherVersion = "0.2.12" + LauncherAPKSize = 4099556 + ImeName = "宿主输入法 org.modavd.ime" ) // Advice for missing components (AUTO-MAS FETCH_COMPONENTS_ADVICE / PACKAGE_ADVICE; "回来点「检查」" is AUTO-MAS's From 03bdb03e893d8e061e43df633a9850683f806bff Mon Sep 17 00:00:00 2001 From: DeathofBrain <109609173+DeathofBrain@users.noreply.github.com> Date: Fri, 6 Nov 2026 18:40:09 +0800 Subject: [PATCH 6/6] fix: reuse named system image directories across platforms --- internal/root/platform.go | 9 ++++++--- internal/root/root.go | 23 ++++++++++++----------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/internal/root/platform.go b/internal/root/platform.go index 3196e18..5467eee 100644 --- a/internal/root/platform.go +++ b/internal/root/platform.go @@ -1,6 +1,9 @@ package root -import "runtime" +import ( + "runtime" + "strings" +) func platformKeyFiles() map[string][]string { p := CurrentPlatform() @@ -50,14 +53,14 @@ func CurrentPlatform() Platform { Emulator: "emulator", ADB: "adb", QEMU: "qemu-system-aarch64", QEMUHeadless: "qemu-system-aarch64-headless", QemuDir: "darwin-aarch64", PackageSuffix: "macos-arm64", MirrorOS: "macos", MirrorArch: "arm64", GuestABI: "arm64-v8a", GuestArch: "arm64", TargetAPI: "android-35", - SystemImageDir: "system-images/android-35/google_apis/arm64-v8a", SystemImageName: MacOSSystemImageName, + SystemImageDir: MacOSSystemImageDir, SystemImageName: MacOSSystemImageName, } case runtime.GOOS == "linux" && runtime.GOARCH == "amd64": return Platform{ Emulator: "emulator", ADB: "adb", QEMU: "qemu-system-x86_64", QEMUHeadless: "qemu-system-x86_64", PackageSuffix: "linux-x64", MirrorOS: "linux", MirrorArch: "x64", GuestABI: "x86_64", GuestArch: "x86_64", TargetAPI: "android-34", - SystemImageDir: "system-images/android-34/google_apis/x86_64", SystemImageName: SystemImageName, + SystemImageDir: strings.ReplaceAll(SystemImageDir, `\`, "/"), SystemImageName: SystemImageName, } case runtime.GOOS == "windows" && runtime.GOARCH == "amd64": return Platform{ diff --git a/internal/root/root.go b/internal/root/root.go index d3706bb..ce74f54 100644 --- a/internal/root/root.go +++ b/internal/root/root.go @@ -215,17 +215,18 @@ func jsonOneLine(v *jsonord.Value) string { // SystemImageDir retains the Windows/AUTO-MAS layout; other hosts use Platform.SystemImageDir. const ( - SDKDir = "sdk" - AvdDir = "avd" - ComponentsDir = "components" - MetadataFile = "mas-avd.json" - LogsDir = "logs" - ShaderCacheDir = "shader-cache" - GuestPatchDir = "guest-patches" - ManifestFile = "modavd-manifest.json" - SystemImageDir = `system-images\android-34\google_apis\x86_64` - LauncherAPKName = "de.jrpie.android.launcher_57.apk" - ImeAPKName = "modavd-ime.apk" + SDKDir = "sdk" + AvdDir = "avd" + ComponentsDir = "components" + MetadataFile = "mas-avd.json" + LogsDir = "logs" + ShaderCacheDir = "shader-cache" + GuestPatchDir = "guest-patches" + ManifestFile = "modavd-manifest.json" + SystemImageDir = `system-images\android-34\google_apis\x86_64` + MacOSSystemImageDir = "system-images/android-35/google_apis/arm64-v8a" + LauncherAPKName = "de.jrpie.android.launcher_57.apk" + ImeAPKName = "modavd-ime.apk" ) // MinModBuild: emulators whose Pkg.BuildId is below mas- refuse to boot (AUTO-MAS MIN_MOD_AVD_BUILD).