From 37e48041c139b9a54c3784dd7a8142525d69e843 Mon Sep 17 00:00:00 2001 From: Emanuel L Date: Sun, 13 Sep 2026 08:43:16 -0600 Subject: [PATCH 1/3] fix(desktop): carry ANDROID_HOME through from the login shell MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A GUI launch inherits no shell rc, so `ANDROID_HOME` and `ANDROID_SDK_ROOT` exported from a profile never reach the app. The device host falls back to the default SDK locations, and when the SDK lives anywhere else it reports "Android SDK was not found. Install it with Android Studio or set ANDROID_HOME to your SDK directory" — to a user who has already done exactly that. Add both names to the POSIX login-shell hydration, alongside the existing HOMEBREW_* and XDG_* entries. Windows reads user variables from the registry and already inherits them. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/shell/DesktopShellEnvironment.test.ts | 20 +++++++++++++++++++ .../src/shell/DesktopShellEnvironment.ts | 7 +++++++ 2 files changed, 27 insertions(+) diff --git a/apps/desktop/src/shell/DesktopShellEnvironment.test.ts b/apps/desktop/src/shell/DesktopShellEnvironment.test.ts index 5a76402b1d34e..cca8d99b33015 100644 --- a/apps/desktop/src/shell/DesktopShellEnvironment.test.ts +++ b/apps/desktop/src/shell/DesktopShellEnvironment.test.ts @@ -129,6 +129,26 @@ describe("DesktopShellEnvironment", () => { }), ); + it.effect("hydrates the Android SDK location from the login shell on macOS", () => + Effect.gen(function* () { + const env: NodeJS.ProcessEnv = { SHELL: "/bin/zsh", PATH: "/usr/bin" }; + + yield* runShellEnvironment({ + env, + platform: "darwin", + handler: () => + envOutput({ + PATH: "/usr/bin", + ANDROID_HOME: "/Volumes/Work/android-sdk", + ANDROID_SDK_ROOT: "/Volumes/Work/android-sdk", + }), + }); + + assert.equal(env.ANDROID_HOME, "/Volumes/Work/android-sdk"); + assert.equal(env.ANDROID_SDK_ROOT, "/Volumes/Work/android-sdk"); + }), + ); + it.effect("preserves inherited POSIX values when present", () => Effect.gen(function* () { const env: NodeJS.ProcessEnv = { diff --git a/apps/desktop/src/shell/DesktopShellEnvironment.ts b/apps/desktop/src/shell/DesktopShellEnvironment.ts index 5c6b67fd58c01..60c5b9923f70d 100644 --- a/apps/desktop/src/shell/DesktopShellEnvironment.ts +++ b/apps/desktop/src/shell/DesktopShellEnvironment.ts @@ -69,6 +69,11 @@ export class DesktopShellEnvironment extends Context.Service< const LOGIN_SHELL_ENV_NAMES = [ "PATH", + // A GUI launch runs no shell rc, so an SDK in a non-default location is + // invisible to the device host, which then tells the user to "set + // ANDROID_HOME" — advice they have usually already followed in their profile. + "ANDROID_HOME", + "ANDROID_SDK_ROOT", "DBUS_SESSION_BUS_ADDRESS", "DISPLAY", "LANG", @@ -466,6 +471,8 @@ const installPosixEnvironment = Effect.fn("desktop.shellEnvironment.installPosix } for (const name of [ + "ANDROID_HOME", + "ANDROID_SDK_ROOT", "DISPLAY", "HOMEBREW_PREFIX", "HOMEBREW_CELLAR", From b9cb75be5287d2455748674c0c50554447c815cb Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:50:59 -0700 Subject: [PATCH 2/3] fix(desktop): keep Android SDK hydration regression focused --- .../src/shell/DesktopShellEnvironment.test.ts | 22 +++++++++---------- .../src/shell/DesktopShellEnvironment.ts | 5 ++--- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/apps/desktop/src/shell/DesktopShellEnvironment.test.ts b/apps/desktop/src/shell/DesktopShellEnvironment.test.ts index 4762586a90dc8..282c98a4db23a 100644 --- a/apps/desktop/src/shell/DesktopShellEnvironment.test.ts +++ b/apps/desktop/src/shell/DesktopShellEnvironment.test.ts @@ -220,29 +220,27 @@ describe("DesktopShellEnvironment", () => { }), ); - it.effect("hydrates the Android SDK location from the login shell on macOS", () => + it.effect("hydrates an unset Android SDK location from the login shell on macOS", () => Effect.gen(function* () { - const env: NodeJS.ProcessEnv = { SHELL: "/bin/zsh", PATH: "/usr/bin" }; + const env: NodeJS.ProcessEnv = { + SHELL: "/bin/zsh", + PATH: "/usr/bin", + ANDROID_SDK_ROOT: "/inherited/android-sdk", + }; yield* runShellEnvironment({ env, platform: "darwin", - handler: (command) => { - assert.equal(command._tag, "StandardCommand"); - if (command._tag !== "StandardCommand") return ""; - const script = command.args.join(" "); - assert.include(script, "__T3CODE_ENV_ANDROID_HOME_START__"); - assert.include(script, "__T3CODE_ENV_ANDROID_SDK_ROOT_START__"); - return envOutput({ + handler: () => + envOutput({ PATH: "/usr/bin", ANDROID_HOME: "/Volumes/Work/android-sdk", ANDROID_SDK_ROOT: "/Volumes/Work/android-sdk", - }); - }, + }), }); assert.equal(env.ANDROID_HOME, "/Volumes/Work/android-sdk"); - assert.equal(env.ANDROID_SDK_ROOT, "/Volumes/Work/android-sdk"); + assert.equal(env.ANDROID_SDK_ROOT, "/inherited/android-sdk"); }), ); diff --git a/apps/desktop/src/shell/DesktopShellEnvironment.ts b/apps/desktop/src/shell/DesktopShellEnvironment.ts index dd7e8d8ec566d..482b832a3acaa 100644 --- a/apps/desktop/src/shell/DesktopShellEnvironment.ts +++ b/apps/desktop/src/shell/DesktopShellEnvironment.ts @@ -71,9 +71,8 @@ export class DesktopShellEnvironment extends Context.Service< const LOGIN_SHELL_ENV_NAMES = [ "PATH", - // A GUI launch runs no shell rc, so an SDK in a non-default location is - // invisible to the device host, which then tells the user to "set - // ANDROID_HOME" — advice they have usually already followed in their profile. + // The device host locates the Android SDK from these; set only in a profile, + // they are otherwise invisible to a GUI launch. "ANDROID_HOME", "ANDROID_SDK_ROOT", "DBUS_SESSION_BUS_ADDRESS", From c4c9ff1ee99dd41d53ade45bf47b8295d449de27 Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:56:29 -0700 Subject: [PATCH 3/3] fix(desktop): preserve inherited Android SDK selection --- .../src/shell/DesktopShellEnvironment.test.ts | 28 +++++++++++++------ .../src/shell/DesktopShellEnvironment.ts | 16 ++++++++--- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/apps/desktop/src/shell/DesktopShellEnvironment.test.ts b/apps/desktop/src/shell/DesktopShellEnvironment.test.ts index 282c98a4db23a..d0dd17fcdb3b3 100644 --- a/apps/desktop/src/shell/DesktopShellEnvironment.test.ts +++ b/apps/desktop/src/shell/DesktopShellEnvironment.test.ts @@ -220,13 +220,25 @@ describe("DesktopShellEnvironment", () => { }), ); - it.effect("hydrates an unset Android SDK location from the login shell on macOS", () => + it.effect.each([ + { + name: "hydrates both Android SDK names when neither is inherited", + inherited: {}, + expected: { + ANDROID_HOME: "/Volumes/Work/android-sdk", + ANDROID_SDK_ROOT: "/Volumes/Work/android-sdk", + }, + }, + { + // The device host reads ANDROID_HOME before ANDROID_SDK_ROOT, so a shell + // ANDROID_HOME next to an inherited ANDROID_SDK_ROOT would change the SDK. + name: "keeps an inherited ANDROID_SDK_ROOT selected over a login-shell ANDROID_HOME", + inherited: { ANDROID_SDK_ROOT: "/inherited/android-sdk" }, + expected: { ANDROID_HOME: undefined, ANDROID_SDK_ROOT: "/inherited/android-sdk" }, + }, + ])("$name", ({ inherited, expected }) => Effect.gen(function* () { - const env: NodeJS.ProcessEnv = { - SHELL: "/bin/zsh", - PATH: "/usr/bin", - ANDROID_SDK_ROOT: "/inherited/android-sdk", - }; + const env: NodeJS.ProcessEnv = { SHELL: "/bin/zsh", PATH: "/usr/bin", ...inherited }; yield* runShellEnvironment({ env, @@ -239,8 +251,8 @@ describe("DesktopShellEnvironment", () => { }), }); - assert.equal(env.ANDROID_HOME, "/Volumes/Work/android-sdk"); - assert.equal(env.ANDROID_SDK_ROOT, "/inherited/android-sdk"); + assert.equal(env.ANDROID_HOME, expected.ANDROID_HOME); + assert.equal(env.ANDROID_SDK_ROOT, expected.ANDROID_SDK_ROOT); }), ); diff --git a/apps/desktop/src/shell/DesktopShellEnvironment.ts b/apps/desktop/src/shell/DesktopShellEnvironment.ts index 482b832a3acaa..095387f413d2e 100644 --- a/apps/desktop/src/shell/DesktopShellEnvironment.ts +++ b/apps/desktop/src/shell/DesktopShellEnvironment.ts @@ -71,8 +71,6 @@ export class DesktopShellEnvironment extends Context.Service< const LOGIN_SHELL_ENV_NAMES = [ "PATH", - // The device host locates the Android SDK from these; set only in a profile, - // they are otherwise invisible to a GUI launch. "ANDROID_HOME", "ANDROID_SDK_ROOT", "DBUS_SESSION_BUS_ADDRESS", @@ -99,6 +97,7 @@ const LOGIN_SHELL_ENV_NAMES = [ ] as const; const WINDOWS_PROFILE_ENV_NAMES = ["PATH", "FNM_DIR", "FNM_MULTISHELL_PATH"] as const; const LOCALE_ENV_NAMES = ["LANG", "LC_ALL", "LC_CTYPE"] as const; +const ANDROID_SDK_ENV_NAMES = ["ANDROID_HOME", "ANDROID_SDK_ROOT"] as const; const FALLBACK_LC_CTYPE = "en_US.UTF-8"; const WINDOWS_SHELL_CANDIDATES = ["pwsh.exe", "powershell.exe"] as const; const LOGIN_SHELL_TIMEOUT = Duration.seconds(5); @@ -455,8 +454,6 @@ const installPosixEnvironment = Effect.fn("desktop.shellEnvironment.installPosix } for (const name of [ - "ANDROID_HOME", - "ANDROID_SDK_ROOT", "DISPLAY", "HOMEBREW_PREFIX", "HOMEBREW_CELLAR", @@ -478,6 +475,17 @@ const installPosixEnvironment = Effect.fn("desktop.shellEnvironment.installPosix } } + // The device host picks the Android SDK from ANDROID_HOME before ANDROID_SDK_ROOT, + // so a profile-only ANDROID_HOME must not shadow an inherited ANDROID_SDK_ROOT. + if (ANDROID_SDK_ENV_NAMES.every((name) => Option.isNone(trimNonEmpty(config.env[name])))) { + for (const name of ANDROID_SDK_ENV_NAMES) { + const value = trimNonEmpty(shellEnvironment[name]); + if (Option.isSome(value)) { + config.env[name] = value.value; + } + } + } + // Locale variables form one precedence group: LC_ALL can override an inherited // LANG or LC_CTYPE, so only hydrate the group when the process has none of them. if (