Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 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
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
46 changes: 29 additions & 17 deletions internal/cli/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,33 @@ var commands []*command

// The table is built in init: help's run function refers to it.
func init() {
componentDetails := "status(默认)查看组件,和 check 里的组件一样。fetch 补全缺少的组件:" +
"先从镜像取《Android 软件开发套件许可协议》全文并显示,输入 y 同意才继续,否则什么也不下;" +
"再依次试 googledownloads.cn、腾讯云镜像、dl.google.com,下载缺少的 adb 和系统镜像(约 1.5 GB,中断了再运行会接着下)," +
"校验后装进 sdk\\。已经齐全的跳过;不完整的旧目录改名为 <目录>.incomplete-<时间> 留着,不删。" +
"--play-store 再补全可选的 Google Play 商店(playstore <i> install 要用;已经有了就跳过):下 Google Play 版系统镜像," +
"从里面的 system.img 只取出商店、Play 服务、GSF、合作伙伴设置和商店的证书这五个文件,核对 SHA-256 后放进 sdk\\playstore," +
"解出的镜像和下载的压缩包随即删掉,只留这五个文件。" +
"import-playstore 用自己的文件补全它,不下载、不问许可协议:给 Google 官方的 Google Play 版镜像压缩包(x86_64-34_r14.zip,名字不限," +
"按 Google 仓库清单核对大小和 SHA-1),或者那五个文件本身(名字不限,逐个核对大小和 SHA-256;给文件夹就取里面同名的几个);" +
"认不出或对不上的一律拒收,什么也不改;只读你的文件,不动它们。" +
"status 列出这五个文件在不在、从哪来(download / import),不把它算进必需组件。" +
"不带参数时默认查看组件;source 查看或修改记住的下载源(auto、googledownloads_cn、tencent、google)。" +
"--json 时必须给 --accept-license(仅 fetch)。只有 Windows 有组件清单,darwin、linux 返回 unsupported。" +
"退出码:没同意许可协议是 2(license_declined),路径有中文、空间不够、不是魔改 AVD 的文件夹是 4," +
"正在补全是 5,下不成是 9(components_unavailable),下载或解压后没通过校验、导入的文件被拒收也是 9(components_rejected," +
"--json 的 error.details.reason 是 unknown_file、mismatch 或 incomplete)。"
if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
componentDetails = "status(默认)查看组件,和 check 里的组件一样。fetch 补全缺少的 adb 和 Android 15 Google APIs ARM64 系统镜像:" +
"先从镜像取待下载组件的 Google 许可协议全文,逐份输入 y 同意才继续,否则不下载归档。" +
"默认依次试 googledownloads.cn、腾讯云镜像、dl.google.com,下载约 1.8 GB,中断后再次运行会接着下;" +
"校验归档、版本和平台后装进 sdk/,保留执行权限。已经齐全的跳过;不完整的旧目录改名为 <目录>.incomplete-<时间> 留着,不删。" +
"SDK 使用中会拒绝补全,请先自行关闭使用它的进程。组件补全后,当前 macOS 版本仍未开放实例创建与启动。" +
"--play-store 和 import-playstore 目前仅支持 Windows。" +
"source 查看或修改记住的下载源(auto、googledownloads_cn、tencent、google);--mirror 或 --mirror-url 可覆盖本次选择。" +
"--json 时必须给 --accept-license(仅 fetch),表示已同意待下载组件的全部许可协议。" +
"退出码:未同意许可是 2(license_declined),未就绪或预检失败是 4,SDK 使用中或正在补全是 5,下载不可用或校验失败是 9。"
}
commands = []*command{
{
name: "list", summary: "列出全部实例和状态",
Expand Down Expand Up @@ -215,23 +242,8 @@ func init() {
},
{
name: "components", args: "[status] | source [auto|googledownloads_cn|tencent|google] | fetch [--accept-license] [--mirror ID] [--mirror-url URL] [--keep-downloads] [--play-store] | import-playstore <文件或文件夹>…",
summary: "查看或补全 Google 组件(adb 和系统镜像,可选的 Google Play 商店)",
details: "status(默认)查看组件,和 check 里的组件一样。fetch 补全缺少的组件:" +
"先从镜像取《Android 软件开发套件许可协议》全文并显示,输入 y 同意才继续,否则什么也不下;" +
"再依次试 googledownloads.cn、腾讯云镜像、dl.google.com,下载缺少的 adb 和系统镜像(约 1.5 GB,中断了再运行会接着下)," +
"校验后装进 sdk\\。已经齐全的跳过;不完整的旧目录改名为 <目录>.incomplete-<时间> 留着,不删。" +
"--play-store 再补全可选的 Google Play 商店(playstore <i> install 要用;已经有了就跳过):下 Google Play 版系统镜像," +
"从里面的 system.img 只取出商店、Play 服务、GSF、合作伙伴设置和商店的证书这五个文件,核对 SHA-256 后放进 sdk\\playstore," +
"解出的镜像和下载的压缩包随即删掉,只留这五个文件。" +
"import-playstore 用自己的文件补全它,不下载、不问许可协议:给 Google 官方的 Google Play 版镜像压缩包(x86_64-34_r14.zip,名字不限," +
"按 Google 仓库清单核对大小和 SHA-1),或者那五个文件本身(名字不限,逐个核对大小和 SHA-256;给文件夹就取里面同名的几个);" +
"认不出或对不上的一律拒收,什么也不改;只读你的文件,不动它们。" +
"status 列出这五个文件在不在、从哪来(download / import),不把它算进必需组件。" +
"不带参数时默认查看组件;source 查看或修改记住的下载源(auto、googledownloads_cn、tencent、google)。" +
"--json 时必须给 --accept-license(仅 fetch)。只有 Windows 有组件清单,darwin、linux 返回 unsupported。" +
"退出码:没同意许可协议是 2(license_declined),路径有中文、空间不够、不是魔改 AVD 的文件夹是 4," +
"正在补全是 5,下不成是 9(components_unavailable),下载或解压后没通过校验、导入的文件被拒收也是 9(components_rejected," +
"--json 的 error.details.reason 是 unknown_file、mismatch 或 incomplete)。",
summary: "查看或补全 Google 组件(adb 和系统镜像,可选的 Google Play 商店)",
details: componentDetails,
flags: fetchFlags,
posNames: []string{"status、source、fetch 或 import-playstore", "下载源或要导入的文件"}, maxPos: -1,
final: true,
Expand Down
67 changes: 59 additions & 8 deletions internal/components/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"context"
"crypto/sha1"
"crypto/sha256"
"encoding/hex"
"encoding/xml"
"errors"
Expand Down Expand Up @@ -54,11 +55,15 @@ func (f *fetcher) downloader() *download.Client {
// license gets the Android SDK license text from the first mirror that serves repository2-3.xml, and moves the
// mirrors that failed to the end of the list, so a dead one does not cost each component its timeout again.
func (f *fetcher) license(ctx context.Context) (text, from string, err error) {
return f.licenseFor(ctx, licenseManifest, licenseID, "")
}

func (f *fetcher) licenseFor(ctx context.Context, manifest, id, digest string) (text, from string, err error) {
var reasons []string
var failed []Mirror
for k, m := range f.mirrors {
f.where = "从「" + m.Name + "」获取许可协议清单 " + licenseManifest
t, err := f.licenseFrom(ctx, m)
f.where = "从「" + m.Name + "」获取许可协议清单 " + manifest
t, err := f.licenseFromSpec(ctx, m, manifest, id, digest)
if err == nil {
text, from = t, m.Name
break
Expand All @@ -74,7 +79,7 @@ func (f *fetcher) license(ctx context.Context) (text, from string, err error) {
}
}
if text == "" {
f.where = "获取许可协议清单 " + licenseManifest + ",试过的镜像都不行"
f.where = "获取许可协议清单 " + manifest + ",试过的镜像都不行"
return "", "", errs.New(errs.ComponentsUnavailable, "取不到《Android SDK 许可协议》全文,什么也没有下载(各镜像的情况:"+
strings.Join(reasons, ";")+")").WithAdvice(netAdvice("请检查网络后重试"))
}
Expand Down Expand Up @@ -106,11 +111,25 @@ func containsMirror(list []Mirror, m Mirror) bool {
}

func (f *fetcher) licenseFrom(ctx context.Context, m Mirror) (string, error) {
body, err := f.getBytes(ctx, m.Base+licenseManifest, "取许可协议清单 "+licenseManifest)
return f.licenseFromSpec(ctx, m, licenseManifest, licenseID, "")
}

func (f *fetcher) licenseFromSpec(ctx context.Context, m Mirror, manifest, id, digest string) (string, error) {
body, err := f.getBytes(ctx, m.Base+manifest, "取许可协议清单 "+manifest)
if err != nil {
return "", err
}
text, err := licenseText(body, id)
if err != nil {
return "", err
}
return licenseText(body, licenseID)
if digest != "" {
sum := sha256.Sum256([]byte(text + "\n"))
if hex.EncodeToString(sum[:]) != digest {
return "", fmt.Errorf("许可协议 %s 与固定版本的 SHA-256 不符", id)
}
}
return text, nil
}

func (f *fetcher) getBytes(ctx context.Context, rawURL, what string) ([]byte, error) {
Expand Down Expand Up @@ -177,6 +196,11 @@ func (f *fetcher) zipPath(c root.SDKComponent) string {
// size and SHA-1 are right. Otherwise each mirror in turn, up to downloadAttempts tries each, into <zip>.part, which is
// checked for SHA-1 and then renamed. The .part survives a failed try and a change of mirror: the next one resumes it.
func (f *fetcher) download(ctx context.Context, c root.SDKComponent) (string, error) {
if f.darwin() {
if err := f.platformPaths(); err != nil {
return "", err
}
}
zipFile := f.zipPath(c)
if err := os.MkdirAll(filepath.Dir(zipFile), 0o755); err != nil {
return "", errs.Wrap(errs.Internal, err, "建不了 downloads 目录:"+err.Error())
Expand All @@ -186,7 +210,7 @@ func (f *fetcher) download(ctx context.Context, c root.SDKComponent) (string, er
if st, err := os.Stat(zipFile); err == nil {
f.where = "校验上次下好的 " + name
f.say(fmt.Sprintf(" 发现上次下好的 %s,校验中…", name))
if sha, err := fileSHA1(zipFile); err == nil && st.Size() == c.Size && sha == c.SHA1 {
if sha, err := fileSHA1(zipFile); err == nil && st.Size() == c.Size && sha == c.SHA1 && archiveSHA256Matches(zipFile, c.SHA256) {
f.say(" 校验通过,直接用")
return zipFile, nil
}
Expand All @@ -201,14 +225,18 @@ func (f *fetcher) download(ctx context.Context, c root.SDKComponent) (string, er
if err != nil {
return "", errs.Wrap(errs.Internal, err, "读不了 "+part+":"+err.Error())
}
if sha == c.SHA1 {
if sha == c.SHA1 && archiveSHA256Matches(part, c.SHA256) {
f.say(" 校验通过,直接用")
if err := os.Rename(part, zipFile); err != nil {
return "", err
}
return zipFile, nil
}
f.warn(" 上次留下的 .part SHA-1 不对,丢弃后重新下载")
if c.SHA256 != "" {
f.warn(" 上次留下的 .part SHA-1/SHA-256 校验不通过,丢弃后重新下载")
} else {
f.warn(" 上次留下的 .part SHA-1 不对,丢弃后重新下载")
}
if err := os.Remove(part); err != nil {
return "", err
}
Expand Down Expand Up @@ -267,6 +295,13 @@ func (f *fetcher) download(ctx context.Context, c root.SDKComponent) (string, er
os.Remove(part)
continue
}
if !archiveSHA256Matches(part, c.SHA256) {
f.warn(" SHA-256 不对或文件不可读;丢弃,换下一个镜像")
reasons = append(reasons, m.Name+" 校验:SHA-256 与固定归档不符")
only416 = false
os.Remove(part)
continue
}
f.say(" 校验通过")
if err := os.Rename(part, zipFile); err != nil {
return "", errs.Wrap(errs.Internal, err, "改不了名:"+part+":"+err.Error())
Expand Down Expand Up @@ -331,6 +366,22 @@ func fileSHA1(p string) (string, error) {
return hex.EncodeToString(h.Sum(nil)), nil
}

func archiveSHA256Matches(path, want string) bool {
if want == "" {
return true
}
f, err := os.Open(path)
if err != nil {
return false
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return false
}
return hex.EncodeToString(h.Sum(nil)) == want
}

// clampMB is mb MiB in bytes without overflowing.
func clampMB(mb int64) int64 {
if mb > math.MaxInt64>>20 {
Expand Down
Loading
Loading