Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

### Bug fixes

* 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.
* Fix `InteractiveViewer` failing to render when `alignment` is set ([#6744](https://github.com/flet-dev/flet/issues/6744), [#6887](https://github.com/flet-dev/flet/pull/6887)) by @ndonkoHenri.
* flet-charts: Fix `ScatterChart` failing to render when a `ScatterChartSpot` has `x_error` or `y_error` set. Error ranges are now set with the new `ChartErrorRange` type and drawn as error bars in the color of their spot ([#6885](https://github.com/flet-dev/flet/pull/6885)) by @ndonkoHenri.
* Fix iOS apps that use `sqlite3` stopping with `Assertion failed: (0), function isBulkReadStatement` when run from Xcode on iOS 18 and later, by turning off Xcode's Thread Performance Checker in the scheme generated by `flet build` ([#5480](https://github.com/flet-dev/flet/issues/5480), [#6882](https://github.com/flet-dev/flet/pull/6882)) by @ndonkoHenri.
Expand Down
63 changes: 26 additions & 37 deletions sdk/python/packages/flet-cli/src/flet_cli/utils/android_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@
ANDROID_CMDLINE_TOOLS_VERSION = "12.0"

MINIMAL_PACKAGES = [
"cmdline-tools;latest",
"platform-tools",
"platforms;android-35",
"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:
"""
Expand Down Expand Up @@ -103,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.

Expand All @@ -111,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
Expand Down Expand Up @@ -322,36 +330,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):
Expand All @@ -368,23 +363,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")

Expand Down Expand Up @@ -415,7 +401,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.

Expand All @@ -427,6 +413,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`.
Expand All @@ -447,4 +435,5 @@ def run(self, args, env=None, cwd=None, capture_output=True):
env=cmd_env,
capture_output=capture_output,
log=self.log,
input=input,
)
20 changes: 19 additions & 1 deletion sdk/python/packages/flet-cli/src/flet_cli/utils/processes.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import contextlib
import os
import subprocess
from typing import Optional
Expand All @@ -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.

Expand All @@ -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`
Expand Down Expand Up @@ -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",
Expand All @@ -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()
Expand Down
Loading