From dc1960434d0651fca9f15958b4e899810edb3e01 Mon Sep 17 00:00:00 2001 From: dadachi Date: Sun, 3 May 2026 13:12:18 +0900 Subject: [PATCH] adb: resolve to known-good binary instead of trusting PATH order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Surfaced during the first real Layer 3 visual smoke against a booted Android emulator. The agent's installAndLaunch (#39) and captureScreenshot (#38) were calling `adb` via the PATH-resolved binary, which on this dev machine was `~/.apportable/SDK/bin/adb` — an i386 Mach-O from 2014 that fails to exec on Apple Silicon with "spawn Unknown system error -86". Visible adb installs (Android Studio default, /Applications/android- sdk-macosx, Homebrew) were further down PATH and never reached. New helper resolveAdbPath() in src/adb.ts walks a fixed priority order: env-var locations first ($ANDROID_HOME, $ANDROID_SDK_ROOT), then known macOS install paths (Android Studio default, the older /Applications/android-sdk-macosx, /opt/homebrew, /usr/local), and falls back to "adb" via PATH. First existing path wins. src/validation/capture.ts (Android branch) and src/validation/launch.ts (install + launch) now resolve via this helper instead of trusting PATH. Real-mode smoke confirms the fix: on this machine, adb now resolves to /Applications/android-sdk-macosx/platform-tools/adb (universal binary, x86_64+arm64). The Apple Silicon spawn error is gone. Newly-surfaced (separate concern, documented in README): when multiple Android targets are attached (e.g. physical device + emulator), adb requires ANDROID_SERIAL= to disambiguate. This is a stock adb feature, not something the agent needs to implement — the agent runs adb directly and inherits the env var. Tests: 19/19 npm run ci green. (No adb-specific test in CI since that requires a real Android SDK; resolution logic is verified by the real-mode smoke output above.) Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 3 +++ src/adb.ts | 30 ++++++++++++++++++++++++++++++ src/validation/capture.ts | 6 ++++-- src/validation/launch.ts | 10 ++++++---- 4 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 src/adb.ts diff --git a/README.md b/README.md index 2525211..76163ba 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,9 @@ The agent will also be available as a Claude Code plugin. - `NATIVEAPPTEMPLATE_VISUAL=1` — opts the run into Stage 1 visual judging (Layer 3). When set, Layer 2 runs in **build mode** instead of fast mode (full `xcodebuild build` + `./gradlew assembleDebug`), then for each platform the agent installs the app on the booted sim/emulator, captures the home screen, and judges it with Opus 4.7 vision against `DEFAULT_STAGE1_RUBRIC`. Adds 60-180s per platform depending on cold-build time. Requires a sim/emulator booted for each platform you want judged. Off by default — `npm run dev` keeps the existing fast path. - `NATIVEAPPTEMPLATE_AGENT_ANTHROPIC_KEY` — dedicated workspace key, see [Security](#security). +- `ANDROID_SERIAL` — when more than one Android device/emulator is attached (e.g. a physical device plus a running emulator), `adb` standard practice is to set `ANDROID_SERIAL=` to disambiguate. The agent honors this transparently because it runs `adb` directly. Run `adb devices` to list serials. Visual-judge runs with multiple Android targets attached will error with `more than one device/emulator` if this isn't set. + +The agent resolves `adb` to a known-good binary in this priority order: `$ANDROID_HOME/platform-tools/adb`, `$ANDROID_SDK_ROOT/platform-tools/adb`, `~/Library/Android/sdk/platform-tools/adb` (Android Studio default), `/Applications/android-sdk-macosx/platform-tools/adb`, `/opt/homebrew/bin/adb`, `/usr/local/bin/adb`, then PATH. This avoids surprises like a stale `~/.apportable/SDK/bin/adb` (i386, won't exec on Apple Silicon) shadowing a working `adb` on PATH. ## Validation (three layers) diff --git a/src/adb.ts b/src/adb.ts new file mode 100644 index 0000000..50082cd --- /dev/null +++ b/src/adb.ts @@ -0,0 +1,30 @@ +import { existsSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; + +// Some dev machines have a stale `adb` shadowing the modern one on PATH — +// the canonical case is /Users//.apportable/SDK/bin/adb, an i386 binary +// from 2014 that fails to exec on Apple Silicon with "Unknown system error +// -86". Resolve to a known-good adb instead of trusting PATH order: +// +// 1. $ANDROID_HOME/platform-tools/adb (canonical env var) +// 2. $ANDROID_SDK_ROOT/platform-tools/adb (legacy spelling) +// 3. ~/Library/Android/sdk/platform-tools/adb (Android Studio default on macOS) +// 4. /Applications/android-sdk-macosx/platform-tools/adb (older macOS standalone install) +// 5. /opt/homebrew/bin/adb (Homebrew on Apple Silicon) +// 6. /usr/local/bin/adb (Homebrew on Intel) +// 7. "adb" (fall back to PATH lookup) +export function resolveAdbPath(): string { + const candidates = [ + process.env['ANDROID_HOME'] ? join(process.env['ANDROID_HOME'], "platform-tools", "adb") : null, + process.env['ANDROID_SDK_ROOT'] ? join(process.env['ANDROID_SDK_ROOT'], "platform-tools", "adb") : null, + join(homedir(), "Library", "Android", "sdk", "platform-tools", "adb"), + "/Applications/android-sdk-macosx/platform-tools/adb", + "/opt/homebrew/bin/adb", + "/usr/local/bin/adb", + ]; + for (const candidate of candidates) { + if (candidate && existsSync(candidate)) return candidate; + } + return "adb"; +} diff --git a/src/validation/capture.ts b/src/validation/capture.ts index 5297456..a25747e 100644 --- a/src/validation/capture.ts +++ b/src/validation/capture.ts @@ -3,6 +3,7 @@ import { createWriteStream } from "node:fs"; import { mkdir } from "node:fs/promises"; import { dirname } from "node:path"; import { scrubbedEnv } from "../env.js"; +import { resolveAdbPath } from "../adb.js"; export type CapturePlatform = "ios" | "android"; @@ -97,12 +98,13 @@ async function captureIos(outPath: string, timeoutMs: number): Promise { - const command = `adb exec-out screencap -p > ${outPath}`; + const adb = resolveAdbPath(); + const command = `${adb} exec-out screencap -p > ${outPath}`; const started = Date.now(); return new Promise((resolvePromise) => { let child; try { - child = spawn("adb", ["exec-out", "screencap", "-p"], { + child = spawn(adb, ["exec-out", "screencap", "-p"], { env: scrubbedEnv(), stdio: ["ignore", "pipe", "pipe"], }); diff --git a/src/validation/launch.ts b/src/validation/launch.ts index 1ee51bf..8640ed8 100644 --- a/src/validation/launch.ts +++ b/src/validation/launch.ts @@ -1,5 +1,6 @@ import { spawn } from "node:child_process"; import { scrubbedEnv } from "../env.js"; +import { resolveAdbPath } from "../adb.js"; export type LaunchResult = { ok: boolean; @@ -78,11 +79,12 @@ async function installAndLaunchIos(appPath: string, bundleId: string, timeoutMs: } async function installAndLaunchAndroid(apkPath: string, packageName: string, timeoutMs: number): Promise { + const adb = resolveAdbPath(); const started = Date.now(); - const installCmd = `adb install -r ${apkPath}`; - const launchCmd = `adb shell monkey -p ${packageName} -c android.intent.category.LAUNCHER 1`; + const installCmd = `${adb} install -r ${apkPath}`; + const launchCmd = `${adb} shell monkey -p ${packageName} -c android.intent.category.LAUNCHER 1`; - const install = await runOnce("adb", ["install", "-r", apkPath], timeoutMs); + const install = await runOnce(adb, ["install", "-r", apkPath], timeoutMs); if (!install.ok) { return { ok: false, @@ -92,7 +94,7 @@ async function installAndLaunchAndroid(apkPath: string, packageName: string, tim }; } const launch = await runOnce( - "adb", + adb, ["shell", "monkey", "-p", packageName, "-c", "android.intent.category.LAUNCHER", "1"], timeoutMs, );