diff --git a/.fvmrc b/.fvmrc index afd20432..efcdf3cd 100644 --- a/.fvmrc +++ b/.fvmrc @@ -1,3 +1,3 @@ { - "flutter": "3.44.2" + "flutter": "3.44.3" } \ No newline at end of file diff --git a/src/serious_python/CHANGELOG.md b/src/serious_python/CHANGELOG.md index ab930e35..9fe964c7 100644 --- a/src/serious_python/CHANGELOG.md +++ b/src/serious_python/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.1.0 + +* **Android:** run first-launch asset unpacking and native library loading off the platform main thread so they no longer block vsync — boot-time animations (e.g. a splash / boot screen spinner) stay smooth while the app starts. Also ship consumer ProGuard rules that keep the pyjnius bootstrap classes, fixing pyjnius in release (minified) Android builds. See `serious_python_android` 4.1.0. + ## 4.0.0 * **App packaging lifted into serious_python.** Your Python app now ships **unpacked inside the application bundle**, next to the Python stdlib and site-packages, on macOS / iOS / Windows / Linux — no first-launch `app.zip` extraction. On **Android** the app ships as a *stored* `app.zip` asset inside the APK and is unpacked once (version-keyed) to the app-support files dir on the first launch after an install/update, like the existing `extract.zip`. Web (Pyodide) is unchanged. The `package` command stages the processed app into **`SERIOUS_PYTHON_APP`** (symmetric with `SERIOUS_PYTHON_SITE_PACKAGES`); each platform's native build copies it into the bundle (Android zips it as a stored asset). diff --git a/src/serious_python/pubspec.yaml b/src/serious_python/pubspec.yaml index 8839d397..3ec931c6 100644 --- a/src/serious_python/pubspec.yaml +++ b/src/serious_python/pubspec.yaml @@ -2,7 +2,7 @@ name: serious_python description: A cross-platform plugin for adding embedded Python runtime to your Flutter apps. homepage: https://flet.dev repository: https://github.com/flet-dev/serious-python -version: 4.0.0 +version: 4.1.0 platforms: ios: diff --git a/src/serious_python_android/CHANGELOG.md b/src/serious_python_android/CHANGELOG.md index f6a44a9a..54eafbf5 100644 --- a/src/serious_python_android/CHANGELOG.md +++ b/src/serious_python_android/CHANGELOG.md @@ -1,3 +1,9 @@ +## 4.1.0 + +* Run the `extractAsset` / `unzipAsset` / `loadLibrary` method-channel handlers on a background `Executor` (posting the `Result` back on the main looper) instead of inline on the platform main thread. The first-launch asset unpack and the pyjnius native-library load no longer block Android's `Choreographer`, so Flutter's vsync isn't starved and on-screen animations (e.g. a boot/splash spinner) stay smooth while the app starts. +* Ship consumer ProGuard/R8 keep rules (`-keep class com.flet.serious_python_android.** { *; }`) so release (minified) builds don't obfuscate or strip the classes pyjnius resolves by name at runtime. Without them R8 renamed `PythonActivity` and dropped its static `mActivity` field, breaking pyjnius in release builds with `type object 'C.f' has no attribute 'mActivity'` (debug builds were unaffected). +* Version bump aligning with the `serious_python_*` 4.1.0 release. + ## 4.0.0 * Ship the app as a *stored* `app.zip` asset in the APK and unpack it once (version-keyed) to `/flet/app` on the first launch after install/update, via the new `prepareApp()`. The version-keyed unpack moved out of `run()`; user data in the sibling `/data` is preserved across updates. diff --git a/src/serious_python_android/android/build.gradle.kts b/src/serious_python_android/android/build.gradle.kts index a34b3d28..dc41a973 100644 --- a/src/serious_python_android/android/build.gradle.kts +++ b/src/serious_python_android/android/build.gradle.kts @@ -21,7 +21,7 @@ buildscript { } group = "com.flet.serious_python_android" -version = "4.0.0" +version = "4.1.0" rootProject.allprojects { repositories { @@ -78,6 +78,10 @@ configure { ndk { abiFilters.addAll(abis) } + // Keep rules for classes pyjnius / the native runtime resolve by name at + // runtime (e.g. PythonActivity.mActivity); merged into the consuming app's + // R8 pass so release builds don't obfuscate/strip them. + consumerProguardFiles("consumer-rules.pro") } // No jniLibs packaging config needed: the native modules are real ELF .so that diff --git a/src/serious_python_android/android/consumer-rules.pro b/src/serious_python_android/android/consumer-rules.pro new file mode 100644 index 00000000..e7bb0afc --- /dev/null +++ b/src/serious_python_android/android/consumer-rules.pro @@ -0,0 +1,10 @@ +# Consumer ProGuard/R8 rules — automatically applied to apps that depend on +# serious_python_android. +# +# pyjnius and the native runtime look these classes (and their members) up by +# name via JNI/reflection at runtime. Without these keep rules, release-mode R8 +# minification renames/strips them — e.g. PythonActivity -> "C.f" and its static +# `mActivity` field is dropped — which breaks pyjnius with: +# pyjnius: not available on this platform - +# type object 'C.f' has no attribute 'mActivity' +-keep class com.flet.serious_python_android.** { *; } diff --git a/src/serious_python_android/android/src/main/java/com/flet/serious_python_android/AndroidPlugin.java b/src/serious_python_android/android/src/main/java/com/flet/serious_python_android/AndroidPlugin.java index e207744f..bb9c636b 100644 --- a/src/serious_python_android/android/src/main/java/com/flet/serious_python_android/AndroidPlugin.java +++ b/src/serious_python_android/android/src/main/java/com/flet/serious_python_android/AndroidPlugin.java @@ -5,6 +5,11 @@ import androidx.annotation.NonNull; import android.system.Os; import android.content.Intent; +import android.os.Handler; +import android.os.Looper; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; @@ -31,6 +36,25 @@ public class AndroidPlugin implements FlutterPlugin, MethodCallHandler, Activity private MethodChannel channel; private Context context; + // Heavy native work (asset extraction/unzipping, native library loading) must + // NOT run on the platform main thread: it would block Android's Choreographer + // and starve Flutter's vsync, freezing on-screen animations (e.g. the boot + // spinner). Run it on a background executor and post the MethodChannel result + // back on the main thread (Flutter requires result callbacks there). + private final ExecutorService ioExecutor = Executors.newSingleThreadExecutor(); + private final Handler mainHandler = new Handler(Looper.getMainLooper()); + + private void runAsync(@NonNull Result result, String errorCode, Callable work) { + ioExecutor.execute(() -> { + try { + Object value = work.call(); + mainHandler.post(() -> result.success(value)); + } catch (Throwable e) { + mainHandler.post(() -> result.error(errorCode, e.getMessage(), null)); + } + }); + } + @Override public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) { channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), @@ -107,19 +131,30 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { // Load a native library by name via Java's System.loadLibrary(), which — // unlike dart:ffi's dlopen-based DynamicLibrary.open used for // libdart_bridge — runs the library's JNI_OnLoad. That's how pyjnius's - // helper (libpyjni.so) captures the JavaVM + app ClassLoader. Called from - // app code here, so JNI_OnLoad sees the app's class loader. - try { - System.loadLibrary((String) call.argument("libname")); - result.success(null); - } catch (Throwable e) { - result.error("loadLibrary", e.getMessage(), null); - } + // helper (libpyjni.so) captures the JavaVM + app ClassLoader. + // + // Run off the main thread (dlopen + JNI_OnLoad can be slow). System.loadLibrary + // resolves the .so via the calling class's loader (AndroidPlugin -> app loader) + // regardless of thread, and JNI_OnLoad's FindClass uses that same loader; we + // also pin the worker's context loader to the app loader so JNI_OnLoad sees it + // if it reads the thread context loader. + final String libname = call.argument("libname"); + runAsync(result, "loadLibrary", () -> { + Thread t = Thread.currentThread(); + ClassLoader prev = t.getContextClassLoader(); + t.setContextClassLoader(context.getClassLoader()); + try { + System.loadLibrary(libname); + } finally { + t.setContextClassLoader(prev); + } + return null; + }); } else if (call.method.equals("extractAsset")) { // Stream an APK asset to disk as one whole file (e.g. stdlib.zip). - try { - String asset = call.argument("asset"); - String dest = call.argument("dest"); + final String asset = call.argument("asset"); + final String dest = call.argument("dest"); + runAsync(result, "extractAsset", () -> { java.io.File destFile = new java.io.File(dest); if (destFile.getParentFile() != null) destFile.getParentFile().mkdirs(); byte[] buf = new byte[1 << 16]; @@ -128,15 +163,13 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { int n; while ((n = in.read(buf)) > 0) out.write(buf, 0, n); } - result.success(dest); - } catch (Exception e) { - result.error("extractAsset", e.getMessage(), null); - } + return dest; + }); } else if (call.method.equals("unzipAsset")) { // Unpack an APK asset zip (e.g. extract.zip) into a directory tree. - try { - String asset = call.argument("asset"); - String destDir = call.argument("dest"); + final String asset = call.argument("asset"); + final String destDir = call.argument("dest"); + runAsync(result, "unzipAsset", () -> { java.io.File root = new java.io.File(destDir); byte[] buf = new byte[1 << 16]; try (java.io.InputStream in = context.getAssets().open(asset); @@ -155,10 +188,8 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { } } } - result.success(destDir); - } catch (Exception e) { - result.error("unzipAsset", e.getMessage(), null); - } + return destDir; + }); } else { result.notImplemented(); } @@ -167,6 +198,7 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { @Override public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { channel.setMethodCallHandler(null); + ioExecutor.shutdown(); } @Override diff --git a/src/serious_python_android/pubspec.yaml b/src/serious_python_android/pubspec.yaml index dcba567a..bef524ef 100644 --- a/src/serious_python_android/pubspec.yaml +++ b/src/serious_python_android/pubspec.yaml @@ -2,7 +2,7 @@ name: serious_python_android description: Android implementation of the serious_python plugin homepage: https://flet.dev repository: https://github.com/flet-dev/serious-python -version: 4.0.0 +version: 4.1.0 environment: sdk: ">=3.0.0 <4.0.0" diff --git a/src/serious_python_darwin/CHANGELOG.md b/src/serious_python_darwin/CHANGELOG.md index af7f33a2..7a977af4 100644 --- a/src/serious_python_darwin/CHANGELOG.md +++ b/src/serious_python_darwin/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.1.0 + +* Version bump aligning with the `serious_python_*` 4.1.0 release. + ## 4.0.0 * **Swift Package Manager support (dual with CocoaPods).** The plugin now builds under SPM as well as CocoaPods, so apps can use either integration (CocoaPods goes read-only in December 2026; Flutter ships SPM on by default since 3.44). A new `darwin/serious_python_darwin/Package.swift` builds the same Swift source as the podspec, with `getResourcePath` resolving `Bundle.module` under SPM (`#if SWIFT_PACKAGE`) and the framework `python.bundle` under CocoaPods. diff --git a/src/serious_python_darwin/darwin/serious_python_darwin.podspec b/src/serious_python_darwin/darwin/serious_python_darwin.podspec index a40434af..7c50571d 100644 --- a/src/serious_python_darwin/darwin/serious_python_darwin.podspec +++ b/src/serious_python_darwin/darwin/serious_python_darwin.podspec @@ -4,7 +4,7 @@ # Pod::Spec.new do |s| s.name = 'serious_python_darwin' - s.version = '4.0.0' + s.version = '4.1.0' s.summary = 'A cross-platform plugin for adding embedded Python runtime to your Flutter apps.' s.description = <<-DESC A cross-platform plugin for adding embedded Python runtime to your Flutter apps. diff --git a/src/serious_python_darwin/pubspec.yaml b/src/serious_python_darwin/pubspec.yaml index 8c2e09ee..b5c3c0cd 100644 --- a/src/serious_python_darwin/pubspec.yaml +++ b/src/serious_python_darwin/pubspec.yaml @@ -2,7 +2,7 @@ name: serious_python_darwin description: iOS and macOS implementations of the serious_python plugin homepage: https://flet.dev repository: https://github.com/flet-dev/serious-python -version: 4.0.0 +version: 4.1.0 environment: # The Swift Package Manager build path needs Flutter 3.44 / Dart 3.11 (the diff --git a/src/serious_python_linux/CHANGELOG.md b/src/serious_python_linux/CHANGELOG.md index 4592a1b7..0cbc4374 100644 --- a/src/serious_python_linux/CHANGELOG.md +++ b/src/serious_python_linux/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.1.0 + +* Version bump aligning with the `serious_python_*` 4.1.0 release. + ## 4.0.0 * `prepareApp()` returns `/app`; the app's Python sources ship unpacked next to the bundled stdlib + site-packages (no first-launch extraction). The CMakeLists copies `SERIOUS_PYTHON_APP` into the bundle. diff --git a/src/serious_python_linux/pubspec.yaml b/src/serious_python_linux/pubspec.yaml index 008cc136..25fec40e 100644 --- a/src/serious_python_linux/pubspec.yaml +++ b/src/serious_python_linux/pubspec.yaml @@ -2,7 +2,7 @@ name: serious_python_linux description: Linux implementations of the serious_python plugin homepage: https://flet.dev repository: https://github.com/flet-dev/serious-python -version: 4.0.0 +version: 4.1.0 environment: sdk: '>=3.1.3 <4.0.0' diff --git a/src/serious_python_platform_interface/CHANGELOG.md b/src/serious_python_platform_interface/CHANGELOG.md index 9c171132..913edd0a 100644 --- a/src/serious_python_platform_interface/CHANGELOG.md +++ b/src/serious_python_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.1.0 + +* Version bump aligning with the `serious_python_*` 4.1.0 release. + ## 4.0.0 * Add `prepareApp()` to `SeriousPythonPlatform` — materializes the packaged app on disk (if needed) and returns the directory containing its entry point. diff --git a/src/serious_python_platform_interface/pubspec.yaml b/src/serious_python_platform_interface/pubspec.yaml index 6d8c433d..be4f127c 100644 --- a/src/serious_python_platform_interface/pubspec.yaml +++ b/src/serious_python_platform_interface/pubspec.yaml @@ -2,7 +2,7 @@ name: serious_python_platform_interface description: A common platform interface for the serious_python plugin. homepage: https://flet.dev repository: https://github.com/flet-dev/serious-python -version: 4.0.0 +version: 4.1.0 environment: sdk: ">=3.0.0 <4.0.0" diff --git a/src/serious_python_windows/CHANGELOG.md b/src/serious_python_windows/CHANGELOG.md index 13c9482f..9e51d374 100644 --- a/src/serious_python_windows/CHANGELOG.md +++ b/src/serious_python_windows/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.1.0 + +* Version bump aligning with the `serious_python_*` 4.1.0 release. + ## 4.0.0 * `prepareApp()` returns `/app`; the app's Python sources ship unpacked next to the bundled CPython `Lib`/`DLLs`/`site-packages` (no first-launch extraction). The CMakeLists copies `SERIOUS_PYTHON_APP` into the runner output. diff --git a/src/serious_python_windows/pubspec.yaml b/src/serious_python_windows/pubspec.yaml index 820c6b3e..f80be415 100644 --- a/src/serious_python_windows/pubspec.yaml +++ b/src/serious_python_windows/pubspec.yaml @@ -2,7 +2,7 @@ name: serious_python_windows description: Windows implementations of the serious_python plugin homepage: https://flet.dev repository: https://github.com/flet-dev/serious-python -version: 4.0.0 +version: 4.1.0 environment: sdk: '>=3.1.3 <4.0.0'