diff --git a/CHANGELOG.md b/CHANGELOG.md index aa4c8d092d..955bc603e3 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` 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/_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 d5d44a5223..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 @@ -42,6 +42,18 @@ "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. 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): """ @@ -505,6 +517,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 +2058,26 @@ 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. 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 + or self.get_pyproject( + f"tool.flet.{self.config_platform}.extract_packages" + ) + or self.get_pyproject("tool.flet.extract_packages") + or [] + ) + self.android_extract_packages = list( + dict.fromkeys(ANDROID_DEFAULT_EXTRACT_PACKAGES + user_extract_packages) + ) + if self.get_bool_setting(self.options.compile_app, "compile.app", False): package_args.append("--compile-app") @@ -2232,6 +2273,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/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 diff --git a/website/docs/publish/index.md b/website/docs/publish/index.md index 4a512776b4..5e597de6bd 100644 --- a/website/docs/publish/index.md +++ b/website/docs/publish/index.md @@ -695,6 +695,45 @@ 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. + +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 + +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]