From 58cb7aff480199a3726899ea5144f0d23230d779 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Fri, 25 Sep 2026 14:40:50 +0200 Subject: [PATCH 1/3] Fix Android SDK auto-installation declining the licenses on Windows On Windows the installer answered the license prompts with `cmd.exe /C echo y | sdkmanager.bat ...`. The space before the pipe is part of what cmd's `echo` prints, so sdkmanager read "y " and declined every license, which skipped each package. sdkmanager exits with 0 when it skips a package or when licenses are declined, so the installer reported the SDK as installed and Flutter later failed with "No Android SDK found". A single answer could not have accepted the licenses anyway: `--licenses` asks once before the licenses and once per license. sdkmanager now runs directly on every platform, with its answers written to its standard input, and the installer fails when a package directory or the SDK license file is missing after sdkmanager returns. --- CHANGELOG.md | 6 ++ .../src/flet_cli/utils/android_sdk.py | 55 +++++++------------ .../flet-cli/src/flet_cli/utils/processes.py | 20 ++++++- 3 files changed, 45 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4ee25dc62..46c2e04cf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 1.0.2 + +### Bug fixes + +* Fix the Android SDK auto-installation failing on Windows ([#6522](https://github.com/flet-dev/flet/issues/6522)) by @ndonkoHenri. + ## 1.0.1 ### Improvements diff --git a/sdk/python/packages/flet-cli/src/flet_cli/utils/android_sdk.py b/sdk/python/packages/flet-cli/src/flet_cli/utils/android_sdk.py index bd122bad8b..9fc374481d 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/utils/android_sdk.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/utils/android_sdk.py @@ -19,6 +19,10 @@ "build-tools;34.0.0", ] +# Written to the standard input of `sdkmanager`, which reads one answer line per +# license prompt; `--licenses` also asks once before showing the licenses. +SDKMANAGER_ANSWERS = "y\n" * 50 + class AndroidSDK: """ @@ -322,36 +326,23 @@ def _install_package(self, home_dir: Path, package_name: str) -> int: RuntimeError: If package installation fails. """ - if home_dir.joinpath(*package_name.split(";")).exists(): + package_dir = home_dir.joinpath(*package_name.split(";")) + if package_dir.exists(): self.log(f'Android SDK package "{package_name}" is already installed') return 0 self.log(f'Installing Android SDK package "{package_name}"') p = self.run( - ( - [ - "sh", - "-c", - f'yes | "{self.sdkmanager_exe(home_dir)}" "{package_name}"', - ] - if platform.system() != "Windows" - else [ - "cmd.exe", - "/C", - "echo", - "y", - "|", - self.sdkmanager_exe(home_dir), - package_name, - ] - ), + [self.sdkmanager_exe(home_dir), package_name], env={"ANDROID_HOME": str(home_dir)}, capture_output=False, + input=SDKMANAGER_ANSWERS, ) - if p.returncode != 0: + # sdkmanager exits with 0 when it skips a package whose license is declined + if p.returncode != 0 or not package_dir.exists(): self.log(p.stderr) - raise RuntimeError("Error installing Android SDK tools") + raise RuntimeError(f'Error installing Android SDK package "{package_name}"') return 1 def _accept_licenses(self, home_dir: Path): @@ -368,23 +359,14 @@ def _accept_licenses(self, home_dir: Path): self.log("Accepting Android SDK licenses") p = self.run( - ( - ["sh", "-c", f'yes | "{self.sdkmanager_exe(home_dir)}" --licenses'] - if platform.system() != "Windows" - else [ - "cmd.exe", - "/C", - "echo", - "y", - "|", - self.sdkmanager_exe(home_dir), - "--licenses", - ] - ), + [self.sdkmanager_exe(home_dir), "--licenses"], env={"ANDROID_HOME": str(home_dir)}, capture_output=False, + input=SDKMANAGER_ANSWERS, ) - if p.returncode != 0: + # sdkmanager exits with 0 when licenses are declined + license_file = home_dir / "licenses" / "android-sdk-license" + if p.returncode != 0 or not license_file.exists(): self.log(p.stderr) raise RuntimeError("Error accepting Android SDK licenses") @@ -415,7 +397,7 @@ def get_installed_packages(self, home_dir: Path): ) return p.stdout - def run(self, args, env=None, cwd=None, capture_output=True): + def run(self, args, env=None, cwd=None, capture_output=True, input=None): """ Run a subprocess configured for Android SDK tooling. @@ -427,6 +409,8 @@ def run(self, args, env=None, cwd=None, capture_output=True): env: Optional additional environment variables. cwd: Optional working directory. Defaults to current directory. capture_output: Forwarded to subprocess helper. + input: Optional text for the subprocess's standard input. Forwarded + to subprocess helper. Returns: Subprocess result object returned by `flet_cli.utils.processes.run`. @@ -447,4 +431,5 @@ def run(self, args, env=None, cwd=None, capture_output=True): env=cmd_env, capture_output=capture_output, log=self.log, + input=input, ) diff --git a/sdk/python/packages/flet-cli/src/flet_cli/utils/processes.py b/sdk/python/packages/flet-cli/src/flet_cli/utils/processes.py index cce84bf5e0..b1ae6df4bc 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/utils/processes.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/utils/processes.py @@ -1,3 +1,4 @@ +import contextlib import os import subprocess from typing import Optional @@ -8,7 +9,14 @@ from ctypes import windll -def run(args, cwd, env: Optional[dict] = None, capture_output=True, log=None): +def run( + args, + cwd, + env: Optional[dict] = None, + capture_output=True, + log=None, + input: Optional[str] = None, +): """ Execute a subprocess command with optional streamed logging. @@ -22,6 +30,9 @@ def run(args, cwd, env: Optional[dict] = None, capture_output=True, log=None): capture_output: If `True`, run with `subprocess.run` and capture output in memory. If `False`, stream combined output line by line. log: Optional callback receiving each output line when `capture_output=False`. + input: Optional text written to the command's standard input, which is + then closed. When `capture_output=False`, it is written before any + output is read, so it must fit in the OS pipe buffer. Returns: A completed `subprocess.CompletedProcess` @@ -49,6 +60,7 @@ def run(args, cwd, env: Optional[dict] = None, capture_output=True, log=None): process = subprocess.run( args, cwd=cwd, + input=input, capture_output=True, text=True, encoding="utf-8", @@ -68,6 +80,12 @@ def run(args, cwd, env: Optional[dict] = None, capture_output=True, log=None): errors="replace", ) + if input is not None: + # The process may exit or close its standard input before reading it all + with contextlib.suppress(OSError): + process.stdin.write(input) + process.stdin.close() + try: while True: stdout_line = process.stdout.readline() From b0111f63b199c6f3f8d337bfabd3561e04f51582 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Fri, 25 Sep 2026 15:22:18 +0200 Subject: [PATCH 2/3] Keep the Android SDK installation on the pinned command-line tools Version 23.0 of the Android SDK command-line tools replaces sdkmanager with a wrapper around the Android CLI. The wrapper downloads the Android CLI on first use and ignores `--licenses`. It also rejects `--no-metrics`, so the Android CLI collects usage metrics by default. On Windows, the wrapper splits package names such as `platforms;android-35` at the `;`. On Windows 11 ARM64, where the x64 `android.exe` runs emulated, every Android CLI run exits with 0xC0000409, even after installing the package. Installing `cmdline-tools;latest` brought that version into every new SDK and made it the sdkmanager used for the remaining packages. Flutter only needs a `cmdline-tools` directory, so the pinned version that the installer downloads is enough. `cmdline-tools;latest` is no longer installed, and the pinned version is preferred over `latest` when both are present. --- .../packages/flet-cli/src/flet_cli/utils/android_sdk.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sdk/python/packages/flet-cli/src/flet_cli/utils/android_sdk.py b/sdk/python/packages/flet-cli/src/flet_cli/utils/android_sdk.py index 9fc374481d..60a9cfb750 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/utils/android_sdk.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/utils/android_sdk.py @@ -13,7 +13,6 @@ ANDROID_CMDLINE_TOOLS_VERSION = "12.0" MINIMAL_PACKAGES = [ - "cmdline-tools;latest", "platform-tools", "platforms;android-35", "build-tools;34.0.0", @@ -107,6 +106,11 @@ def cmdline_tools_bin(self, home_dir: Path) -> Optional[Path]: """ Return the `cmdline-tools` binary directory in an SDK installation. + The version installed by this CLI is preferred over `latest`, because + from version 23.0, `sdkmanager` is a wrapper around the Android CLI, + which ignores `--licenses`, collects usage metrics by default and fails + on Windows. + Args: home_dir: Android SDK home directory. @@ -115,8 +119,8 @@ def cmdline_tools_bin(self, home_dir: Path) -> Optional[Path]: """ for d in [ - home_dir / "cmdline-tools" / "latest" / "bin", home_dir / "cmdline-tools" / ANDROID_CMDLINE_TOOLS_VERSION / "bin", + home_dir / "cmdline-tools" / "latest" / "bin", ]: if d.exists(): return d From bec12f44c034966bd6b181453b4f924fc886e1c3 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Fri, 25 Sep 2026 16:37:40 +0200 Subject: [PATCH 3/3] Add PR link to changelog entry --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46c2e04cf1..b581211d98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ### Bug fixes -* Fix the Android SDK auto-installation failing on Windows ([#6522](https://github.com/flet-dev/flet/issues/6522)) by @ndonkoHenri. +* Fix the Android SDK auto-installation failing on Windows ([#6522](https://github.com/flet-dev/flet/issues/6522), [#6892](https://github.com/flet-dev/flet/pull/6892)) by @ndonkoHenri. ## 1.0.1