Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 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
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
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
702f23b
Merge main into macOS preflight checks
DeathofBrain Nov 6, 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
53 changes: 43 additions & 10 deletions internal/emu/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -59,7 +60,7 @@ func (m *Manager) Check(ctx context.Context, opt CheckOptions) (*CheckResult, er
var accel, vulkan CheckItem
var wg sync.WaitGroup
wg.Go(func() { accel = m.accelerationItem(ctx, !opt.Refresh) })
wg.Go(func() { vulkan = vulkanItemFromProbe(m.probeVulkan(ctx, !opt.Refresh)) })
wg.Go(func() { vulkan = m.graphicsItem(ctx, !opt.Refresh) })
wg.Wait()
if err := ctxErr(ctx); err != nil {
return nil, err
Expand All @@ -74,6 +75,7 @@ func (m *Manager) Check(ctx context.Context, opt CheckOptions) (*CheckResult, er
m.diskItemNow(),
memoryItemNow(memoryMB),
}
items = append(items, m.platformCheckItems()...)
if it, ok := portsItem(m.Root.Ports(), m.Root.CheckPorts()); ok {
items = append(items[:1], append([]CheckItem{it}, items[1:]...)...)
}
Expand Down Expand Up @@ -110,6 +112,7 @@ func (m *Manager) precheckBeforeLaunch(ctx context.Context, memoryMB int) error
memoryItemNow(memoryMB),
m.diskItemNow(),
}
items = append(items, m.platformCheckItems()...)
if err := ctxErr(ctx); err != nil {
return err
}
Expand All @@ -124,7 +127,7 @@ func (m *Manager) precheckBeforeLaunch(ctx context.Context, memoryMB int) error
return nil
})
}
vulkan := vulkanItemFromProbe(m.probeVulkan(ctx, true))
vulkan := m.graphicsItem(ctx, true)
if err := ctxErr(ctx); err != nil {
return err
}
Expand All @@ -148,10 +151,12 @@ func itemMessage(it CheckItem) string {
return it.Reason
}

// firstBlockingFailure is the first item with blocking && ok == false.
// firstBlockingFailure is the first blocking failure. On macOS an unknown
// blocking result also prevents readiness; a failed host query is no approval
// to boot. The existing Windows treatment of unknown results is preserved.
func firstBlockingFailure(items []CheckItem) (CheckItem, bool) {
for _, it := range items {
if it.Blocking && it.OK != nil && !*it.OK {
if it.Blocking && ((it.OK != nil && !*it.OK) || (runtime.GOOS == "darwin" && it.OK == nil)) {
return it, true
}
}
Expand Down Expand Up @@ -302,7 +307,7 @@ func emulatorItem(f emulatorFacts) CheckItem {
if !f.Present {
return CheckItem{
ID: "emulator", Title: title, OK: checkOK(false), Blocking: true,
Reason: "这个目录里还没有魔改 AVD 内测包", Advice: testPackageAdvice,
Reason: "这个目录里还没有魔改 AVD 内测包", Advice: platformPackageAdvice(),
}
}
version := f.Version
Expand All @@ -313,7 +318,7 @@ func emulatorItem(f emulatorFacts) CheckItem {
return CheckItem{
ID: "emulator", Title: title, OK: checkOK(false), Blocking: true,
Reason: fmt.Sprintf("这里的模拟器(%s)不是魔改 AVD 内测包里的版本,只支持内测包", version),
Advice: testPackageAdvice,
Advice: platformPackageAdvice(),
}
}
if f.HasBuild && f.Build >= root.MinModBuild {
Expand Down Expand Up @@ -435,17 +440,26 @@ func accelItem(res accelResult, summary string) CheckItem {
if summary == "" {
summary = "无输出"
}
backend, advice := "Windows 虚拟机监控程序平台", accelAdvice
if runtime.GOOS == "darwin" {
backend = "Hypervisor.framework"
advice = "请在 Apple Silicon Mac 本机运行 macos-arm64 版魔改 AVD,确认模拟器带 com.apple.security.hypervisor 签名权限;用 emulator -accel-check 检查 HVF"
}
return CheckItem{
ID: "acceleration", Title: "硬件虚拟化", OK: checkOK(false), Blocking: true,
Reason: fmt.Sprintf("硬件虚拟化(Windows 虚拟机监控程序平台)不可用,魔改 AVD 无法启动(检查结果:%s)", summary),
Advice: accelAdvice,
Reason: fmt.Sprintf("硬件虚拟化(%s)不可用,魔改 AVD 无法启动(检查结果:%s)", backend, summary),
Advice: advice,
}
}

// checkAcceleration is `emulator.exe -accel-check` (AUTO-MAS check_acceleration), cached per root for 120 s: it runs
// before every boot, but several boots within seconds need not start it again.
func (m *Manager) checkAcceleration(ctx context.Context, useCache bool) accelResult {
key := strings.ToLower(m.Root.Dir)
if runtime.GOOS == "darwin" {
// APFS can be case sensitive; different roots must not share a result.
key = m.Root.Dir
}
if useCache {
accelCache.Lock()
e, ok := accelCache.m[key]
Expand Down Expand Up @@ -487,10 +501,17 @@ func (m *Manager) checkAcceleration(ctx context.Context, useCache bool) accelRes
return res
}

// parseAccelCheck: usable only with exit code 0 and "usable" in the output (AUTO-MAS parse_accel_check).
// ProbeHVF prints an OS version rather than "usable". Accept its known success
// line only alongside a successful process exit; keep the Windows parser intact.
var hvfNameVersion = regexp.MustCompile(`(?m)^Hypervisor\.Framework OS X Version ([0-9]+\.[0-9]+(?:\.[0-9]+)?)[\t ]*\r?$`)

func parseAccelCheck(code int, output string) accelResult {
text := strings.TrimSpace(output)
return accelResult{OK: code == 0 && strings.Contains(strings.ToLower(text), "usable"), Detail: text}
ready := strings.Contains(strings.ToLower(text), "usable")
if runtime.GOOS == "darwin" {
ready = root.CurrentPlatform().GuestABI == "arm64-v8a" && hvfNameVersion.MatchString(text)
}
return accelResult{OK: code == 0 && ready, Detail: text}
}

// accelSummary drops the frame lines of the -accel-check output ("accel:", the return code, "accel") and keeps the
Expand Down Expand Up @@ -527,8 +548,20 @@ var accelNameVersion = regexp.MustCompile(`(\w+)\s*\(([\d.]+)\)`)
// accelOKText: "WHPX(10.0.26200) is installed and usable." -> "可用(WHPX 10.0.26200)"; just "可用" when the name and
// version cannot be found.
func accelOKText(summary string) string {
if runtime.GOOS == "darwin" {
if mm := hvfNameVersion.FindStringSubmatch(summary); mm != nil {
return fmt.Sprintf("可用(HVF;macOS %s)", mm[1])
}
}
if mm := accelNameVersion.FindStringSubmatch(summary); mm != nil {
return fmt.Sprintf("可用(%s %s)", mm[1], mm[2])
}
return "可用"
}

func platformPackageAdvice() string {
if runtime.GOOS == "darwin" {
return "请把魔改 AVD 的 macos-arm64 内测包解压到这个目录(解压后应有 sdk/emulator)"
}
return testPackageAdvice
}
71 changes: 71 additions & 0 deletions internal/emu/check_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//go:build darwin

package emu

import (
"context"
"fmt"
"path/filepath"
"strings"

"modavd/internal/host"
"modavd/internal/root"
)

// platformCheckItems validates image metadata as well as the platform row.
// A directory named arm64-v8a must not silently make an x86 image compatible.
func (m *Manager) platformCheckItems() []CheckItem {
it := CheckItem{ID: "guest-architecture", Title: "客体架构", OK: checkOK(false), Blocking: true,
Advice: "请使用 macos-arm64 版魔改 AVD 和对应的 Apple Silicon 系统镜像"}
p := root.CurrentPlatform()
if !p.Supported() || p.GuestABI != "arm64-v8a" || p.GuestArch != "arm64" {
it.Reason = "当前 macOS 管理器仅支持 Apple Silicon 和 arm64-v8a 客体"
return []CheckItem{it}
}
it.Advice = fmt.Sprintf("请补齐%s;不要使用其他 Android 版本或 x86_64 镜像", p.SystemImageName)
props := filepath.Join(m.Root.RuntimeSDK(), p.SystemImageDir, "source.properties")
data, err := root.ReadFileShared(props)
if err != nil {
it.OK = nil
it.Reason = fmt.Sprintf("查不了系统镜像架构(%s): %v", props, err)
return []CheckItem{it}
}
_, values := root.ReadKeyValues(data, false)
abi := values["SystemImage.Abi"]
if abi != p.GuestABI {
if abi == "" {
abi = "未声明"
}
it.Reason = fmt.Sprintf("系统镜像架构为 %s,Apple Silicon 需要 %s", abi, p.GuestABI)
return []CheckItem{it}
}
level := values["AndroidVersion.ApiLevel"]
if level != strings.TrimPrefix(p.TargetAPI, "android-") {
if level == "" {
level = "未声明"
}
it.Reason = fmt.Sprintf("系统镜像 API 为 %s,当前平台需要 %s", level, p.TargetAPI)
return []CheckItem{it}
}
it.OK, it.Reason, it.Advice = checkOK(true), "Apple Silicon / arm64-v8a / "+p.TargetAPI, ""
return []CheckItem{it}
}

func (m *Manager) graphicsItem(ctx context.Context, _ bool) CheckItem {
// system_profiler reports host Metal support, not the emulator's Vulkan
// loader or bundled graphics stack. Keep this diagnostic non-blocking.
devices, err := host.MetalDevices(ctx)
it := CheckItem{ID: "vulkan", Title: "图形加速"}
if err != nil {
it.Reason = "查不了宿主 Metal 图形支持: " + err.Error()
return it
}
it.OK = checkOK(len(devices) > 0)
if len(devices) == 0 {
it.Reason = "macOS 没有报告支持 Metal 的显卡"
it.Advice = "请在 Apple Silicon Mac 本机运行并检查 macOS 更新;模拟器图形能力仍需实机验证"
return it
}
it.Reason = "宿主 Metal 显卡:" + strings.Join(devices, "、") + ";模拟器 Vulkan 和渲染能力仍需实机验证"
return it
}
11 changes: 11 additions & 0 deletions internal/emu/check_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//go:build !darwin

package emu

import "context"

func (m *Manager) platformCheckItems() []CheckItem { return nil }

func (m *Manager) graphicsItem(ctx context.Context, useCache bool) CheckItem {
return vulkanItemFromProbe(m.probeVulkan(ctx, useCache))
}
5 changes: 5 additions & 0 deletions internal/host/darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package host

import (
"context"
"os"
"sync/atomic"
"time"
Expand Down Expand Up @@ -53,3 +54,7 @@ func Lock(path string, timeout time.Duration) (*FileLock, bool, error) {
func Listeners() ([]Listener, error) { return macos.Listeners() }
func ListenerPID(port int) (uint32, bool, error) { return macos.ListenerPID(port) }
func BusyPorts(ports []int) ([]int, error) { return macos.BusyPorts(ports) }

func DiskFree(path string) (uint64, error) { return macos.DiskFree(path) }
func AvailableMemoryMB() (uint64, error) { return macos.AvailableMemoryMB() }
func MetalDevices(ctx context.Context) ([]string, error) { return macos.MetalDevices(ctx) }
5 changes: 5 additions & 0 deletions internal/host/other.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package host

import (
"context"
"os"
"time"
)
Expand Down Expand Up @@ -98,3 +99,7 @@ func ListenerPID(int) (uint32, bool, error) { return 0, false, errUnsupported }

// BusyPorts compares requested ports with the Windows TCP and UDP tables.
func BusyPorts([]int) ([]int, error) { return nil, errUnsupported }

func DiskFree(string) (uint64, error) { return 0, errUnsupported }
func AvailableMemoryMB() (uint64, error) { return 0, errUnsupported }
func MetalDevices(context.Context) ([]string, error) { return nil, errUnsupported }
6 changes: 0 additions & 6 deletions internal/host/unsupported_other.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,6 @@ func L3Groups() []L3Group { return nil }
// SharedMemoryExists calls OpenFileMappingW for the Windows named mapping.
func SharedMemoryExists(string) bool { return false }

// DiskFree calls GetDiskFreeSpaceExW for the Windows volume.
func DiskFree(string) (uint64, error) { return 0, errUnsupported }

// GetCompressedFileSize calls GetCompressedFileSizeW for the Windows file.
func GetCompressedFileSize(string) (uint64, error) { return 0, errUnsupported }

Expand All @@ -50,9 +47,6 @@ func VolumeFileSystem(string) (string, error) { return "", errUnsupported }
// MoveFileReplaceThrough calls MoveFileExW (REPLACE_EXISTING | WRITE_THROUGH).
func MoveFileReplaceThrough(string, string) error { return errUnsupported }

// AvailableMemoryMB calls GlobalMemoryStatusEx on Windows.
func AvailableMemoryMB() (uint64, error) { return 0, errUnsupported }

// Protect calls CryptProtectData (DPAPI) for the current Windows user.
func Protect([]byte, []byte) ([]byte, error) { return nil, errUnsupported }

Expand Down
66 changes: 66 additions & 0 deletions internal/macos/graphics_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//go:build darwin && cgo

package macos

import (
"context"
"encoding/json"
"fmt"
"os/exec"
"slices"
"strings"
"time"
)

// MetalDevices lists the GPUs system_profiler reports as supporting Metal.
// This host diagnostic does not establish the emulator's Vulkan capabilities.
func MetalDevices(ctx context.Context) ([]string, error) {
cctx, cancel := context.WithTimeout(ctx, 20*time.Second)
defer cancel()
data, err := exec.CommandContext(cctx, "/usr/sbin/system_profiler", "SPDisplaysDataType", "-json").Output()
if err != nil {
if cctx.Err() != nil {
err = cctx.Err()
}
return nil, fmt.Errorf("读取 macOS 图形信息: %w", err)
}
var output struct {
Displays []struct {
Name string `json:"_name"`
Model string `json:"sppci_model"`
Metal string `json:"spdisplays_metal"`
Family string `json:"spdisplays_mtlgpufamilysupport"`
} `json:"SPDisplaysDataType"`
}
if err := json.Unmarshal(data, &output); err != nil {
return nil, fmt.Errorf("解析 macOS 图形信息: %w", err)
}
if len(output.Displays) == 0 {
return nil, fmt.Errorf("macOS 没有返回显卡信息")
}
devices := []string{}
for _, gpu := range output.Displays {
if !metalSupported(gpu.Metal) && !metalSupported(gpu.Family) {
continue
}
name := gpu.Model
if name == "" {
name = gpu.Name
}
if name == "" {
name = "未命名显卡"
}
devices = append(devices, name)
}
slices.Sort(devices)
return slices.Compact(devices), nil
}

func metalSupported(value string) bool {
// Match known positive symbols; "unsupported" also contains "supported".
switch strings.ToLower(strings.TrimSpace(value)) {
case "supported", "spdisplays_supported", "spdisplays_metal1", "spdisplays_metal2", "spdisplays_metal3", "spdisplays_metal4":
return true
}
return false
}
Loading
Loading