From bc5a82b26d4b3916a90e27b0b6a19b1fbeb09540 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 7 Jul 2026 10:22:07 +0100 Subject: [PATCH] fix(android): auto-detect the Android SDK when fresh worktrees lack local.properties (#101273) --- scripts/run-android-gradle.mjs | 35 ++++++++++++++-- test/scripts/run-android-gradle.test.ts | 53 +++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 4 deletions(-) diff --git a/scripts/run-android-gradle.mjs b/scripts/run-android-gradle.mjs index ed12ee0c9188..ecaf841fb535 100644 --- a/scripts/run-android-gradle.mjs +++ b/scripts/run-android-gradle.mjs @@ -1,5 +1,7 @@ #!/usr/bin/env node import { spawn } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; @@ -39,11 +41,35 @@ export function linuxArmAndroidGradleSkipMessage(platform = process.platform, ar ); } -export function run(command, args, cwd) { +// Fresh git worktrees do not carry the gitignored apps/android/local.properties, +// so AGP tasks fail with "SDK location not found" even when an SDK is installed. +// Fall back to the Android Studio default install path when nothing names one. +export function resolveAndroidSdkEnv(options = {}) { + const env = options.env ?? process.env; + if (env.ANDROID_HOME || env.ANDROID_SDK_ROOT) { + return env; + } + const existsSync = options.existsSync ?? fs.existsSync; + if (existsSync(path.join(androidDir, "local.properties"))) { + return env; + } + const homeDir = options.homeDir ?? os.homedir(); + const platform = options.platform ?? process.platform; + const defaultSdkDir = + platform === "darwin" + ? path.join(homeDir, "Library", "Android", "sdk") + : path.join(homeDir, "Android", "Sdk"); + if (!existsSync(defaultSdkDir)) { + return env; + } + return { ...env, ANDROID_HOME: defaultSdkDir }; +} + +export function run(command, args, cwd, env = process.env) { return new Promise((resolve) => { const child = spawn(command, args, { cwd, - env: process.env, + env, stdio: "inherit", }); child.on("close", (status, signal) => { @@ -78,12 +104,13 @@ export async function main(argv = process.argv.slice(2)) { return 0; } - const gradleStatus = await run("./gradlew", gradleArgs, androidDir); + const env = resolveAndroidSdkEnv(); + const gradleStatus = await run("./gradlew", gradleArgs, androidDir, env); if (gradleStatus !== 0 || postArgs.length === 0) { return gradleStatus; } - return await run(postArgs[0], postArgs.slice(1), repoRoot); + return await run(postArgs[0], postArgs.slice(1), repoRoot, env); } if (isMain) { diff --git a/test/scripts/run-android-gradle.test.ts b/test/scripts/run-android-gradle.test.ts index 239e6fe4fa71..56fa9b653393 100644 --- a/test/scripts/run-android-gradle.test.ts +++ b/test/scripts/run-android-gradle.test.ts @@ -1,6 +1,8 @@ +import path from "node:path"; import { describe, expect, it } from "vitest"; import { linuxArmAndroidGradleSkipMessage, + resolveAndroidSdkEnv, shouldSkipLinuxArmAndroidGradle, splitAndroidGradleArgs, } from "../../scripts/run-android-gradle.mjs"; @@ -37,4 +39,55 @@ describe("run-android-gradle", () => { "OPENCLAW_ANDROID_GRADLE_ALLOW_LINUX_ARM=1", ); }); + + describe("resolveAndroidSdkEnv", () => { + const macSdk = path.join("/Users/dev", "Library", "Android", "sdk"); + const linuxSdk = path.join("/home/dev", "Android", "Sdk"); + + it("keeps env untouched when ANDROID_HOME or ANDROID_SDK_ROOT is set", () => { + const env = { ANDROID_HOME: "/opt/sdk" }; + expect(resolveAndroidSdkEnv({ env, existsSync: () => true })).toBe(env); + const rootEnv = { ANDROID_SDK_ROOT: "/opt/sdk" }; + expect(resolveAndroidSdkEnv({ env: rootEnv, existsSync: () => true })).toBe(rootEnv); + }); + + it("keeps env untouched when local.properties exists", () => { + const env = {}; + const result = resolveAndroidSdkEnv({ + env, + existsSync: (p: string) => p.endsWith("local.properties"), + homeDir: "/Users/dev", + platform: "darwin", + }); + expect(result).toBe(env); + }); + + it("falls back to the Android Studio default SDK path per platform", () => { + const darwin = resolveAndroidSdkEnv({ + env: {}, + existsSync: (p: string) => p === macSdk, + homeDir: "/Users/dev", + platform: "darwin", + }); + expect(darwin.ANDROID_HOME).toBe(macSdk); + const linux = resolveAndroidSdkEnv({ + env: {}, + existsSync: (p: string) => p === linuxSdk, + homeDir: "/home/dev", + platform: "linux", + }); + expect(linux.ANDROID_HOME).toBe(linuxSdk); + }); + + it("keeps env untouched when no default SDK install exists", () => { + const env = {}; + const result = resolveAndroidSdkEnv({ + env, + existsSync: () => false, + homeDir: "/Users/dev", + platform: "darwin", + }); + expect(result).toBe(env); + }); + }); });