Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 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
16190f1
macOS: enable owned ARM64 instance lifecycle
DeathofBrain Nov 6, 2026
09bd4a9
macOS: complete lifecycle lock probing and SDK emulator status
DeathofBrain Nov 6, 2026
bf26706
Merge latest main into macOS lifecycle support
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
4 changes: 2 additions & 2 deletions internal/components/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ func (f *fetcher) run(ctx context.Context) (res *Result, err error) {
}
if len(todo) == 0 {
if f.darwin() {
f.say("组件都已齐全,不需要下载。当前 macOS 版本暂未开放实例创建与启动。")
f.say("组件都已齐全,不需要下载。创建或启动前仍需通过 HVF、镜像与模拟器版本检查。")
} else {
f.say("组件都已齐全,不需要下载。完成,可以在 AUTO-MAS 里添加这个文件夹了。")
}
Expand Down Expand Up @@ -532,7 +532,7 @@ func (f *fetcher) run(ctx context.Context) (res *Result, err error) {
}
}
if f.darwin() {
f.say("组件补全完成。当前 macOS 版本暂未开放实例创建与启动。")
f.say("组件补全完成。创建或启动前仍需通过 HVF、镜像与模拟器版本检查。")
} else {
f.say("完成,可以在 AUTO-MAS 里添加这个文件夹了。")
}
Expand Down
21 changes: 19 additions & 2 deletions internal/emu/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func (m *Manager) Check(ctx context.Context, opt CheckOptions) (*CheckResult, er
items := []CheckItem{
pathItem(m.Root.Dir),
componentsItem(comps, m.missingAdvice),
emulatorItem(m.emulatorFacts()),
platformEmulatorItem(m.emulatorFacts()),
accel,
vulkan,
m.diskItemNow(),
Expand Down Expand Up @@ -107,7 +107,7 @@ func (m *Manager) precheckBeforeLaunch(ctx context.Context, memoryMB int) error
items := []CheckItem{
pathItem(m.Root.Dir),
componentsItem(comps, m.missingAdvice),
emulatorItem(m.emulatorFacts()),
platformEmulatorItem(m.emulatorFacts()),
m.accelerationItem(ctx, true),
memoryItemNow(memoryMB),
m.diskItemNow(),
Expand Down Expand Up @@ -300,6 +300,23 @@ func (m *Manager) emulatorFacts() emulatorFacts {
return f
}

// platformEmulatorItem keeps the Windows package contract unchanged. The macOS
// lifecycle also supports the official ARM64 SDK emulator verified with API35;
// other revisions require explicit validation before they can launch instances.
func platformEmulatorItem(f emulatorFacts) CheckItem {
if root.CurrentPlatform().MirrorOS == "macos" && f.Present && !f.SelfBuilt {
ok := root.OfficialEmulatorVersionSupported(f.Version)
item := CheckItem{ID: "emulator", Title: "模拟器版本", OK: checkOK(ok), Blocking: true,
Reason: fmt.Sprintf("官方 ARM64 Emulator %s", f.Version)}
if !ok {
item.Reason = "当前官方 Emulator 版本尚未验证:" + f.Version
item.Advice = "macOS 当前支持官方 ARM64 Emulator 37.2.13 与 API35 arm64-v8a 镜像"
}
return item
}
return emulatorItem(f)
}

// emulatorItem: only the self-built emulator of the ModAVD package (user decision: Google's builds are not
// supported), and its build must be new enough. Blocking.
func emulatorItem(f emulatorFacts) CheckItem {
Expand Down
6 changes: 3 additions & 3 deletions internal/emu/crud.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func (m *Manager) Create(ctx context.Context, req CreateRequest) (_ *InstanceInf
retErr = m.withCreateErrorState(index, retErr, false)
}
}()
if err := requireWindows(); err != nil {
if err := requireLifecycle(); err != nil {
return nil, err
}
if req.Resume {
Expand Down Expand Up @@ -197,7 +197,7 @@ func (m *Manager) resumeCreate(ctx context.Context, req CreateRequest) (*Instanc

// Update changes title / memory / cpu / fps / data partition size (next boot).
func (m *Manager) Update(ctx context.Context, i int, req UpdateRequest) (*UpdateResult, error) {
if err := requireWindows(); err != nil {
if err := requireLifecycle(); err != nil {
return nil, err
}
if _, err := m.instance(i); err != nil {
Expand Down Expand Up @@ -265,7 +265,7 @@ func (m *Manager) Update(ctx context.Context, i int, req UpdateRequest) (*Update
// without this the retry the error advises would end in "不存在". errs.NotFound only when none of mas_<i>.ini,
// mas_<i>.avd and instances.<i> is there.
func (m *Manager) Delete(ctx context.Context, i int) (*DeleteResult, error) {
if err := requireWindows(); err != nil {
if err := requireLifecycle(); err != nil {
return nil, err
}
in, err := m.instance(i)
Expand Down
3 changes: 3 additions & 0 deletions internal/emu/gates_other_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ func gateTree(t *testing.T, dir string) []string {
}

func TestUnsupportedOperationsLeaveRootUntouched(t *testing.T) {
if adb.OwnershipSupported && root.CurrentPlatform().MirrorOS == "macos" {
t.Skip("native instance lifecycle is implemented on this host")
}
dir := testutil.TempDir(t)
r, err := root.New(dir)
if err != nil {
Expand Down
19 changes: 14 additions & 5 deletions internal/emu/launch.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const adbServerStartTimeout = 20 * time.Second
// - -audio none and -feature -VirtioSndCard: no host sound and no guest sound card (the image's virtio-snd ignores
// hw.audio*); qemu CPU on the 1999 main screen 147% -> 104%;
// - -memory: the instance's own setting (avd.MemoryFor), hw.ramSize is left alone;
// - the balloon (free page reporting) is always on; QuickbootFileBacked has to be off for it, or the guest RAM is
// - the Windows balloon (free page reporting) is on; QuickbootFileBacked has to be off for it, or the guest RAM is
// a file mapping whose pages qemu cannot drop;
// - VulkanBatchedDescriptorSetUpdate: without it Endfield's render thread spent 67.5% in synchronous
// vkAllocateDescriptorSets round trips;
Expand Down Expand Up @@ -59,7 +59,7 @@ func buildLaunchArgs(avdName string, console, memoryMB int, headless bool) []str
// Silicon. -gpu host alone selects the older host GLES translation path.
args = append(args, "-feature", "GuestAngle,Vulkan")
}
return append(args,
args = append(args,
"-feature", "-QuickbootFileBacked",
"-feature", "-VirtioSndCard",
"-feature", "VulkanBatchedDescriptorSetUpdate",
Expand All @@ -69,8 +69,13 @@ func buildLaunchArgs(avdName string, console, memoryMB int, headless bool) []str
"-qemu",
"-global", "virtio-blk-device.write-cache=off",
"-global", "virtio-blk-device.config-wce=off",
"-device", "virtio-balloon-pci,free-page-reporting=on",
)
// Google's ARM64 QEMU does not implement ModAVD's custom balloon property.
// Keep the Windows argv byte-for-byte; native macOS has no reporting balloon.
if root.CurrentPlatform().MirrorOS != "macos" {
args = append(args, "-device", "virtio-balloon-pci,free-page-reporting=on")
}
return args
}

// launchPlan is what prepareLaunch settled before emulator.exe is started.
Expand Down Expand Up @@ -166,8 +171,12 @@ func (m *Manager) launchPlanFor(ctx context.Context, inst *avd.Instance, memoryM
if n, ok := inst.CPU(); ok {
cpu = strconv.Itoa(n)
}
m.Log.Infof("启动魔改 AVD 实例 %d(%s,%dp %d Hz,%d MB / %s 核,气球开,不带声卡,ANDROID_EMU_VK_HIDE_REBAR=%s): emulator %s",
i, mode, inst.Resolution(), inst.FPS(), memoryMB, cpu, hideRebar, strings.Join(args, " "))
balloon := "气球开,"
if root.CurrentPlatform().MirrorOS == "macos" {
balloon = ""
}
m.Log.Infof("启动魔改 AVD 实例 %d(%s,%dp %d Hz,%d MB / %s 核,%s不带声卡,ANDROID_EMU_VK_HIDE_REBAR=%s): emulator %s",
i, mode, inst.Resolution(), inst.FPS(), memoryMB, cpu, balloon, hideRebar, strings.Join(args, " "))
return &launchPlan{args: args, logPath: logPath, memoryMB: memoryMB}, nil
}

Expand Down
12 changes: 12 additions & 0 deletions internal/emu/platform.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package emu

import (
"modavd/internal/adb"
"modavd/internal/errs"
"modavd/internal/root"
)
Expand All @@ -14,3 +15,14 @@ func requireWindows() error {
}
return nil
}

// requireLifecycle only opens shared instance operations when the host can prove
// QEMU ownership, lock files and query ports. In particular a no-cgo Darwin build
// must fail before creating instance metadata or partial initialization state.
func requireLifecycle() error {
p := root.CurrentPlatform()
if p.Windows() || (p.MirrorOS == "macos" && adb.OwnershipSupported) {
return nil
}
return errs.New(errs.Unsupported, "当前平台暂不支持实例管理")
}
2 changes: 1 addition & 1 deletion internal/emu/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ var startFindQemu = (*Manager).findQemu
// the guest reboots on its own (qemu stays), the watch runs this start again, and so does any start that finds the
// recorded boot_id changed. The per-boot steps then run again and the Vulkan fix is owed as above.
func (m *Manager) Start(ctx context.Context, i int, opt StartOptions) (*StartResult, error) {
if err := requireWindows(); err != nil {
if err := requireLifecycle(); err != nil {
return nil, err
}
t0 := time.Now()
Expand Down
6 changes: 3 additions & 3 deletions internal/emu/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func (m *Manager) qemuOf(i int) (host.Process, bool, error) {
// the process scan, creation metadata / locks need no adb: List would start the
// private server and ask each instance, up to 5 s out of a 30 s shutdown budget.
func (m *Manager) StopTargets(ctx context.Context) ([]int, error) {
if err := requireWindows(); err != nil {
if err := requireLifecycle(); err != nil {
return nil, err
}
if err := ctxErr(ctx); err != nil {
Expand Down Expand Up @@ -84,7 +84,7 @@ func (m *Manager) StopTargets(ctx context.Context) ([]int, error) {

// List is every instance with its status (one process scan, getprop only for running ones).
func (m *Manager) List(ctx context.Context) (*ListResult, error) {
if err := requireWindows(); err != nil {
if err := requireLifecycle(); err != nil {
return nil, err
}
insts, err := avd.List(m.Root)
Expand Down Expand Up @@ -120,7 +120,7 @@ func (m *Manager) List(ctx context.Context) (*ListResult, error) {

// Info is one instance. errs.NotFound when it does not exist.
func (m *Manager) Info(ctx context.Context, i int) (*InstanceInfo, error) {
if err := requireWindows(); err != nil {
if err := requireLifecycle(); err != nil {
return nil, err
}
in, err := m.instance(i)
Expand Down
2 changes: 1 addition & 1 deletion internal/emu/stop.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ var stopOpsOf = (*Manager).stopOpsFor
// end qemu (see qemuNotEnded). An external stop of a creating instance is errs.Busy unless CancelCreate is explicit;
// Force only selects the shutdown method. Internal setup shutdowns and bridge restarts use creating instead.
func (m *Manager) Stop(ctx context.Context, i int, opt StopOptions) (*StopResult, error) {
if err := requireWindows(); err != nil {
if err := requireLifecycle(); err != nil {
return nil, err
}
in, err := m.instance(i)
Expand Down
7 changes: 7 additions & 0 deletions internal/host/lock_probe_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//go:build darwin && cgo

package host

import "modavd/internal/macos"

func ExclusiveLockHeld(path string) (bool, error) { return macos.ExclusiveLockHeld(path) }
5 changes: 5 additions & 0 deletions internal/host/lock_probe_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
//go:build !windows && (!darwin || !cgo)

package host

func ExclusiveLockHeld(string) (bool, error) { return false, errUnsupported }
3 changes: 0 additions & 3 deletions internal/host/unsupported_other.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,4 @@ func OpenStableRead(string) (*os.File, error) { return nil, errUnsupported }
// FileChangeStamp disables caching without a supported file identity API.
func FileChangeStamp(*os.File) string { return "" }

// ExclusiveLockHeld requires the Windows lifecycle lock probe.
func ExclusiveLockHeld(string) (bool, error) { return false, errUnsupported }

func InfoBox(string) error { return errors.New("当前平台暂不支持消息框") }
29 changes: 29 additions & 0 deletions internal/macos/lock_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,32 @@ func (l *FileLock) Unlock() {
_ = l.f.Close() // closing also releases the lock when explicit unlocking fails
l.f = nil
}

// ExclusiveLockHeld is a read-only shared-lock probe. Missing files are free;
// never create a file or directory just to inspect lifecycle state.
func ExclusiveLockHeld(path string) (bool, error) {
fd, err := syscall.Open(path, syscall.O_RDONLY|syscall.O_CLOEXEC|syscall.O_NOFOLLOW, 0)
if errors.Is(err, os.ErrNotExist) {
return false, nil
}
if err != nil {
return false, err
}
f := os.NewFile(uintptr(fd), path)
defer f.Close()
st, err := f.Stat()
if err != nil {
return false, err
}
if !st.Mode().IsRegular() {
return false, fmt.Errorf("锁文件不是普通文件")
}
err = syscall.Flock(fd, syscall.LOCK_SH|syscall.LOCK_NB)
if err == nil {
return false, syscall.Flock(fd, syscall.LOCK_UN)
}
if errors.Is(err, syscall.EWOULDBLOCK) || errors.Is(err, syscall.EAGAIN) {
return true, nil
}
return false, err
}
8 changes: 8 additions & 0 deletions internal/root/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,8 @@ func (r *Root) Components() []ComponentStatus {
emulator.Build = r.VersionText()
b, ok := r.ModBuild()
emulator.Installed = ok && b >= MinModBuild
} else if r.EmulatorPresent() && OfficialEmulatorVersionSupported(emulator.Version) {
emulator.Installed = true
}
launcher := ComponentStatus{ID: "launcher", Name: LauncherName, Version: LauncherVersion, Optional: true}
if fi, err := os.Stat(r.LauncherAPK()); err == nil && fi.Size() == LauncherAPKSize {
Expand Down Expand Up @@ -893,6 +895,12 @@ func (r *Root) EmulatorPresent() bool {
return allFiles(filepath.Join(r.RuntimeSDK(), "emulator"), keyFiles["emulator"])
}

// OfficialEmulatorVersionSupported is the verified macOS SDK fallback until a
// native ModAVD package is distributed. Windows keeps requiring the ModAVD build.
func OfficialEmulatorVersionSupported(version string) bool {
return CurrentPlatform().MirrorOS == "macos" && (version == "37.2.13" || version == "37.2.13.0")
}

// EmulatorSelfBuilt: present and qemu-system-x86_64-headless.exe contains SelfBuiltMark.
func (r *Root) EmulatorSelfBuilt() bool {
return r.EmulatorPresent() && HasSelfBuiltMark(r.QemuHeadlessExe())
Expand Down
Loading