From 2534be659fe98ce3b3cc697a73c53c329cf9cea5 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Sat, 6 Jun 2026 12:10:51 -0700 Subject: [PATCH 1/2] feat(build): cache flet-build-template.zip across builds; share FLET_CACHE_DIR with Gradle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes flet-dev/flet#6555. Add a small on-disk cache for the build template zip downloaded by `flet build` / `flet debug`. The template at a versioned release URL is immutable, so caching is "use if present, else download once" — no revalidation needed. The download bypasses cookiecutter's network fetch and writes to /build-template/v/flet-build-template.zip via urllib + atomic .tmp + os.replace; cookiecutter then unpacks the local zip path directly. Cache root resolution: $FLET_CACHE_DIR if set, otherwise ~/.flet/cache. The resolved root is exported back into os.environ so the Gradle child process (serious_python_android >= 1.0.1) lands its Python dist tarballs under the same root by default, fixing the multi-minute "downloadDistArchive_*" delay on every Android debug build. Bumps the bundled build template's serious_python pin from 1.0.0 to 1.0.1 so the new persistent Python-tarball cache + conditional-GET revalidation in serious_python_android 1.0.1 is picked up. Custom --template URLs and the local-dev template path are unchanged — the cache layer only engages for the default versioned-release URL. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 2 + .../src/flet_cli/commands/build_base.py | 15 ++++-- .../src/flet_cli/utils/template_cache.py | 48 +++++++++++++++++++ .../{{cookiecutter.out_dir}}/pubspec.yaml | 2 +- 4 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 sdk/python/packages/flet-cli/src/flet_cli/utils/template_cache.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 59c3bbba12..d74b7afec6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ * Allow `[tool.flet.android.permission]` values to be TOML inline tables in addition to booleans — each `key = "value"` entry adds an `android:=""` attribute to the generated `` element, unlocking modifiers like `android:maxSdkVersion` and `android:usesPermissionFlags` that real-world Android permissions (e.g. Bluetooth LE) require. The boolean form and the `--android-permissions` CLI flag are unchanged; a non-empty inline table is always emitted, an empty table (`{}`) is treated as `false`, and invalid value types fail the build with a clear error ([#6550](https://github.com/flet-dev/flet/issues/6550), [#6551](https://github.com/flet-dev/flet/pull/6551)) by @FeodorFitsner. * Upgrade the bundled Pyodide runtime in the `flet build web` template from `0.27.5` to `0.27.7` (includes `micropip` `0.9.0`) ([#6549](https://github.com/flet-dev/flet/pull/6549)) by @FeodorFitsner. * Drop generated `web/canvaskit/` build artifacts (`canvaskit.js`/`.wasm`/`.symbols` and the `chromium/` and `skwasm`/`skwasm_st` variants) from the `flet build web` template — these are produced by the Flutter web build and should not have been committed into the cookiecutter template ([#6549](https://github.com/flet-dev/flet/pull/6549)) by @FeodorFitsner. +* Cache the downloaded `flet-build-template.zip` across builds. The build template is bound to an exact Flet version and is immutable, so on every `flet build` / `flet debug` after the first, the CLI now uses a previously-downloaded zip from `$FLET_CACHE_DIR/build-template/v/` (defaulting to `~/.flet/cache/build-template/v/`) instead of re-fetching it via cookiecutter. The CLI also exports `FLET_CACHE_DIR` into the child Gradle process, so `serious_python_android` >= 1.0.1 lands its Python dist tarballs (`python-android-dart--.tar.gz`) in the same cache root by default — fixing the multi-minute "Creating app shell" / `downloadDistArchive_*` delay on every Android debug build. Custom `--template` URLs and the local-dev template path are unchanged ([#6555](https://github.com/flet-dev/flet/discussions/6555)) by @FeodorFitsner. +* Bump the bundled build template's `serious_python` dependency from `1.0.0` to `1.0.1` so Android builds pick up the new persistent Python-tarball cache + conditional-GET revalidation introduced in [`serious_python` 1.0.1](https://github.com/flet-dev/serious-python/blob/main/src/serious_python_android/CHANGELOG.md) by @FeodorFitsner. ### Bug fixes diff --git a/sdk/python/packages/flet-cli/src/flet_cli/commands/build_base.py b/sdk/python/packages/flet-cli/src/flet_cli/commands/build_base.py index cb485ee712..3f6ca5df98 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/commands/build_base.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/commands/build_base.py @@ -1174,6 +1174,9 @@ def create_flutter_project(self, second_pass=False): template_ref = flet.version.flet_version is_local_dev = False + # Identity printed in status / hashed for invalidation; may differ from + # the path cookiecutter actually reads when caching kicks in below. + template_source = template_url if template_url: # User-provided template (git repo or local path) — use checkout checkout = template_ref @@ -1182,13 +1185,19 @@ def create_flutter_project(self, second_pass=False): local_tpl = Path(__file__).resolve().parents[5] / "templates" / "build" if local_tpl.is_dir(): template_url = str(local_tpl) + template_source = template_url checkout = None is_local_dev = True else: - template_url = DEFAULT_TEMPLATE_URL.format(version=template_ref) + from flet_cli.utils.template_cache import get_cached_template_zip + + template_source = DEFAULT_TEMPLATE_URL.format(version=template_ref) + template_url = str( + get_cached_template_zip(template_source, template_ref) + ) checkout = None - hash.update(template_url) + hash.update(template_source) hash.update(template_ref) template_dir = self.options.template_dir or self.get_pyproject( @@ -1214,7 +1223,7 @@ def create_flutter_project(self, second_pass=False): # create a new Flutter bootstrap project directory, if non-existent if not second_pass: self.flutter_dir.mkdir(parents=True, exist_ok=True) - status = f"[bold blue]Creating app shell from {template_url}" + status = f"[bold blue]Creating app shell from {template_source}" if checkout: status += f' with ref "{template_ref}"' status += "..." diff --git a/sdk/python/packages/flet-cli/src/flet_cli/utils/template_cache.py b/sdk/python/packages/flet-cli/src/flet_cli/utils/template_cache.py new file mode 100644 index 0000000000..6ae62e6128 --- /dev/null +++ b/sdk/python/packages/flet-cli/src/flet_cli/utils/template_cache.py @@ -0,0 +1,48 @@ +import os +import shutil +import urllib.request +from pathlib import Path + + +def get_cache_root() -> Path: + """Resolve the Flet on-disk cache root. + + Uses `$FLET_CACHE_DIR` if set, otherwise `~/.flet/cache`. The resolved + path is exported back into the process environment so child processes + (notably the Gradle build of `serious_python_android`) share the same + cache root by default. + """ + root = os.environ.get("FLET_CACHE_DIR") + cache_root = Path(root).expanduser() if root else Path.home() / ".flet" / "cache" + os.environ["FLET_CACHE_DIR"] = str(cache_root) + return cache_root + + +def get_cached_template_zip(url: str, version: str) -> Path: + """Return a local path to `flet-build-template.zip` for `version`. + + The build template at a versioned release URL is immutable, so caching + is a simple "use if present, else download once" — no revalidation. + """ + cache_path = ( + get_cache_root() / "build-template" / f"v{version}" / "flet-build-template.zip" + ) + + if cache_path.exists() and cache_path.stat().st_size > 0: + return cache_path + + cache_path.parent.mkdir(parents=True, exist_ok=True) + tmp_path = cache_path.with_suffix(cache_path.suffix + ".tmp") + + try: + with urllib.request.urlopen(url) as resp, open(tmp_path, "wb") as out: + shutil.copyfileobj(resp, out) + out.flush() + os.fsync(out.fileno()) + os.replace(tmp_path, cache_path) + except BaseException: + if tmp_path.exists(): + tmp_path.unlink(missing_ok=True) + raise + + return cache_path diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml b/sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml index 8046108d96..243eed7c66 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml @@ -17,7 +17,7 @@ dependencies: flet: path: ../../../../../packages/flet - serious_python: 1.0.0 + serious_python: 1.0.1 package_info_plus: ^9.0.0 path_provider: ^2.1.4 From 50ef0d038dd5ebb347a0aec3a428301e2706b0ca Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Sat, 6 Jun 2026 12:11:31 -0700 Subject: [PATCH 2/2] Add PR #6558 link to CHANGELOG entries Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d74b7afec6..3fa24bd00f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,8 @@ * Allow `[tool.flet.android.permission]` values to be TOML inline tables in addition to booleans — each `key = "value"` entry adds an `android:=""` attribute to the generated `` element, unlocking modifiers like `android:maxSdkVersion` and `android:usesPermissionFlags` that real-world Android permissions (e.g. Bluetooth LE) require. The boolean form and the `--android-permissions` CLI flag are unchanged; a non-empty inline table is always emitted, an empty table (`{}`) is treated as `false`, and invalid value types fail the build with a clear error ([#6550](https://github.com/flet-dev/flet/issues/6550), [#6551](https://github.com/flet-dev/flet/pull/6551)) by @FeodorFitsner. * Upgrade the bundled Pyodide runtime in the `flet build web` template from `0.27.5` to `0.27.7` (includes `micropip` `0.9.0`) ([#6549](https://github.com/flet-dev/flet/pull/6549)) by @FeodorFitsner. * Drop generated `web/canvaskit/` build artifacts (`canvaskit.js`/`.wasm`/`.symbols` and the `chromium/` and `skwasm`/`skwasm_st` variants) from the `flet build web` template — these are produced by the Flutter web build and should not have been committed into the cookiecutter template ([#6549](https://github.com/flet-dev/flet/pull/6549)) by @FeodorFitsner. -* Cache the downloaded `flet-build-template.zip` across builds. The build template is bound to an exact Flet version and is immutable, so on every `flet build` / `flet debug` after the first, the CLI now uses a previously-downloaded zip from `$FLET_CACHE_DIR/build-template/v/` (defaulting to `~/.flet/cache/build-template/v/`) instead of re-fetching it via cookiecutter. The CLI also exports `FLET_CACHE_DIR` into the child Gradle process, so `serious_python_android` >= 1.0.1 lands its Python dist tarballs (`python-android-dart--.tar.gz`) in the same cache root by default — fixing the multi-minute "Creating app shell" / `downloadDistArchive_*` delay on every Android debug build. Custom `--template` URLs and the local-dev template path are unchanged ([#6555](https://github.com/flet-dev/flet/discussions/6555)) by @FeodorFitsner. -* Bump the bundled build template's `serious_python` dependency from `1.0.0` to `1.0.1` so Android builds pick up the new persistent Python-tarball cache + conditional-GET revalidation introduced in [`serious_python` 1.0.1](https://github.com/flet-dev/serious-python/blob/main/src/serious_python_android/CHANGELOG.md) by @FeodorFitsner. +* Cache the downloaded `flet-build-template.zip` across builds. The build template is bound to an exact Flet version and is immutable, so on every `flet build` / `flet debug` after the first, the CLI now uses a previously-downloaded zip from `$FLET_CACHE_DIR/build-template/v/` (defaulting to `~/.flet/cache/build-template/v/`) instead of re-fetching it via cookiecutter. The CLI also exports `FLET_CACHE_DIR` into the child Gradle process, so `serious_python_android` >= 1.0.1 lands its Python dist tarballs (`python-android-dart--.tar.gz`) in the same cache root by default — fixing the multi-minute "Creating app shell" / `downloadDistArchive_*` delay on every Android debug build. Custom `--template` URLs and the local-dev template path are unchanged ([#6555](https://github.com/flet-dev/flet/discussions/6555), [#6558](https://github.com/flet-dev/flet/pull/6558)) by @FeodorFitsner. +* Bump the bundled build template's `serious_python` dependency from `1.0.0` to `1.0.1` so Android builds pick up the new persistent Python-tarball cache + conditional-GET revalidation introduced in [`serious_python` 1.0.1](https://github.com/flet-dev/serious-python/blob/main/src/serious_python_android/CHANGELOG.md) ([#6558](https://github.com/flet-dev/flet/pull/6558)) by @FeodorFitsner. ### Bug fixes