Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 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
33afbf6
feat: protect macOS update settings with Keychain
DeathofBrain Nov 6, 2026
17ea66d
feat(update): add native macOS signed package transactions
DeathofBrain Nov 6, 2026
bd5103a
Merge main into macOS Keychain settings
DeathofBrain Nov 7, 2026
41f30a9
Merge branch 'codex/macos-10-keychain' into codex/macos-11-update
DeathofBrain Nov 7, 2026
0d03632
fix(update): align macOS package contract with browser manager
DeathofBrain Nov 7, 2026
d1ac7f8
Merge remote-tracking branch 'origin/main' into codex/macos-11-update
DeathofBrain Nov 10, 2026
d9e357f
Merge main and preserve native macOS update options
DeathofBrain Nov 10, 2026
df6d496
docs: rewrite macOS update package contract in Chinese
DeathofBrain Nov 10, 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
1 change: 1 addition & 0 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ Double-click `ModAVD.exe` in the package root: it starts a local server that lis
- Not printed with `--quiet` (progress is info level). Without `--json` the human-readable text is unchanged. stdout still carries only the final result line.
- "Update" (`更新`) in the manager's "Settings": the manager downloads, verifies and stages the new version (instances keep running meanwhile); if another program runs this root's modavd (for example `modavd mcp`) or an instance is being created, it stops nothing and asks you to deal with that first; it then shuts down every running instance normally, then starts `tools\modavd.exe update install --after-pid <manager pid> --reopen <manager executable> [--reopen-arg ARG]...` and exits. The CLI waits for the manager to exit, swaps the new version in, writes the result to `.update\manager-result.json` and starts the manager again with its original arguments; the new manager shows the result when it opens. If the swap cannot be done, the manager is started again all the same, and the downloaded version is kept so it does not have to be downloaded again. The CLI's output goes to `logs\modavd-update.log`. `--after-pid`, `--reopen` and `--reopen-arg` only go with `install` and are meant for the manager.
- On macOS, update settings and component download sources use `~/Library/Application Support/ModAVD/update-settings.json` and `components-settings.json`. The CDK is encrypted with AES-GCM using a random wrapping key kept in the current user's Keychain. A locked or unauthorized Keychain makes saving fail without plaintext storage; copying the settings to another user or device does not transfer that key.
- Native Apple Silicon builds accept `modavd update prepare --package /path/update.zip --sha256 <64-digit digest from a trusted independent source>`, followed by offline `modavd update install` or `recover`. Packages must follow the [macOS update contract](internal/update/MACOS.md); ARM64 Mach-O, code signature integrity, modes and symlinks are checked, with at least 20 GiB free space retained. Signature verification does not establish publisher identity. Native transactions are separate from the Windows/AUTO-MAS journal. Remote checks return `update_unavailable` when no compatible macOS release asset exists; release packaging is a later stage.
- Only files from the package are swapped: `avd\`, `mas-avd.json` (only `components.emulator` is changed and an `updateHistory` entry is recorded), `sdk\platform-tools`, `sdk\system-images`, `logs\` and so on are never touched; `sdk\emulator` is replaced as a whole. A root folder that sets `sdkRoot` in `mas-avd.json` is not updated.
- `--automas-version` is the version of the calling AUTO-MAS: if the first line of the new version's announcement requires a higher AUTO-MAS, it does not install.
- Exit codes: being unable to check (offline, rate-limited, a GitHub error) is 9 (`update_unavailable`), a downloaded package that fails verification is also 9 (`update_rejected`), and a program in use or an update already in progress is 5.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@ Apple Silicon macOS 可运行 `ModAVD` 管理器;`ModAVD.app` 放在包根目
- `--quiet` 时不出(进度是 info 级)。不带 `--json` 时照旧是人看的文字。stdout 仍然只有最后一行结果。
- 管理器「设置」里点「更新」:管理器先下载、核对、暂存好(这时实例照常用);有别的程序在跑这个根目录的 modavd(比如 `modavd mcp`)或有实例正在新建,就一台都不关、提示先处理;再正常关掉所有在运行的实例,然后启动 `tools\modavd.exe update install --after-pid <管理器 pid> --reopen <管理器程序> [--reopen-arg 参数]...` 并退出。命令行等管理器退出、换上新版,把结果写进 `.update\manager-result.json`,再按原来的参数重新打开管理器,新管理器打开时提示更新结果。换不上也照样重新打开,下载好的新版留着,下次不用重下。命令行的输出在 `logs\modavd-update.log`。`--after-pid`、`--reopen`、`--reopen-arg` 只和 `install` 一起用,是给管理器用的。
- macOS 的更新设置和组件下载源分别保存在 `~/Library/Application Support/ModAVD/update-settings.json` 和 `components-settings.json`。CDK 使用 AES-GCM 加密,随机包装密钥留在当前用户的 Keychain;钥匙串锁定或未授权时保存失败,不会降级为明文存储。复制设置文件到其他用户或设备不会同时转移密钥。
- Apple Silicon 原生构建可用 `modavd update prepare --package /path/update.zip --sha256 <可信渠道取得的64位摘要>` 暂存本地包,再用 `modavd update install` 离线安装;`recover` 也可离线补完或恢复。包须符合 [macOS 更新包协议](internal/update/MACOS.md),核对 ARM64 Mach-O、代码签名完整性、模式和符号链接,并保留至少 20 GiB 可用空间。签名核对不代表发布者身份可信,SHA256 必须独立取得。原生事务与 Windows/AUTO-MAS 状态隔离;尚无符合协议的 macOS 发布资产时,远端检查返回 `update_unavailable`。发布打包流程由后续阶段接入。
- 只换包里的文件:`avd\`、`mas-avd.json`(只改 `components.emulator` 并记一条 `updateHistory`)、`sdk\platform-tools`、`sdk\system-images`、`logs\` 等一律不碰;`sdk\emulator` 整个换掉。根目录在 `mas-avd.json` 里设了 `sdkRoot` 的不更新。
- `--automas-version` 是调用方 AUTO-MAS 的版本:新版的公告第一行要求更高的 AUTO-MAS 时不装。
- 退出码:查不了(断网、限流、GitHub 出错)是 9(`update_unavailable`),下载的包没通过核对也是 9(`update_rejected`),有程序在用、正在更新是 5。
Expand Down
14 changes: 11 additions & 3 deletions internal/cli/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ func init() {
run: runVersion, human: humanOf(formatVersion),
},
{
name: "update", args: "[status|check|prepare|install|recover] [--source github|mirrorchyan] [--cdk-dpapi BASE64] [--automas-version V] [--after-pid PID --reopen 程序 [--reopen-arg 参数]...]",
name: "update", args: "[status|check|prepare|install|recover] [--source github|mirrorchyan] [--cdk-dpapi BASE64] [--automas-version V] [--package ZIP --sha256 HEX] [--after-pid PID --reopen 程序 [--reopen-arg 参数]...]",
summary: "更新这个根目录的 ModAVD 包(模拟器、组件、脚本和管理器)",
details: "按管理器「设置」里选的更新源(GitHub 或 Mirror 酱,Mirror 酱下载要填 CDK);" +
"调用方有自己的设置时用 --source 和 --cdk-dpapi 传这一次的,只盖过「设置」、不保存,status 里的 source、cdk_set 是这次生效的。" +
Expand All @@ -368,12 +368,16 @@ func init() {
"--automas-version 是调用方 AUTO-MAS 的版本,新版要求更高的 AUTO-MAS 时不装。" +
"--after-pid、--reopen、--reopen-arg 是管理器「设置」里点「更新」时用的:管理器下载核对好、关掉实例后启动这条命令并退出," +
"命令等它退出再换上新版,然后重新打开管理器。" +
"macOS 可用 --package ZIP --sha256 HEX 准备或安装本地完整签名包;摘要需从可信来源独立取得。" +
"--json 时每个进度在 stderr 的日志行里多一个 progress 对象(checking、extracting、verifying、installing 开始时各一行,percent 为 -1;" +
"downloading 第一行就带真实进度,之后约 2 秒一行,下完那行一定有):" +
`{"stage":"checking|downloading|extracting|verifying|installing","percent":-1 或 0–100,"done_bytes":N,"total_bytes":N}。` +
"查不了是退出码 9(update_unavailable),下载没通过核对也是 9(update_rejected),有程序在用是 5。",
posNames: []string{"status、check、prepare、install 或 recover"}, maxPos: 1,
flags: []flagSpec{optUpdateSource, optCDKDPAPI, optAutoMAS, optAfterPID, optReopen, optReopenArg}, final: true,
flags: []flagSpec{optUpdateSource, optCDKDPAPI, optAutoMAS,
{name: "package", kind: kindString, arg: "ZIP", help: "macOS 本地完整更新包"},
{name: "sha256", kind: kindString, arg: "HEX", help: "本地更新包的独立可信 SHA-256"},
optAfterPID, optReopen, optReopenArg}, final: true,
run: runUpdate, human: humanOf(formatUpdateStatus),
},
{
Expand Down Expand Up @@ -1447,6 +1451,9 @@ func runUpdate(c *cmdCtx) (any, error) {
return nil, errs.Newf(errs.Usage, "只能是 status、check、prepare、install 或 recover,收到「%s」", c.inv.shownPos(c.inv.pos[0])).
WithAdvice(findCommand("update").usageAdvice())
}
if (c.inv.strOpt("package") != "" || c.inv.strOpt("sha256") != "") && action != "prepare" && action != "install" {
return nil, errs.New(errs.Usage, "--package 和 --sha256 只用于 update prepare 或 install")
}
afterPID, reopen, reopenArgs := c.inv.intOpt("after-pid"), c.inv.strOpt("reopen"), c.inv.strsOpt("reopen-arg")
handoff := afterPID != 0 || reopen != "" || len(reopenArgs) > 0
switch {
Expand All @@ -1460,7 +1467,8 @@ func runUpdate(c *cmdCtx) (any, error) {
case reopen != "" && !filepath.IsAbs(reopen):
return nil, errs.Newf(errs.Usage, "--reopen 要给程序的完整路径,收到「%s」", reopen)
}
opt := update.Options{AutoMASVersion: c.inv.strOpt("automas-version")}
opt := update.Options{AutoMASVersion: c.inv.strOpt("automas-version"),
PackagePath: c.inv.strOpt("package"), PackageSHA256: c.inv.strOpt("sha256")}
if err := updateCallerSettings(c.inv, action, &opt); err != nil {
return nil, err
}
Expand Down
13 changes: 13 additions & 0 deletions internal/download/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,9 @@ type FileOptions struct {
Fresh bool
Start func(offset int64, restarted bool)
Progress func(done int64)
// BeforeWrite can stop a transfer before opening the output or writing the
// next chunk (for example when the target volume runs out of space).
BeforeWrite func() error
}

// File resumes from the existing file size and validates the response before writing. Success requires exactly
Expand Down Expand Up @@ -271,6 +274,11 @@ func (c *Client) File(ctx context.Context, o FileOptions) error {
return sizeError("文件大小和预期不符(服务器给 %d 字节,应为 %d 字节)", resp.ContentLength, expected)
}
flags := os.O_WRONLY | os.O_CREATE | os.O_TRUNC
if o.BeforeWrite != nil {
if err := o.BeforeWrite(); err != nil {
return err
}
}
restarted := existing > 0 && status == http.StatusOK
switch {
case resume:
Expand All @@ -293,6 +301,11 @@ func (c *Client) File(ctx context.Context, o FileOptions) error {
for {
n, rerr := resp.read(buf, c.stallTimeout())
if n > 0 {
if o.BeforeWrite != nil {
if err := o.BeforeWrite(); err != nil {
return err
}
}
if done+int64(n) > expected {
return sizeError("文件大小和预期不符(收到的比 %d 字节多)", expected)
}
Expand Down
105 changes: 105 additions & 0 deletions internal/macos/xattr_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
//go:build darwin && cgo

package macos

/*
#include <sys/xattr.h>
#include <stdlib.h>
static ssize_t signature_listxattr(const char *path, void *buf, size_t size) {
return listxattr(path, buf, size, XATTR_NOFOLLOW);
}
static ssize_t signature_getxattr(const char *path, const char *name, void *buf, size_t size) {
return getxattr(path, name, buf, size, 0, XATTR_NOFOLLOW);
}
static int signature_setxattr(const char *path, const char *name, const void *buf, size_t size) {
return setxattr(path, name, buf, size, 0, XATTR_NOFOLLOW);
}
*/
import "C"

import (
"encoding/base64"
"fmt"
"strings"
"unsafe"
)

var signatureAttrs = map[string]bool{
"com.apple.cs.CodeDirectory": true, "com.apple.cs.CodeRequirements": true,
"com.apple.cs.CodeSignature": true, "com.apple.cs.CodeEntitlements": true,
}

// SignatureXattrs reads the generic code-signature attributes ZIP cannot retain.
// Other metadata is omitted. Unknown code-signature attributes fail closed.
func SignatureXattrs(p string) (map[string]string, error) {
cp := C.CString(p)
defer C.free(unsafe.Pointer(cp))
n, err := C.signature_listxattr(cp, nil, 0)
if n < 0 || n > 65536 {
return nil, fmt.Errorf("读取签名属性列表失败: %s (%v)", p, err)
}
names := make([]byte, int(n)+1)
read, err := C.signature_listxattr(cp, unsafe.Pointer(&names[0]), C.size_t(n))
if read != n {
return nil, fmt.Errorf("签名属性列表发生变化: %s (%v)", p, err)
}
out := map[string]string{}
for _, name := range strings.Split(string(names[:n]), "\x00") {
if !strings.HasPrefix(name, "com.apple.cs.") {
continue
}
if !signatureAttrs[name] {
return nil, fmt.Errorf("不支持的代码签名属性: %s", name)
}
cn := C.CString(name)
n, err := C.signature_getxattr(cp, cn, nil, 0)
if n < 0 || n > 1<<20 {
C.free(unsafe.Pointer(cn))
return nil, fmt.Errorf("代码签名属性不可读或太大: %s (%v)", p, err)
}
b := make([]byte, int(n)+1)
read, err := C.signature_getxattr(cp, cn, unsafe.Pointer(&b[0]), C.size_t(n))
C.free(unsafe.Pointer(cn))
if read != n {
return nil, fmt.Errorf("代码签名属性发生变化: %s (%v)", p, err)
}
out[name] = base64.StdEncoding.EncodeToString(b[:n])
}
return out, nil
}

func ValidateSignatureXattrs(attrs map[string]string) error {
for name, value := range attrs {
if !signatureAttrs[name] || len(value) > (1<<20)*4/3+4 {
return fmt.Errorf("不允许的签名扩展属性: %s", name)
}
b, err := base64.StdEncoding.DecodeString(value)
if err != nil || len(b) > 1<<20 {
return fmt.Errorf("签名扩展属性格式错误: %s", name)
}
}
return nil
}

// RestoreSignatureXattrs accepts only the bounded code-signature allowlist.
func RestoreSignatureXattrs(p string, attrs map[string]string) error {
if err := ValidateSignatureXattrs(attrs); err != nil {
return err
}
cp := C.CString(p)
defer C.free(unsafe.Pointer(cp))
for name, value := range attrs {
b, _ := base64.StdEncoding.DecodeString(value)
cn := C.CString(name)
var ptr unsafe.Pointer
if len(b) > 0 {
ptr = unsafe.Pointer(&b[0])
}
result, err := C.signature_setxattr(cp, cn, ptr, C.size_t(len(b)))
C.free(unsafe.Pointer(cn))
if result != 0 {
return fmt.Errorf("不能恢复签名属性 %s: %w", name, err)
}
}
return nil
}
2 changes: 2 additions & 0 deletions internal/ui/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ Apple Silicon macOS 的组件下载/导入使用 API35 Google Play ARM64 r9,
3. 用 `host.StartOrphan` 启动根目录的 `tools\modavd.exe --root <根> --json update install --after-pid <管理器 pid> --reopen <管理器程序> --reopen-arg …`(输出追加到 `logs\modavd-update.log`)。半秒后它还在跑,任务才算成功,结果 `{handoff: true, version, stopped, message}`;从这时起别的任务一律回 409,约 1.5 秒后管理器退出。命令行等管理器退出,换上新版,把结果写进 `.update\manager-result.json`,再按原来的参数(`--root`,以及 `--port`、`--no-window`、`--dev`)重新打开管理器;新管理器启动时读走这份结果,放进 `handoff`。换不上(有程序占着等)也照样重新打开,暂存的新版留着,下次不用重下。
- 管理器装的时候不带 AUTO-MAS 的版本检查(和人用命令行装一样),页面在 `required_automas` 不为空时提示用 AUTO-MAS 的先更新 AUTO-MAS。
- 开始前就拒绝、不开任务(422):不是 Windows,或者管理器不知道怎么重新打开自己(`unsupported`);根目录没有 `tools\modavd.exe`,或者它的版本和管理器不一样(`not_ready`,旧版命令行不认这些选项)。有别的任务在跑时回 409 `busy`;更新任务在跑时,别的任务也回 409。
- macOS 原生更新由 CLI 的 `update prepare/install --package ZIP --sha256 HEX` 选择本地签名包;暂存后可离线 `install/recover`。没有兼容的 `macos-arm64` 远端资产时,检查结果为 `update_unavailable`。界面不提供安装入口;包协议见 `internal/update/MACOS.md`。


## 生命周期

Expand Down
Loading
Loading