From da8d8390630a12f42ad0c618fbce009c3bd2fd54 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Tue, 16 Jun 2026 21:05:21 -0700 Subject: [PATCH 1/3] build(android): consume serious_python native-mmap packaging + extract-packages option - Build template: point serious_python git ref at android-native-mmap; drop the stale packaging{jniLibs{useLegacyPackaging=true; keepDebugSymbols}} block from the Android app build.gradle.kts (native modules now load mmap'd from the APK; modern packaging at minSdk 23+ is all that's needed). - flet-cli: add --android-extract-packages CLI flag + [tool.flet.android].extract_packages (cross-platform [tool.flet].extract_packages fallback), a built-in ANDROID_DEFAULT_EXTRACT_PACKAGES set (certifi), merged and exported as SERIOUS_PYTHON_ANDROID_EXTRACT_PACKAGES for the serious_python package step. Mirrors the existing --source-packages -> SERIOUS_PYTHON_ALLOW_SOURCE_DISTRIBUTIONS flow. --- .../src/flet_cli/commands/build_base.py | 39 +++++++++++++++++++ .../android/app/build.gradle.kts | 15 ++----- .../{{cookiecutter.out_dir}}/pubspec.yaml | 9 +++-- 3 files changed, 48 insertions(+), 15 deletions(-) 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 d5d44a5223..3a23122c31 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 @@ -42,6 +42,15 @@ "v{version}/flet-build-template.zip" ) +# Android (serious_python native-mmap packaging): pure Python ships in stored zips +# read via zipimport, which breaks packages that read bundled data through a real +# filesystem path (__file__ / pkg_resources) instead of importlib.resources. These +# are shipped extracted to disk by default; users extend the list via +# --android-extract-packages or [tool.flet.android].extract_packages. +ANDROID_DEFAULT_EXTRACT_PACKAGES = [ + "certifi", +] + class BaseBuildCommand(BaseFlutterCommand): """ @@ -505,6 +514,15 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None: default=[], help="The list of Python packages to install from source distributions", ) + parser.add_argument( + "--android-extract-packages", + dest="android_extract_packages", + nargs="+", + default=[], + help="Android only: Python packages (relative paths) to ship extracted " + "to disk instead of inside the app zip — for packages that read bundled " + "data via __file__ / pkg_resources rather than importlib.resources", + ) parser.add_argument( "--python-version", dest="python_version", @@ -2037,6 +2055,27 @@ def package_python_app(self): source_packages ) + # android-extract-packages: path-hungry packages shipped extracted to disk + # instead of inside the zip (serious_python Android native-mmap packaging). + # A built-in default set covers commonly-broken packages; the user list + # (CLI / pyproject) is merged on top. + if self.package_platform == "Android": + user_extract_packages = ( + self.options.android_extract_packages + or self.get_pyproject( + f"tool.flet.{self.config_platform}.extract_packages" + ) + or self.get_pyproject("tool.flet.extract_packages") + or [] + ) + extract_packages = list( + dict.fromkeys(ANDROID_DEFAULT_EXTRACT_PACKAGES + user_extract_packages) + ) + if extract_packages: + package_env["SERIOUS_PYTHON_ANDROID_EXTRACT_PACKAGES"] = ",".join( + extract_packages + ) + if self.get_bool_setting(self.options.compile_app, "compile.app", False): package_args.append("--compile-app") diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/android/app/build.gradle.kts b/sdk/python/templates/build/{{cookiecutter.out_dir}}/android/app/build.gradle.kts index fbc14d0f87..7a75bc2714 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/android/app/build.gradle.kts +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/android/app/build.gradle.kts @@ -23,17 +23,10 @@ android { compileSdk = flutter.compileSdkVersion ndkVersion = flutter.ndkVersion - packaging { - jniLibs { - useLegacyPackaging = true - keepDebugSymbols += listOf( - "*/arm64-v8a/libpython*.so", - "*/armeabi-v7a/libpython*.so", - "*/x86/libpython*.so", - "*/x86_64/libpython*.so", - ) - } - } + // No native-library packaging config is needed: serious_python loads Python + // extension modules directly from the APK (memory-mapped) and ships pure + // Python in stored asset zips. Modern packaging (the default at minSdk 23+) + // is all that's required. compileOptions { sourceCompatibility = JavaVersion.VERSION_17 diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml b/sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml index ec2dba4660..c67d74dc08 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml @@ -17,13 +17,14 @@ dependencies: flet: path: ../../../../../packages/flet - # PythonBridge (in-process dart_bridge FFI transport) ships on the - # serious-python `dart-bridge` branch. Swap back to a version pin - # (`serious_python: ^2.0.0`) once it's published to pub.dev. + # PythonBridge (in-process dart_bridge FFI transport) + the Android + # native-mmap packaging ship on the serious-python `android-native-mmap` + # branch. Swap back to a version pin (`serious_python: ^2.0.0`) once it's + # published to pub.dev. serious_python: git: url: https://github.com/flet-dev/serious-python.git - ref: dart-bridge + ref: android-native-mmap path: src/serious_python # MsgPack codec used by the dart_bridge FletBackendChannel implementation From 379fe08f487c8ea4e3447b2ee90de557d1daa75f Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Tue, 16 Jun 2026 21:27:51 -0700 Subject: [PATCH 2/3] build(android): route extract-packages env to the flutter build + docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The serious_python_android Gradle split (which partitions site-packages into the stored sitepackages.zip vs the extracted extract.zip) runs during `flutter build`, not the `serious_python package` step. Set SERIOUS_PYTHON_ANDROID_EXTRACT_PACKAGES on build_env (resolved once into self.android_extract_packages) so the default set (certifi) and user list actually reach the split — previously it was set on the package step's env and silently ignored, leaving extract.zip empty. Add the "Android extract packages" publish-docs section (resolution order, CLI + pyproject example), a CHANGELOG entry, and allowlist `certifi` in typos. Verified end-to-end on an arm64 emulator via both `flet build apk` and `flet build aab` (bundletool split install): numpy mmap'd from the APK, certifi extracted to disk, flet icons.json read from the stored zip, flet.version resolved, app launched. --- CHANGELOG.md | 1 + sdk/python/_typos.toml | 2 ++ .../src/flet_cli/commands/build_base.py | 18 ++++++---- website/docs/publish/index.md | 35 +++++++++++++++++++ 4 files changed, 50 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa4c8d092d..7707e4c771 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ ### Improvements +* **Smaller Android apps with no native-packaging config.** `flet build apk`/`aab` consume serious_python's new Android packaging: Python extension modules load **memory-mapped directly from the APK** (no extraction to disk), and pure Python ships in stored asset zips read via `zipimport`, so the standard library is no longer duplicated per ABI. Apps no longer need `useLegacyPackaging` / `keepDebugSymbols` — the `flet build` Android template drops them; just use `minSdk 23+`. New `--android-extract-packages` flag and `[tool.flet.android].extract_packages` (plus a built-in default set, e.g. `certifi`) ship "path-hungry" packages — those that read bundled data via `__file__` / `pkg_resources` instead of `importlib.resources` — extracted to disk instead of inside the zip. Requires `serious_python` with the native-mmap packaging (dart_bridge 1.4.0). * Pyodide is no longer pre-baked into the `flet build` template. Each `flet build web` / `flet publish` run downloads the matching `pyodide-core-.tar.bz2` (plus the runtime `micropip` and `packaging` wheels) into a per-version cache at `~/.flet/pyodide//` and copies the files into the build output. Subsequent builds reuse the cache; the older `0.27.5` bundle previously shipped in the cookiecutter template is gone ([#6577](https://github.com/flet-dev/flet/pull/6577)) by @FeodorFitsner. * The supported Python / Pyodide / dart_bridge versions are loaded on demand from `flet-dev/python-build`'s date-keyed `manifest.json` (fetched once and cached under `~/.flet/cache`), the single source of truth shared with `serious_python` — replacing flet's hand-mirrored version table. `flet build` forwards only `SERIOUS_PYTHON_VERSION` and lets `serious_python` derive the full version / build date / dart_bridge version from its own committed snapshot. The module exposes `get_supported_python_versions()` / `get_default_python_version()` (the previous `SUPPORTED_PYTHON_VERSIONS` / `DEFAULT_PYTHON_VERSION` constants are removed) ([#6577](https://github.com/flet-dev/flet/pull/6577)) by @FeodorFitsner. * `flet --version` shows just the Flet and Flutter versions; the static `Pyodide: …` line and the global `flet.version.pyodide_version` export are removed (the supported Python / Pyodide set now lives in python-build's manifest, not the CLI output) ([#6577](https://github.com/flet-dev/flet/pull/6577)) by @FeodorFitsner. diff --git a/sdk/python/_typos.toml b/sdk/python/_typos.toml index e68027a320..b503150fa2 100644 --- a/sdk/python/_typos.toml +++ b/sdk/python/_typos.toml @@ -4,3 +4,5 @@ AACHE = "AACHE" ROUTEROS = "ROUTEROS" # OpenType variable font axis tag for width wdth = "wdth" +# Python package name (Mozilla CA bundle) +certifi = "certifi" 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 3a23122c31..2e9287ba5e 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 @@ -2058,7 +2058,10 @@ def package_python_app(self): # android-extract-packages: path-hungry packages shipped extracted to disk # instead of inside the zip (serious_python Android native-mmap packaging). # A built-in default set covers commonly-broken packages; the user list - # (CLI / pyproject) is merged on top. + # (CLI / pyproject) is merged on top. Consumed by the serious_python_android + # Gradle split during `flutter build`, so the env var is set on build_env + # (see _run_flutter_command), not on the package step. + self.android_extract_packages: list[str] = [] if self.package_platform == "Android": user_extract_packages = ( self.options.android_extract_packages @@ -2068,13 +2071,9 @@ def package_python_app(self): or self.get_pyproject("tool.flet.extract_packages") or [] ) - extract_packages = list( + self.android_extract_packages = list( dict.fromkeys(ANDROID_DEFAULT_EXTRACT_PACKAGES + user_extract_packages) ) - if extract_packages: - package_env["SERIOUS_PYTHON_ANDROID_EXTRACT_PACKAGES"] = ",".join( - extract_packages - ) if self.get_bool_setting(self.options.compile_app, "compile.app", False): package_args.append("--compile-app") @@ -2271,6 +2270,13 @@ def _run_flutter_command(self): self.build_dir / "site-packages" ) + # Path-hungry packages to ship extracted to disk: consumed by the + # serious_python_android Gradle split during `flutter build`. + if self.package_platform == "Android" and self.android_extract_packages: + build_env["SERIOUS_PYTHON_ANDROID_EXTRACT_PACKAGES"] = ",".join( + self.android_extract_packages + ) + if self.package_platform == "Emscripten" and not self.template_data["no_wasm"]: build_args.append("--wasm") diff --git a/website/docs/publish/index.md b/website/docs/publish/index.md index 4a512776b4..5dec0b978d 100644 --- a/website/docs/publish/index.md +++ b/website/docs/publish/index.md @@ -695,6 +695,41 @@ source_packages = ["package1", "package2"] +### Android extract packages + +:::note +[Android](android.md) only. +::: + +On Android, pure Python ships in a stored zip read in place (`zipimport`) and native modules are +loaded memory-mapped from the APK. Packages that read their bundled **data files** through a real +filesystem path — `__file__` / `pkg_resources` instead of +[`importlib.resources`](https://docs.python.org/3/library/importlib.resources.html) — don't work +from inside the zip. List such "path-hungry" packages here to ship them **extracted to disk** +instead. A built-in default set (e.g. `certifi`) is always extracted; your list is merged on top. + +#### Resolution order + +1. [`--android-extract-packages`](../cli/flet-build.md#--android-extract-packages) +2. `[tool.flet.android].extract_packages` +3. `[tool.flet].extract_packages` + +#### Example + + + +```bash +flet build apk --android-extract-packages package1 package2 +``` + + +```toml +[tool.flet.android] +extract_packages = ["package1", "package2"] +``` + + + ### Icons :::note[Platform support] From 6693b32b1a11a6d95ec2e1ec4ed2d50c26370073 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Wed, 17 Jun 2026 07:49:45 -0700 Subject: [PATCH 3/3] =?UTF-8?q?build(android):=20empty=20the=20default=20e?= =?UTF-8?q?xtract=20set=20=E2=80=94=20certifi=20is=20zip-safe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit certifi reads cacert.pem via importlib.resources.as_file(), which extracts it to a temp file on demand, so certifi.where() works from inside the stored sitepackages.zip (verified: imported via zipimport, where() returns a valid 234KB cacert.pem). It does not need to ship extracted. Make ANDROID_DEFAULT_EXTRACT_PACKAGES empty — the common data-bundling packages use importlib.resources (zip-safe). The --android-extract-packages / [tool.flet.android].extract_packages mechanism stays for genuinely path-hungry packages (those reading data via __file__ / pkg_resources). Update docs and CHANGELOG accordingly. --- CHANGELOG.md | 2 +- .../flet-cli/src/flet_cli/commands/build_base.py | 15 +++++++++------ website/docs/publish/index.md | 6 +++++- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7707e4c771..955bc603e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ ### Improvements -* **Smaller Android apps with no native-packaging config.** `flet build apk`/`aab` consume serious_python's new Android packaging: Python extension modules load **memory-mapped directly from the APK** (no extraction to disk), and pure Python ships in stored asset zips read via `zipimport`, so the standard library is no longer duplicated per ABI. Apps no longer need `useLegacyPackaging` / `keepDebugSymbols` — the `flet build` Android template drops them; just use `minSdk 23+`. New `--android-extract-packages` flag and `[tool.flet.android].extract_packages` (plus a built-in default set, e.g. `certifi`) ship "path-hungry" packages — those that read bundled data via `__file__` / `pkg_resources` instead of `importlib.resources` — extracted to disk instead of inside the zip. Requires `serious_python` with the native-mmap packaging (dart_bridge 1.4.0). +* **Smaller Android apps with no native-packaging config.** `flet build apk`/`aab` consume serious_python's new Android packaging: Python extension modules load **memory-mapped directly from the APK** (no extraction to disk), and pure Python ships in stored asset zips read via `zipimport`, so the standard library is no longer duplicated per ABI. Apps no longer need `useLegacyPackaging` / `keepDebugSymbols` — the `flet build` Android template drops them; just use `minSdk 23+`. New `--android-extract-packages` flag and `[tool.flet.android].extract_packages` ship "path-hungry" packages — those that read bundled data via `__file__` / `pkg_resources` instead of `importlib.resources` — extracted to disk instead of inside the zip (most packages, including `certifi`, are zip-safe and need no entry). Requires `serious_python` with the native-mmap packaging (dart_bridge 1.4.0). * Pyodide is no longer pre-baked into the `flet build` template. Each `flet build web` / `flet publish` run downloads the matching `pyodide-core-.tar.bz2` (plus the runtime `micropip` and `packaging` wheels) into a per-version cache at `~/.flet/pyodide//` and copies the files into the build output. Subsequent builds reuse the cache; the older `0.27.5` bundle previously shipped in the cookiecutter template is gone ([#6577](https://github.com/flet-dev/flet/pull/6577)) by @FeodorFitsner. * The supported Python / Pyodide / dart_bridge versions are loaded on demand from `flet-dev/python-build`'s date-keyed `manifest.json` (fetched once and cached under `~/.flet/cache`), the single source of truth shared with `serious_python` — replacing flet's hand-mirrored version table. `flet build` forwards only `SERIOUS_PYTHON_VERSION` and lets `serious_python` derive the full version / build date / dart_bridge version from its own committed snapshot. The module exposes `get_supported_python_versions()` / `get_default_python_version()` (the previous `SUPPORTED_PYTHON_VERSIONS` / `DEFAULT_PYTHON_VERSION` constants are removed) ([#6577](https://github.com/flet-dev/flet/pull/6577)) by @FeodorFitsner. * `flet --version` shows just the Flet and Flutter versions; the static `Pyodide: …` line and the global `flet.version.pyodide_version` export are removed (the supported Python / Pyodide set now lives in python-build's manifest, not the CLI output) ([#6577](https://github.com/flet-dev/flet/pull/6577)) by @FeodorFitsner. 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 2e9287ba5e..e29a484980 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 @@ -44,12 +44,15 @@ # Android (serious_python native-mmap packaging): pure Python ships in stored zips # read via zipimport, which breaks packages that read bundled data through a real -# filesystem path (__file__ / pkg_resources) instead of importlib.resources. These -# are shipped extracted to disk by default; users extend the list via -# --android-extract-packages or [tool.flet.android].extract_packages. -ANDROID_DEFAULT_EXTRACT_PACKAGES = [ - "certifi", -] +# filesystem path (__file__ / pkg_resources) instead of importlib.resources. Such +# packages are shipped extracted to disk via --android-extract-packages or +# [tool.flet.android].extract_packages. +# +# The default set is empty: the common offenders read their data via +# importlib.resources, which is zip-safe (e.g. certifi.where() works from the zip — +# importlib.resources.as_file() extracts cacert.pem to a temp file on demand). Add +# real offenders here as they are found. +ANDROID_DEFAULT_EXTRACT_PACKAGES: list[str] = [] class BaseBuildCommand(BaseFlutterCommand): diff --git a/website/docs/publish/index.md b/website/docs/publish/index.md index 5dec0b978d..5e597de6bd 100644 --- a/website/docs/publish/index.md +++ b/website/docs/publish/index.md @@ -706,7 +706,11 @@ loaded memory-mapped from the APK. Packages that read their bundled **data files filesystem path — `__file__` / `pkg_resources` instead of [`importlib.resources`](https://docs.python.org/3/library/importlib.resources.html) — don't work from inside the zip. List such "path-hungry" packages here to ship them **extracted to disk** -instead. A built-in default set (e.g. `certifi`) is always extracted; your list is merged on top. +instead. + +Most packages that bundle data (including `certifi`) read it through `importlib.resources`, which +is zip-safe, so they need no entry here — only add packages that actually fail to find their data +when imported from the zip. #### Resolution order