From 79df82e1205d0d64a07be41181d376a795a4432f Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Mon, 6 Jul 2026 21:28:52 +0200 Subject: [PATCH 01/19] feat(build): macOS runner intercepts multiprocessing child re-execs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Python's multiprocessing (spawn/forkserver start methods and the resource tracker) launches child processes by re-executing sys.executable with a CPython command line — and in a flet-built app sys.executable is the app binary itself. Each worker therefore booted a full Flutter GUI instance that treated '-B' as a dev-mode page URL, never ran the spawn payload, and never exited: one stray window per worker, hung pools, and "resource_tracker: process died unexpectedly, relaunching" loops. Add macos/Runner/main.swift as the explicit entry point (AppDelegate drops @main; the xib still instantiates and wires the delegate): before NSApplicationMain, it checks argv against dart_bridge >= 1.5.0's serious_python_is_mp_invocation and, on a match, exits with serious_python_main's return code — the embedded interpreter services the child headlessly (Py_BytesMain) with no AppKit/Flutter initialization. The child resolves the bundled stdlib/site-packages via the PYTHONHOME/PYTHONPATH the parent already setenv'd process-wide. Both entry points are resolved via dlsym from the current process image (dart_bridge is a static archive force-loaded into the host binary), so apps built against an older dart_bridge still link and launch — they just don't get the interception. Verified end-to-end on macOS with an instrumented probe app: Process+Queue round-trip with a main.py-defined worker, and a ProcessPoolExecutor run (4 workers, reused across 8 tasks) — headless children, correct results, ~4x speedup over sequential, no leftover processes. --- .../macos/Runner.xcodeproj/project.pbxproj | 4 ++ .../macos/Runner/AppDelegate.swift | 3 +- .../macos/Runner/main.swift | 38 +++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 sdk/python/templates/build/{{cookiecutter.out_dir}}/macos/Runner/main.swift diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/macos/Runner.xcodeproj/project.pbxproj b/sdk/python/templates/build/{{cookiecutter.out_dir}}/macos/Runner.xcodeproj/project.pbxproj index bb413260d2..bbcba4a34e 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/macos/Runner.xcodeproj/project.pbxproj +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/macos/Runner.xcodeproj/project.pbxproj @@ -27,6 +27,7 @@ 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F12044A3C60003C046 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C046 /* main.swift */; }; 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; @@ -69,6 +70,7 @@ 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; 33CC10ED2044A3C60003C045 /* {{ cookiecutter.artifact_name }}.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "{{ cookiecutter.artifact_name }}.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F02044A3C60003C046 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; @@ -166,6 +168,7 @@ isa = PBXGroup; children = ( 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC10F02044A3C60003C046 /* main.swift */, 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, 33E51913231747F40026EE4D /* DebugProfile.entitlements */, 33E51914231749380026EE4D /* Release.entitlements */, @@ -370,6 +373,7 @@ files = ( 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 33CC10F12044A3C60003C046 /* main.swift in Sources */, 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/macos/Runner/AppDelegate.swift b/sdk/python/templates/build/{{cookiecutter.out_dir}}/macos/Runner/AppDelegate.swift index b3c1761412..1c86ba9ccc 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/macos/Runner/AppDelegate.swift +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/macos/Runner/AppDelegate.swift @@ -1,7 +1,8 @@ import Cocoa import FlutterMacOS -@main +// See main.swift for the entry point. + class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/macos/Runner/main.swift b/sdk/python/templates/build/{{cookiecutter.out_dir}}/macos/Runner/main.swift new file mode 100644 index 0000000000..7873ebf383 --- /dev/null +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/macos/Runner/main.swift @@ -0,0 +1,38 @@ +import Cocoa + +// Python's multiprocessing spawn/forkserver paths, including the resource +// tracker, create child processes by re-executing sys.executable with a +// CPython-style command line. In a flet-built macOS app, sys.executable is this +// app binary. Detect that argv shape before AppKit/Flutter initialization and +// run the embedded interpreter headlessly instead of starting another GUI app. +// See: https://github.com/flet-dev/flet/issues/4283 +// +// These optional entry points live in dart_bridge, which is force-loaded into +// the host binary by serious_python_darwin. Resolve them dynamically from the +// current process image, similar to Dart FFI's DynamicLibrary.process(), so apps +// built with older dart_bridge versions still link and launch; they just won't +// have multiprocessing child interception. +private typealias SPArgvFn = @convention(c) ( + Int32, UnsafeMutablePointer?>? +) -> Int32 + +private let processHandle = dlopen(nil, RTLD_NOW) + +private func spResolve(_ name: String) -> SPArgvFn? { + guard let processHandle, + let sym = dlsym(processHandle, name) else { + return nil + } + return unsafeBitCast(sym, to: SPArgvFn.self) +} + +if let isMpInvocation = spResolve("serious_python_is_mp_invocation"), + let runPythonMain = spResolve("serious_python_main"), + isMpInvocation(CommandLine.argc, CommandLine.unsafeArgv) != 0 +{ + exit(runPythonMain(CommandLine.argc, CommandLine.unsafeArgv)) +} + +// Not a multiprocessing child process, or the interception hooks are unavailable: +// continue with the normal macOS Flutter app launch. +_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv) From 8a06e440a6ff1570784235a996e2c5be51b84421 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Mon, 6 Jul 2026 21:29:31 +0200 Subject: [PATCH 02/19] feat(build): Windows runner intercepts multiprocessing child re-execs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same mechanism as the macOS runner: multiprocessing spawn children (and the resource tracker) re-execute sys.executable — the app .exe — with a CPython command line, which previously booted one GUI window per worker in dev-mode and never ran the spawn payload. At the top of wWinMain, before any console/COM/window/Flutter work, MaybeRunPythonChild() parses the wide command line (CommandLineToArgvW), loads dart_bridge.dll (falling back to dart_bridge_d.dll for debug-CRT builds), and resolves the wide-char entry points serious_python_is_mp_invocation_w / serious_python_main_w (dart_bridge >= 1.5.0; wide variants because decoding wWinMain argv through the ANSI code page would be lossy — serious_python_main_w wraps Py_Main). On a match, the process runs as a headless interpreter and returns its exit code. Resolution is dynamic (GetProcAddress with graceful fallthrough), so apps built against an older dart_bridge launch unchanged. Eagerly loading the DLL costs nothing on the normal startup path — the plugin loads it moments later anyway. Note: not yet runtime-verified on Windows (the macOS equivalent is verified end-to-end); needs a probe run on a Windows machine. --- .../windows/runner/main.cpp | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/windows/runner/main.cpp b/sdk/python/templates/build/{{cookiecutter.out_dir}}/windows/runner/main.cpp index 7c91006a81..67a07c56f9 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/windows/runner/main.cpp +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/windows/runner/main.cpp @@ -1,12 +1,68 @@ #include #include +#include #include #include "flutter_window.h" #include "utils.h" +// Python's multiprocessing spawn path, including the resource tracker, creates +// child processes by re-executing sys.executable with a CPython-style command +// line. In a flet-built Windows app, sys.executable is this app binary. Detect +// that argv shape before Win32 window/COM/Flutter initialization and run the +// embedded interpreter headlessly instead of starting another GUI app. +// See: https://github.com/flet-dev/flet/issues/4283 +// +// These optional entry points live in dart_bridge.dll. Load the bridge early +// and resolve them dynamically so apps built with older dart_bridge versions +// still launch; they just won't have multiprocessing child interception. +// +// Returns true when this process was handled as a multiprocessing child. In +// that case, exit_code receives the interpreter process exit code. +static bool MaybeRunPythonChild(int& exit_code) { + using SpArgvFn = int (*)(int, wchar_t **); + + int argc = 0; + wchar_t **argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (!argv) { + return false; + } + + bool handled = false; + + // The DLL, and its python3XX.dll dependency, would be loaded moments later by + // the plugin anyway, so eagerly loading it here does not add meaningful cost + // to the normal startup path. + HMODULE bridge = ::LoadLibraryW(L"dart_bridge.dll"); + if (!bridge) { + bridge = ::LoadLibraryW(L"dart_bridge_d.dll"); + } + + if (bridge) { + auto is_mp_invocation = reinterpret_cast( + ::GetProcAddress(bridge, "serious_python_is_mp_invocation_w")); + auto run_python_main = reinterpret_cast( + ::GetProcAddress(bridge, "serious_python_main_w")); + + if (is_mp_invocation && run_python_main && + is_mp_invocation(argc, argv) != 0) { + exit_code = run_python_main(argc, argv); + handled = true; + } + } + + ::LocalFree(argv); + return handled; +} + int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { + // Multiprocessing child re-exec? Run it headlessly and exit, instead of starting Flutter GUI. + int mp_exit_code = 0; + if (MaybeRunPythonChild(mp_exit_code)) { + return mp_exit_code; + } + // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { From 955fbbea53842c6d7cda6b9d4d5cbe99bd14aca1 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Mon, 6 Jul 2026 21:29:47 +0200 Subject: [PATCH 03/19] feat(build): Linux runner intercepts multiprocessing child re-execs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same mechanism as the macOS/Windows runners, with an extra reason to care on Linux: Python 3.14 changed the default start method from fork to forkserver (gh-84559), and the forkserver's server process is launched through the same sys.executable re-exec as spawn — so Linux apps that previously happened to work via raw fork break by default on the bundled 3.14. At the top of main(), before any GTK/Flutter initialization, maybe_run_python_child() dlopen()s libdart_bridge.so — resolved through the runner's $ORIGIN/lib RUNPATH, exactly the way the Dart FFI's DynamicLibrary.open() finds it later — and resolves serious_python_is_mp_invocation / serious_python_main (dart_bridge >= 1.5.0). On a match, the process runs as a headless interpreter and returns its exit code. Resolution is dynamic with graceful fallthrough, so apps built against an older dart_bridge launch unchanged. ${CMAKE_DL_LIBS} is added to the runner link for dlopen/dlsym (a no-op with glibc >= 2.34, where libdl is merged into libc). Note: not yet runtime-verified on Linux (the macOS equivalent is verified end-to-end); needs a probe run on a Linux machine. --- .../linux/CMakeLists.txt | 3 ++ .../{{cookiecutter.out_dir}}/linux/main.cc | 46 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/linux/CMakeLists.txt b/sdk/python/templates/build/{{cookiecutter.out_dir}}/linux/CMakeLists.txt index c72838044f..208fce2ac7 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/linux/CMakeLists.txt +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/linux/CMakeLists.txt @@ -73,6 +73,9 @@ apply_standard_settings(${BINARY_NAME}) # Add dependency libraries. Add any application-specific dependencies here. target_link_libraries(${BINARY_NAME} PRIVATE flutter) target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) +# dlopen/dlsym for main.cc's multiprocessing child interception +# (a no-op with glibc >= 2.34, where libdl is merged into libc). +target_link_libraries(${BINARY_NAME} PRIVATE ${CMAKE_DL_LIBS}) # Run the Flutter tool portions of the build. This must not be removed. add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/linux/main.cc b/sdk/python/templates/build/{{cookiecutter.out_dir}}/linux/main.cc index e7c5c54370..f89420aa49 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/linux/main.cc +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/linux/main.cc @@ -1,6 +1,52 @@ #include "my_application.h" +#include + +// Python's multiprocessing spawn/forkserver paths, including the resource +// tracker, create child processes by re-executing sys.executable with a +// CPython-style command line. In a flet-built Linux app, sys.executable is this +// app binary. Detect that argv shape before GTK/Flutter initialization and run +// the embedded interpreter headlessly instead of starting another GUI app. +// See: https://github.com/flet-dev/flet/issues/4283 +// +// These optional entry points live in libdart_bridge.so. Load the bridge early +// and resolve them dynamically so apps built with older dart_bridge versions +// still launch; they just won't have multiprocessing child interception. +// +// Returns true when this process was handled as a multiprocessing child. In +// that case, exit_code receives the interpreter process exit code. +static bool maybe_run_python_child(int argc, char** argv, int& exit_code) { + typedef int (*sp_argv_fn)(int, char**); + + // The shared library, and its Python dependency, would be loaded moments + // later by the plugin anyway, so eagerly loading it here does not add + // meaningful cost to the normal startup path. + void* bridge = dlopen("libdart_bridge.so", RTLD_NOW); + if (!bridge) { + return false; + } + + auto is_mp_invocation = reinterpret_cast( + dlsym(bridge, "serious_python_is_mp_invocation")); + auto run_python_main = + reinterpret_cast(dlsym(bridge, "serious_python_main")); + + if (is_mp_invocation && run_python_main && + is_mp_invocation(argc, argv) != 0) { + exit_code = run_python_main(argc, argv); + return true; + } + + return false; +} + int main(int argc, char** argv) { + // Multiprocessing child re-exec? Run it headlessly and exit, instead of starting Flutter GUI. + int mp_exit_code = 0; + if (maybe_run_python_child(argc, argv, mp_exit_code)) { + return mp_exit_code; + } + g_autoptr(MyApplication) app = my_application_new(); return g_application_run(G_APPLICATION(app), argc, argv); } From ba857212d907ef6fd37c86b72a185b6a104d6052 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Mon, 6 Jul 2026 21:30:10 +0200 Subject: [PATCH 04/19] fix(build): run the app module as the real __main__; point multiprocessing at the host binary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two independent multiprocessing breakages lived in the boot script, and either alone was fatal even with the runners' child interception in place: 1. __main__ identity. runpy.run_module(run_name="__main__") executes the app module in a scratch namespace that is never installed in sys.modules, so pickling any function defined in the app module failed IN THE PARENT with "Can't pickle : it's not found as __main__." — before a child was ever spawned. Replace it with _sp_run_module_as_main(): resolve the module spec (with `python -m` package semantics — pkg runs pkg.__main__ — and clear ImportErrors for loaderless/codeless specs), build a fresh module with __spec__/__file__/__cached__/__loader__/__package__ set, install it as sys.modules["__main__"], and exec the module code in its dict. Spawn children then re-import the app module as __mp_main__ via the standard init_main_from_name path — which is why the documented `if __name__ == "__main__":` guard around ft.run() is now mandatory for multiprocessing users, exactly as in plain CPython. The module is also aliased as sys.modules["__mp_main__"] in the parent, matching what multiprocessing does in children: objects defined in the app module and pickled by a child carry __module__ == "__mp_main__", and without the alias the parent fails to unpickle them (ModuleNotFoundError: __mp_main__). 2. Re-exec target. sys.executable / sys._base_executable are set to the host binary (new {host_executable} placeholder, filled from Platform.resolvedExecutable in native_runtime.dart; JSON string literals are valid Python string literals). On macOS/Windows CPython already computes the host binary itself, but on Linux bare Py_Initialize() PATH-guesses an unrelated "python3" (or none) — this makes the target the runner binary, whose argv interception services the children, on all three desktops deterministically. Set before any user import because multiprocessing snapshots sys.executable at import time. PYTHONINSPECT is also dropped from the inherited environment: it did nothing in the embedded parent, but a real interpreter child would stay open in interactive mode after its -c command completed. (Defense in depth — serious_python >= 4.3.0 stops setting it and dart_bridge's serious_python_main unsets it too.) Verified end-to-end on macOS together with the runner interception; the rendered boot script is byte-checked to compile after all placeholder substitutions. --- .../lib/native_runtime.dart | 4 +- .../{{cookiecutter.out_dir}}/lib/python.dart | 77 +++++++++++++++++-- 2 files changed, 74 insertions(+), 7 deletions(-) diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/native_runtime.dart b/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/native_runtime.dart index edb09ff6a9..4673850aee 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/native_runtime.dart +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/native_runtime.dart @@ -11,6 +11,7 @@ // of `kIsWeb` runtime gates. import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; @@ -109,7 +110,8 @@ Future runPython({ var script = pythonScript .replaceAll("{outLogFilename}", outLogFilename.replaceAll("\\", "\\\\")) .replaceAll('{module_name}', moduleName) - .replaceAll('{argv}', argv); + .replaceAll('{argv}', argv) + .replaceAll('{host_executable}', jsonEncode(Platform.resolvedExecutable)); var completer = Completer(); diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/python.dart b/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/python.dart index 1065d7b89f..07af2d8837 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/python.dart +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/python.dart @@ -1,7 +1,8 @@ const errorExitCode = 100; -const pythonScript = """ -import os, runpy, sys, traceback +const pythonScript = + """ +import importlib.util, os, sys, traceback, types # fix for cryptography package os.environ["CRYPTOGRAPHY_OPENSSL_NO_LEGACY"] = "1" @@ -53,6 +54,7 @@ def initialize_ctypes(): ctypes.CDLL.__init__ = CDLL_init_override + initialize_ctypes() out_file = open("{outLogFilename}", "w+", buffering=1) @@ -66,6 +68,7 @@ out_file = open("{outLogFilename}", "w+", buffering=1) _native_stdout = sys.stdout _native_stderr = sys.stderr + class _TeeWriter: # The file half receives writes raw — preserves byte-for-byte parity # with what Python wrote, so the error-screen capture file matches a @@ -82,6 +85,7 @@ class _TeeWriter: self._native = native self._file = file_ self._native_buf = "" + def write(self, text): if not text: return 0 @@ -98,6 +102,7 @@ class _TeeWriter: except Exception: pass return self._file.write(text) + def flush(self): try: # Drain any pending partial line (no trailing newline in the @@ -110,8 +115,10 @@ class _TeeWriter: except Exception: pass self._file.flush() + def isatty(self): return False + def fileno(self): return self._file.fileno() @@ -121,8 +128,8 @@ sys.stderr = _TeeWriter(_native_stderr, out_file) # Exit-code transport. The Dart side allocated a dedicated PythonBridge port # (FLET_DART_BRIDGE_EXIT_PORT) and is listening on it. `flet_exit` posts the # exit code as raw UTF-8 bytes through that bridge — the Dart side parses, -# then either renders the error screen (code == 100) or terminates the host -# process (any other code) using the file we wrote to above. +# then either renders the error screen (code == $errorExitCode) or terminates +# the host process (any other code) using the file we wrote to above. # # On Android process reuse (Dart VM restarts while libdart_bridge stays # loaded), the exit-bridge port number changes. We keep `_exit_port` in a @@ -152,6 +159,53 @@ if _add_restart is not None: _exit_port[0] = int(new_exit) _add_restart(_on_dart_session_restart) + +def _sp_run_module_as_main(module_name): + # Execute with `python -m module_name` semantics, but inside the real + # sys.modules["__main__"]. `runpy.run_module(..., run_name="__main__")` + # isn't used here, as it uses a temporary namespace, causing pickle/multiprocessing + # not to reliably resolve top-level functions from the app module. + spec = importlib.util.find_spec(module_name) + if spec is None: + raise ImportError("module %r not found" % module_name) + + if spec.submodule_search_locations is not None: + # Package case: `python -m pkg` executes pkg.__main__. + main_name = module_name + ".__main__" + spec = importlib.util.find_spec(main_name) + if spec is None: + raise ImportError( + "%r is a package and cannot be directly executed: " + "no %r module" % (module_name, main_name) + ) + + if spec.loader is None or not hasattr(spec.loader, "get_code"): + raise ImportError("module %r cannot be executed" % module_name) + + code = spec.loader.get_code(spec.name) + if code is None: + raise ImportError("module %r has no executable Python code" % module_name) + + main = types.ModuleType("__main__") + main.__dict__.update( + __spec__=spec, + __file__=spec.origin, + __cached__=spec.cached, + __loader__=spec.loader, + __package__=spec.parent, + __builtins__=__builtins__, + ) + + sys.modules["__main__"] = main + sys.modules["__mp_main__"] = main # Match multiprocessing's spawn alias for the main module. + + # Do not let re-exec'd multiprocessing children inherit PYTHONINSPECT, or + # they may stay open in interactive mode after their command finishes. + os.environ.pop("PYTHONINSPECT", None) + + exec(code, main.__dict__) + + ex = None try: import certifi @@ -172,10 +226,21 @@ try: ssl._create_default_https_context = create_default_context sys.argv = {argv} - runpy.run_module("{module_name}", run_name="__main__") + + # multiprocessing spawn/forkserver children re-execute sys.executable. In a + # packaged Flet app, that must be the host app binary, whose native runner + # detects multiprocessing argv and runs Python headlessly instead of opening + # another GUI window. Set it before user code can import multiprocessing, + # because multiprocessing snapshots the executable during import. + if _sp_host_exe := {host_executable}: + sys.executable = _sp_host_exe + sys._base_executable = _sp_host_exe + + # Execute the configured app module as the real __main__ module. + _sp_run_module_as_main("{module_name}") except Exception as e: ex = e traceback.print_exception(e) -sys.exit(0 if ex is None else 100) +sys.exit(0 if ex is None else $errorExitCode) """; From c485b71becfe7d69275530d2e439a899fc57923a Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Mon, 6 Jul 2026 21:30:43 +0200 Subject: [PATCH 05/19] feat(create): guard ft.run() with __main__ in the app scaffold MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Start every new project with the standard entry-point guard: if __name__ == "__main__": ft.run(main) With multiprocessing now working in packaged desktop apps, spawn children re-import the app's main module (as __mp_main__) exactly like plain CPython — an unguarded ft.run() would start a whole new app session in every worker process. The guard has always been Python best practice; the scaffold now models it so multiprocessing users don't learn it the hard way. Also type the scaffold's event handler parameter (e: ft.Event[ft.FloatingActionButton]) to model typed event handlers. --- .../templates/app/app/{{cookiecutter.out_dir}}/src/main.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sdk/python/templates/app/app/{{cookiecutter.out_dir}}/src/main.py b/sdk/python/templates/app/app/{{cookiecutter.out_dir}}/src/main.py index 9e936e3966..21959dfc5a 100644 --- a/sdk/python/templates/app/app/{{cookiecutter.out_dir}}/src/main.py +++ b/sdk/python/templates/app/app/{{cookiecutter.out_dir}}/src/main.py @@ -4,7 +4,7 @@ def main(page: ft.Page): counter = ft.Text("0", size=50, data=0) - def increment_click(e): + def increment_click(e: ft.Event[ft.FloatingActionButton]): counter.data += 1 counter.value = str(counter.data) @@ -22,4 +22,5 @@ def increment_click(e): ) -ft.run(main) +if __name__ == "__main__": + ft.run(main) From eef99086daa6c3a240d3d451eeb5a357ac42face Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Mon, 6 Jul 2026 22:38:50 +0200 Subject: [PATCH 06/19] refactor(build): splice all dynamic boot-script values via jsonEncode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit JSON string/array literals are valid Python literals, so jsonEncode gives correct escaping for free. The previous hand-rolled escaping was uneven: {outLogFilename} doubled backslashes but not quotes, and {argv} escaped quotes but not backslashes — a Windows-style path in either would corrupt the generated Python source. {host_executable} already used jsonEncode; now all three share the one convention. Empty argv still renders as [""] (CPython always has a sys.argv[0]). Verified: boot script rendered through the real Dart substitution with hostile inputs (backslashes, quotes, non-ASCII) compiles as valid Python and every value round-trips exactly; full flet build macos + the multiprocessing probe suite passes unchanged. --- .../{{cookiecutter.out_dir}}/lib/native_runtime.dart | 9 +++++---- .../build/{{cookiecutter.out_dir}}/lib/python.dart | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/native_runtime.dart b/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/native_runtime.dart index 4673850aee..005ede2ca8 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/native_runtime.dart +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/native_runtime.dart @@ -105,12 +105,13 @@ Future runPython({ required Map environmentVariables, required List args, }) async { - var argvItems = args.map((a) => "\"${a.replaceAll('"', '\\"')}\""); - var argv = "[${argvItems.isNotEmpty ? argvItems.join(',') : '""'}]"; + // JSON literals are valid Python literals, so every dynamic value is + // spliced into the boot script through jsonEncode: it correctly escapes + // backslashes (Windows paths), quotes, and non-ASCII. var script = pythonScript - .replaceAll("{outLogFilename}", outLogFilename.replaceAll("\\", "\\\\")) + .replaceAll('{outLogFilename}', jsonEncode(outLogFilename)) .replaceAll('{module_name}', moduleName) - .replaceAll('{argv}', argv) + .replaceAll('{argv}', jsonEncode(args.isNotEmpty ? args : [""])) .replaceAll('{host_executable}', jsonEncode(Platform.resolvedExecutable)); var completer = Completer(); diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/python.dart b/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/python.dart index 07af2d8837..38f05b7788 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/python.dart +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/python.dart @@ -57,7 +57,7 @@ def initialize_ctypes(): initialize_ctypes() -out_file = open("{outLogFilename}", "w+", buffering=1) +out_file = open({outLogFilename}, "w+", buffering=1) # libdart_bridge >= 1.3.0 installs native-log file-like wrappers as # sys.stdout / sys.stderr right after Py_Initialize so prints land in @@ -199,7 +199,7 @@ def _sp_run_module_as_main(module_name): sys.modules["__main__"] = main sys.modules["__mp_main__"] = main # Match multiprocessing's spawn alias for the main module. - # Do not let re-exec'd multiprocessing children inherit PYTHONINSPECT, or + # Prevent re-exec'd multiprocessing children from inheriting PYTHONINSPECT, or # they may stay open in interactive mode after their command finishes. os.environ.pop("PYTHONINSPECT", None) From a0ed92018fdaec98a1be0f9106e228a16be76aab Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Tue, 7 Jul 2026 14:59:25 +0200 Subject: [PATCH 07/19] docs: Multiprocessing cookbook recipe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New cookbook page documenting multiprocessing support in Flet apps, now that packaged desktop apps service the spawn re-exec protocol: - when to reach for processes vs async/threads, and the platform/version support matrix (desktop-only, Flet >= 0.86.0; iOS/Android/browser unsupported); - the rules that are standard Python multiprocessing discipline but MANDATORY in packaged apps: the `if __name__ == "__main__":` guard (spawn/forkserver children re-import the main module), importable and picklable worker functions (top-level, plain data, no controls/page/ lambdas), and no GUI access from workers; - how it works in a `flet build` app: the embedded interpreter, the app binary recognizing CPython helper command lines and running them as a headless interpreter, and the practical consequences — sys.executable points at the app binary by design (don't set_executable() over it), freeze_support() is unnecessary-but-harmless, worker stdout isn't the app console log, and forcing fork on Linux is unsafe under a running Flutter engine; - a runnable ProcessPoolExecutor example (parallel chunk sorting with live progress) driven off the UI thread via page.run_thread. Listed in the cookbook sidebar after Subprocess. --- website/docs/cookbook/multiprocessing.md | 157 +++++++++++++++++++++++ website/sidebars.yml | 1 + 2 files changed, 158 insertions(+) create mode 100644 website/docs/cookbook/multiprocessing.md diff --git a/website/docs/cookbook/multiprocessing.md b/website/docs/cookbook/multiprocessing.md new file mode 100644 index 0000000000..35a41b25c9 --- /dev/null +++ b/website/docs/cookbook/multiprocessing.md @@ -0,0 +1,157 @@ +--- +title: "Multiprocessing" +--- + +In this cookbook recipe, you'll learn how to use Python's built-in +[`multiprocessing`](https://docs.python.org/3/library/multiprocessing.html) module — +including [`concurrent.futures.ProcessPoolExecutor`](https://docs.python.org/3/library/concurrent.futures.html#processpoolexecutor) — +from a Flet app, for true CPU parallelism across processes. + +For I/O-bound work, or work that just needs to stay off the UI thread, prefer +[async or threads](async-apps.md) — they are lighter and work on every platform. +Reach for `multiprocessing` when you need multiple CPU cores doing Python work +at the same time (number crunching, batch processing, ML inference, etc.), or +when you need process isolation for work that may fail or need to be stopped. + +:::important[Platform and Flet version support] +`multiprocessing` works in Flet desktop apps during development ([`flet run`](../cli/flet-run.md)) and +in packaged desktop apps [built](../publish/index.md) with `flet build macos`, `flet build windows`, or +`flet build linux` when using [Flet v0.86.0](https://github.com/flet-dev/flet/releases/tag/v0.86.0) or newer. + +It is **not supported on iOS and Android** (mobile operating systems don't +allow apps to spawn arbitrary child processes) or **in the browser**. On those +platforms, prefer threads or `asyncio` instead. +::: + +## Rules + +These are standard Python `multiprocessing` rules — but in a packaged Flet app +they are **mandatory**, not just good style. + +### Always guard your entry point + +Start your app only under the `if __name__ == "__main__":` guard. For example: + +```python +import flet as ft + +def main(page: ft.Page): + ... + +if __name__ == "__main__": + ft.run(main) +``` + +With the `spawn` and `forkserver` start methods, worker/helper processes need +to safely import your main module. `spawn` is the default on macOS and Windows; +`forkserver` is the default on Linux starting with Python 3.14. Without the +guard, a child process can try to start your whole app again. + +### Use importable, picklable worker functions + +Worker targets, arguments, and return values must be picklable so Python can +send them between processes. In practice: + +* define worker functions at module top level, not inside `main()` or inside a + button handler +* pass plain data such as numbers, strings, lists, dicts, or dataclasses +* do not pass Flet controls, `page`, database connections, open files, lambdas, + or nested functions + +Good: + +```python +def sort_chunk(chunk): + return sorted(chunk) +``` + +Avoid: + +```python +def main(page: ft.Page): + def sort_chunk(chunk): + return sorted(chunk) +``` + +The nested version is not reliably picklable because worker processes need to +import the function by name from a module. + +### Don't touch the GUI from workers + +Worker processes run in a separate interpreter with no connection to your app's +page. Pass data back through [`multiprocessing.Queue`](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Queue), +[`Pipe`](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Pipe), or +[pool futures](https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Future), +and update the UI from the main process. + +## How does it work? + +In a desktop app packaged with `flet build`, there is no separate `python` executable — +the interpreter is embedded inside your app's binary. When `multiprocessing` spawns a +worker, it re-executes that binary with a CPython helper command line; the binary +recognizes that shape and services it as a plain, windowless Python interpreter. +This also covers multiprocessing's helper processes (the resource tracker and the `forkserver`). + +A few practical consequences and notes: + +- [`sys.executable`](https://docs.python.org/3/library/sys.html#sys.executable) in a packaged app points at your app's binary, not a + `python` executable. That is intentional — don't override it with + [`multiprocessing.set_executable()`](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.set_executable). +- You usually do not need [`multiprocessing.freeze_support()`](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.freeze_support) in Flet apps. + Calling it inside the `if __name__ == "__main__":` block is harmless, but Flet + does not rely on PyInstaller-style frozen-executable bootstrapping. +- Worker `print()` output is not connected to your app's console log; use a + [`Queue`](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Queue) or file-based logging if you need worker diagnostics. +- On Linux, avoid forcing the `fork` start method: your app's process runs the + Flutter engine with many active threads, and forking it is unsafe. Prefer the + platform default (`forkserver`/`spawn`), or request one explicitly with + `multiprocessing.get_context("spawn")`. + +## Examples + +### Parallel sort with live progress + +Sort chunks of data across all CPU cores and stream progress to the page: + +```python +from concurrent.futures import ProcessPoolExecutor, as_completed + +import flet as ft +import random + +def sort_chunk(chunk: list[float]) -> list[float]: + return sorted(chunk) + + +def main(page: ft.Page): + def run_sort(): + chunks = [ + [random.random() for _ in range(250_000)] + for _ in range(8) + ] + completed = 0 + with ProcessPoolExecutor() as pool: + futures = [pool.submit(sort_chunk, c) for c in chunks] + for _ in as_completed(futures): + completed += 1 + progress.value = completed / len(futures) + status.value = f"Sorted {completed}/{len(futures)} chunks" + page.update() + status.value = "Done!" + page.update() + + page.add( + ft.Button("Start sorting", on_click=lambda _: page.run_thread(run_sort)), + status := ft.Text("Idle"), + progress := ft.ProgressBar(value=0, width=300), + ) + + +if __name__ == "__main__": + ft.run(main) +``` + +Note how the long-running orchestration is moved off the UI event handler with +[`page.run_thread`](async-apps.md#threading), while the CPU-heavy work runs in the +process pool. The worker function may live in your main module (as above) or in +a separate importable module — both work. diff --git a/website/sidebars.yml b/website/sidebars.yml index 140ec95e1d..8659c4af71 100644 --- a/website/sidebars.yml +++ b/website/sidebars.yml @@ -36,6 +36,7 @@ docs: Session Storage: cookbook/session-storage.md PubSub: cookbook/pub-sub.md Subprocess: cookbook/subprocess.md + Multiprocessing: cookbook/multiprocessing.md Logging: cookbook/logging.md Authentication: cookbook/authentication.md Encrypting sensitive data: cookbook/encrypting-sensitive-data.md From 6cf2905d1dc688743b635c6672c0bbbe38878d10 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Tue, 7 Jul 2026 16:30:44 +0200 Subject: [PATCH 08/19] add changelog entry --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ce41200c1..f3b45f2fdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,9 @@ ## 0.86.0 +### New features ### New features +* Add support for **Python `multiprocessing` in packaged Flet desktop apps** built with `flet build macos`, `flet build windows`, and `flet build linux`. `multiprocessing` APIs such as `Process`, `ProcessPoolExecutor`, the `spawn`/`forkserver` start methods, and the resource tracker now work in packaged desktop apps. Previously, worker processes re-executed `sys.executable`, which pointed to the app binary itself, causing each worker to launch another GUI app instance and hang. Flet desktop runners now detect CPython multiprocessing child command lines before Flutter starts and divert them to a headless embedded Python interpreter via `dart_bridge` 1.5.0+. The Python bootstrap also runs the app module as the real `sys.modules["__main__"]` with `python -m` semantics, so top-level worker functions in `main.py` can be pickled correctly. When using `multiprocessing`, your app must follow normal Python multiprocessing rules: guard `ft.run(...)` with `if __name__ == "__main__":`, define worker functions at module top level, and do not access Flet UI objects from worker processes. See the new [Multiprocessing](https://docs.flet.dev/cookbook/multiprocessing) cookbook page. Mobile platforms remain unsupported because iOS and Android do not allow apps to spawn arbitrary child processes ([#4283](https://github.com/flet-dev/flet/issues/4283)) by @ndonkoHenri. * Multi-version bundled CPython support in `flet build` and `flet publish`. Pick the runtime your app ships with via the new `--python-version` flag (3.12 / 3.13 / 3.14), or let it be derived from `[project].requires-python` in your `pyproject.toml`; defaults to the latest supported stable (currently 3.14). The matching CPython-standalone build, Pyodide release (0.27.7 / 0.29.4 / 314.0.1), and Emscripten wheel platform tag are all resolved from `flet-dev/python-build`'s date-keyed manifest. Adding a future pre-release CPython line (e.g. 3.15 beta) is a one-row append with `prerelease=True` — opt-in only via an explicit `--python-version 3.15` or `requires-python = "==3.15.*"`, never the auto-resolved default. Requires `serious_python` >= 4.0.0, now pinned in the `flet build` template. See the new [Choosing a Python version](https://flet.dev/docs/publish#choosing-a-python-version) docs section ([#6577](https://github.com/flet-dev/flet/pull/6577)) by @FeodorFitsner. * Add `ft.DataChannel`: dedicated byte channels for widgets that move bulk binary data (image frames, audio buffers, ML tensors) between Dart and Python, bypassing the MsgPack control protocol. The Dart side opens a channel via `FletBackend.of(context).openDataChannel()` and announces it to Python by firing a `data_channel_open` control event with `{channel_name, channel_id}`; the Python side declares `on_data_channel_open: Optional[ft.EventHandler[ft.DataChannelOpenEvent]]` and captures the channel via `self.get_data_channel(e.channel_id)`. Backed by a dedicated `PythonBridge` per channel in embedded native mode (4–7 GiB/s on M2 Pro) and by the default `ProtocolMuxedDataChannelFactory` in dev / web modes (raw-byte frames muxed over the active Flet protocol transport with a 1-byte type discriminator). Pyodide gets zero-copy outbound sends via `postMessage` Transferable ArrayBuffer. First consumer: `flet-charts` `MatplotlibChartCanvas`, migrated from `_invoke_method` PNG dispatch to a 1-byte-opcode data channel by @FeodorFitsner. * **In-process Python transport (`dart_bridge` FFI).** `package:flet` gains a third protocol transport alongside the UDS / TCP socket servers: it can run Flet's MsgPack protocol over an in-process `dart_bridge` byte channel via a `FletApp(channelBuilder: …)` seam (the `flet` package stays Python-independent — it doesn't depend on `serious_python` or know about `PythonBridge`; the embedder wires the channel). `serious_python` >= 3.0.0 uses this seam to embed the Python interpreter **in-process** instead of talking to it over a localhost socket, and the `flet build` template migrates from sockets to the FFI transport. On Android, where the OS may keep the process alive across a back-button quit and restart only the Dart VM, the transport rebinds to the new VM's `dart_bridge` ports on a session-restart signal (`libdart_bridge` >= 1.3.0) — the Python process and its in-memory state are preserved and the Flet session is rebuilt from `REGISTER_CLIENT` by @FeodorFitsner. From 596b4973534e91d73846064181d833474e7c0de5 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Tue, 7 Jul 2026 20:50:10 +0200 Subject: [PATCH 09/19] fix(build): open console.log as UTF-8 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The boot script opened the console-log tee file without an explicit encoding, so Windows used the locale codepage (e.g. cp1252) and the first non-ASCII character an app printed raised UnicodeEncodeError inside the stdout tee — crashing the printing thread. macOS/Linux never hit it because their default filesystem encoding is UTF-8 already. Found while verifying the multiprocessing fix on a Windows VM: the demo app's status line contains "→" and its print() died mid-benchmark. errors="backslashreplace" additionally guarantees that even malformed data (e.g. lone surrogates) degrades to an escaped representation instead of ever breaking the log. --- .../build/{{cookiecutter.out_dir}}/lib/python.dart | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/python.dart b/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/python.dart index 38f05b7788..acbb76d63f 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/python.dart +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/python.dart @@ -57,7 +57,13 @@ def initialize_ctypes(): initialize_ctypes() -out_file = open({outLogFilename}, "w+", buffering=1) +out_file = open( + {outLogFilename}, + "w+", + buffering=1, + encoding="utf-8", + errors="backslashreplace", # prevents encoding failures +) # libdart_bridge >= 1.3.0 installs native-log file-like wrappers as # sys.stdout / sys.stderr right after Py_Initialize so prints land in From 6c65a66b57057fabd1c39b0fb37554817d6908bd Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Tue, 7 Jul 2026 21:53:31 +0200 Subject: [PATCH 10/19] fix: hide the console window of the git version-fallback on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit flet/version.py resolves the package version at import time; in a source checkout (empty baked flet_version) it falls back to `git describe`. On Windows, git.exe is a console-subsystem program, so spawning it from a windowless GUI process pops a visible conhost window. In a dev-built app this meant one console flash at app start — and, with multiprocessing now working, one more flash per spawned worker, since every spawn child re-imports flet and re-runs the fallback. Traced on a Windows VM via Win32_ProcessStartTrace: each worker pid parented a git.exe (+ conhost.exe) at click time. Pass CREATE_NO_WINDOW on Windows so the fallback stays invisible. Released packages are unaffected either way (CI bakes flet_version, so the git path never runs). --- sdk/python/packages/flet/src/flet/version.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sdk/python/packages/flet/src/flet/version.py b/sdk/python/packages/flet/src/flet/version.py index e869bfc3eb..3e50c2eb5b 100644 --- a/sdk/python/packages/flet/src/flet/version.py +++ b/sdk/python/packages/flet/src/flet/version.py @@ -54,6 +54,8 @@ def from_git() -> Optional[str]: capture_output=True, text=True, check=True, + # Prevent console-window-flashes per git invocation on Windows. + creationflags=sp.CREATE_NO_WINDOW if is_windows() else 0, ) tag = result.stdout.strip() return tag[1:] if tag.startswith("v") else tag From e7187d6fbd60f355d1ed99a38862e2ff350baf8a Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Wed, 8 Jul 2026 13:18:45 +0200 Subject: [PATCH 11/19] add cookbook recipe and examples --- .../cookbook/multiprocessing/cancel_task.py | 69 ++++++++++ .../cookbook/multiprocessing/parallel_sort.py | 46 +++++++ .../multiprocessing/persistent_worker.py | 67 ++++++++++ .../multiprocessing/worker_progress.py | 58 +++++++++ website/docs/cookbook/multiprocessing.md | 118 ++++++++++-------- 5 files changed, 305 insertions(+), 53 deletions(-) create mode 100644 sdk/python/examples/cookbook/multiprocessing/cancel_task.py create mode 100644 sdk/python/examples/cookbook/multiprocessing/parallel_sort.py create mode 100644 sdk/python/examples/cookbook/multiprocessing/persistent_worker.py create mode 100644 sdk/python/examples/cookbook/multiprocessing/worker_progress.py diff --git a/sdk/python/examples/cookbook/multiprocessing/cancel_task.py b/sdk/python/examples/cookbook/multiprocessing/cancel_task.py new file mode 100644 index 0000000000..e7c284dbf9 --- /dev/null +++ b/sdk/python/examples/cookbook/multiprocessing/cancel_task.py @@ -0,0 +1,69 @@ +import multiprocessing +import time + +import flet as ft + + +def long_job(seconds: int) -> None: + """Burn CPU for roughly `seconds` seconds. + + Stands in for work you cannot interrupt from Python itself, e.g. a tight + loop inside a C extension library. Runs in a worker process, which is why + it can be cancelled at any point — threads can't be stopped like that. + """ + deadline = time.monotonic() + seconds + while time.monotonic() < deadline: + sum(range(1_000_000)) + + +def main(page: ft.Page): + process = None + + def watch(p: multiprocessing.Process): + """Wait (on a background thread) for the worker to end, then report + whether it finished on its own or was cancelled.""" + p.join() + if p.exitcode == 0: + status.value = "Process finished normally." + else: + # terminate() ends the child with a negative exit code (-SIGTERM). + status.value = f"Process was terminated (exit code {p.exitcode})." + start.disabled = False + cancel.disabled = True + page.update() + + def start_job(): + nonlocal process + process = multiprocessing.Process(target=long_job, args=(30,), daemon=True) + process.start() + status.value = f"Running in process {process.pid}…" + start.disabled = True + cancel.disabled = False + page.update() + page.run_thread(watch, process) + + def cancel_job(): + if process is not None and process.is_alive(): + process.terminate() # threads can't do this + + page.add( + ft.SafeArea( + content=ft.Column( + controls=[ + ft.Row( + controls=[ + start := ft.Button("Start 30s job", on_click=start_job), + cancel := ft.Button( + "Cancel", on_click=cancel_job, disabled=True + ), + ] + ), + status := ft.Text(), + ] + ) + ) + ) + + +if __name__ == "__main__": + ft.run(main) diff --git a/sdk/python/examples/cookbook/multiprocessing/parallel_sort.py b/sdk/python/examples/cookbook/multiprocessing/parallel_sort.py new file mode 100644 index 0000000000..2a9c8e4fa3 --- /dev/null +++ b/sdk/python/examples/cookbook/multiprocessing/parallel_sort.py @@ -0,0 +1,46 @@ +import random +from concurrent.futures import ProcessPoolExecutor, as_completed + +import flet as ft + + +def sort_chunk(chunk: list[float]) -> list[float]: + """Sort one chunk of data.""" + return sorted(chunk) + + +def main(page: ft.Page): + def run_sort(): + """Orchestrate the pool from a background thread, updating the UI + from the main process as results come in.""" + chunks = [[random.random() for _ in range(250_000)] for _ in range(8)] + completed = 0 + # Without max_workers, the pool sizes itself to the machine's CPUs. + with ProcessPoolExecutor() as pool: + futures = [pool.submit(sort_chunk, c) for c in chunks] + # as_completed() yields each future as soon as its worker is done. + for _ in as_completed(futures): + completed += 1 + progress.value = completed / len(futures) + status.value = f"Sorted {completed}/{len(futures)} chunks" + page.update() + status.value = "Done!" + page.update() + + page.add( + ft.SafeArea( + content=ft.Column( + controls=[ + ft.Button( + "Start sorting", on_click=lambda: page.run_thread(run_sort) + ), + status := ft.Text("Idle"), + progress := ft.ProgressBar(value=0, width=300), + ] + ) + ) + ) + + +if __name__ == "__main__": + ft.run(main) diff --git a/sdk/python/examples/cookbook/multiprocessing/persistent_worker.py b/sdk/python/examples/cookbook/multiprocessing/persistent_worker.py new file mode 100644 index 0000000000..8c861f6af1 --- /dev/null +++ b/sdk/python/examples/cookbook/multiprocessing/persistent_worker.py @@ -0,0 +1,67 @@ +import multiprocessing +import time + +import flet as ft + + +def calc_worker(conn) -> None: + """Serve calculation jobs over the pipe until told to stop. + + Runs in a worker process that stays alive across jobs, so the process + startup cost is paid once. Expensive setup (loading models, opening + datasets, warming caches) could be done once here, before the loop, + and reused by every job. Receiving `None` is the shutdown signal. + """ + while (job := conn.recv()) is not None: + print("entering while loop", job) + started = time.perf_counter() + result = sum(i * i for i in range(job)) + conn.send((result, time.perf_counter() - started)) + print("closing connection") + conn.close() + + +def main(page: ft.Page): + # One end of the pipe goes to the worker, the other stays with the UI. + parent_conn, child_conn = multiprocessing.Pipe() + worker = multiprocessing.Process( + target=calc_worker, args=(child_conn,), daemon=True + ) + worker.start() + + def submit(): + # One job in flight at a time: the button stays disabled until the + # worker replies (a bare Pipe is not multiplexed). + button.disabled = True + status.value = "Working…" + page.update() + page.run_thread(request) + + def request(): + """Send a job to the worker and wait for its reply. + + Should be run on a background thread: conn.recv() blocks until the worker + answers, so it must stay off the UI event loop. + """ + parent_conn.send(100_000_000) + result, duration = parent_conn.recv() + status.value = ( + f"sum of squares = {result}\n{duration:.2f}s in process {worker.pid}" + ) + button.disabled = False + page.update() + + page.add( + ft.SafeArea( + content=ft.Column( + controls=[ + button := ft.Button("Compute in worker", on_click=submit), + status := ft.Text(f"Worker ready (pid {worker.pid})"), + ] + ) + ) + ) + + +if __name__ == "__main__": + ft.run(main) diff --git a/sdk/python/examples/cookbook/multiprocessing/worker_progress.py b/sdk/python/examples/cookbook/multiprocessing/worker_progress.py new file mode 100644 index 0000000000..bc89d0158d --- /dev/null +++ b/sdk/python/examples/cookbook/multiprocessing/worker_progress.py @@ -0,0 +1,58 @@ +import multiprocessing + +import flet as ft + + +def crunch(progress_queue: multiprocessing.Queue, steps: int) -> None: + """Do `steps` slices of CPU-heavy work, reporting progress after each one. + + Runs in a worker process, which has no access to the page — the queue is + the only channel back to the UI. Values are fractions 0..1; a final `None` + tells the consumer there is nothing more to read. + """ + for i in range(steps): + sum(range(50_000_000)) # one slice of real work + progress_queue.put((i + 1) / steps) + progress_queue.put(None) # sentinel: no more updates + + +def main(page: ft.Page): + def start(): + button.disabled = True + status.value = "Starting worker…" + page.update() + queue = multiprocessing.Queue() + worker = multiprocessing.Process(target=crunch, args=(queue, 20), daemon=True) + worker.start() + page.run_thread(drain, queue, worker) + + def drain(queue: multiprocessing.Queue, worker: multiprocessing.Process): + """Forward the worker's progress reports to the UI. + + Runs on a background thread: queue.get() blocks until the worker + reports again, so it must stay off the UI event loop. + """ + while (value := queue.get()) is not None: + progress.value = value + status.value = f"Crunching… {value:.0%}" + page.update() + worker.join() + status.value = "Done!" + button.disabled = False + page.update() + + page.add( + ft.SafeArea( + content=ft.Column( + controls=[ + button := ft.Button("Start", on_click=start), + progress := ft.ProgressBar(value=0, width=300), + status := ft.Text(), + ] + ) + ) + ) + + +if __name__ == "__main__": + ft.run(main) diff --git a/website/docs/cookbook/multiprocessing.md b/website/docs/cookbook/multiprocessing.md index 35a41b25c9..d2963ef488 100644 --- a/website/docs/cookbook/multiprocessing.md +++ b/website/docs/cookbook/multiprocessing.md @@ -2,6 +2,8 @@ title: "Multiprocessing" --- +import {CodeExample} from '@site/src/components/crocodocs'; + In this cookbook recipe, you'll learn how to use Python's built-in [`multiprocessing`](https://docs.python.org/3/library/multiprocessing.html) module — including [`concurrent.futures.ProcessPoolExecutor`](https://docs.python.org/3/library/concurrent.futures.html#processpoolexecutor) — @@ -15,18 +17,27 @@ when you need process isolation for work that may fail or need to be stopped. :::important[Platform and Flet version support] `multiprocessing` works in Flet desktop apps during development ([`flet run`](../cli/flet-run.md)) and -in packaged desktop apps [built](../publish/index.md) with `flet build macos`, `flet build windows`, or -`flet build linux` when using [Flet v0.86.0](https://github.com/flet-dev/flet/releases/tag/v0.86.0) or newer. +in packaged **desktop** apps built with [`flet build {macos,windows,linux}`](../cli/flet-build.md) +or [`flet debug {macos,windows,linux}`](../cli/flet-debug.md) when using [Flet v0.86.0](../updates/release-notes.md#086x) or newer. It is **not supported on iOS and Android** (mobile operating systems don't allow apps to spawn arbitrary child processes) or **in the browser**. On those platforms, prefer threads or `asyncio` instead. ::: -## Rules +## How does it work? + +In a desktop app packaged with [`flet build`](../publish/index.md), there is no separate `python` executable — +the interpreter is embedded inside your app's binary. When `multiprocessing` spawns a +worker, it re-executes that binary with a CPython helper command line; the binary +recognizes that shape and services it as a plain, windowless Python interpreter. +This also covers multiprocessing's helper processes (the resource tracker and the `forkserver`). + +## Guidelines -These are standard Python `multiprocessing` rules — but in a packaged Flet app -they are **mandatory**, not just good style. +These are standard +[Python `multiprocessing` guidelines](https://docs.python.org/3/library/multiprocessing.html#programming-guidelines) +— but in a packaged Flet app they are **mandatory**, not just good style. ### Always guard your entry point @@ -84,15 +95,7 @@ page. Pass data back through [`multiprocessing.Queue`](https://docs.python.org/3 [pool futures](https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Future), and update the UI from the main process. -## How does it work? - -In a desktop app packaged with `flet build`, there is no separate `python` executable — -the interpreter is embedded inside your app's binary. When `multiprocessing` spawns a -worker, it re-executes that binary with a CPython helper command line; the binary -recognizes that shape and services it as a plain, windowless Python interpreter. -This also covers multiprocessing's helper processes (the resource tracker and the `forkserver`). - -A few practical consequences and notes: +### Others - [`sys.executable`](https://docs.python.org/3/library/sys.html#sys.executable) in a packaged app points at your app's binary, not a `python` executable. That is intentional — don't override it with @@ -102,10 +105,15 @@ A few practical consequences and notes: does not rely on PyInstaller-style frozen-executable bootstrapping. - Worker `print()` output is not connected to your app's console log; use a [`Queue`](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Queue) or file-based logging if you need worker diagnostics. -- On Linux, avoid forcing the `fork` start method: your app's process runs the - Flutter engine with many active threads, and forking it is unsafe. Prefer the - platform default (`forkserver`/`spawn`), or request one explicitly with - `multiprocessing.get_context("spawn")`. +- On Linux, avoid forcing the `fork` [start method](https://docs.python.org/3/library/multiprocessing.html#contexts-and-start-methods): + your app's process runs the Flutter engine with many active threads, and forking it is unsafe. + Prefer the platform default (usually `forkserver` or `spawn`), or request one explicitly with + [`multiprocessing.set_start_method()`](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.set_start_method) + or [`multiprocessing.get_context()`](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.get_context). +- Starting a worker costs more in a packaged app than in plain Python — each + spawned child loads your app binary's libraries before Python takes over. + Create pools and long-lived workers once and reuse them, rather than spawning + per button click (see [Keep a persistent worker](#keep-a-persistent-worker) example). ## Examples @@ -113,45 +121,49 @@ A few practical consequences and notes: Sort chunks of data across all CPU cores and stream progress to the page: -```python -from concurrent.futures import ProcessPoolExecutor, as_completed + -import flet as ft -import random +Note how the long-running orchestration is moved off the UI event handler with +[`page.run_thread`](async-apps.md#threading), while the CPU-heavy work runs in the +process pool. The worker function may live in your main module (as above) or in +a separate importable module — both work. -def sort_chunk(chunk: list[float]) -> list[float]: - return sorted(chunk) +### Stream progress from a worker +To show fine-grained progress from inside a single long-running job, pass a +[`multiprocessing.Queue`](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Queue) +to the worker and drain it on a background thread. The worker [`put()`](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Queue.put)s progress +values and a `None` sentinel when it is done: -def main(page: ft.Page): - def run_sort(): - chunks = [ - [random.random() for _ in range(250_000)] - for _ in range(8) - ] - completed = 0 - with ProcessPoolExecutor() as pool: - futures = [pool.submit(sort_chunk, c) for c in chunks] - for _ in as_completed(futures): - completed += 1 - progress.value = completed / len(futures) - status.value = f"Sorted {completed}/{len(futures)} chunks" - page.update() - status.value = "Done!" - page.update() - - page.add( - ft.Button("Start sorting", on_click=lambda _: page.run_thread(run_sort)), - status := ft.Text("Idle"), - progress := ft.ProgressBar(value=0, width=300), - ) + +### Keep a persistent worker -if __name__ == "__main__": - ft.run(main) -``` +Starting a process is not free — especially in a packaged app, where each child +loads your app binary's libraries first. For repeated jobs, start one worker +that stays alive and serves requests over a +[`Pipe`](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Pipe), +paying the startup cost once. Expensive setup (loading a model, opening a +dataset) can then also happen once, in the worker: -Note how the long-running orchestration is moved off the UI event handler with -[`page.run_thread`](async-apps.md#threading), while the CPU-heavy work runs in the -process pool. The worker function may live in your main module (as above) or in -a separate importable module — both work. + + +The worker is started with `daemon=True`, so it is terminated automatically +when your app exits. + +### Cancel a runaway task + +Threads cannot be forcefully stopped from the outside — a worker [`Process`](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Process) can, +at any time, with [`terminate()`](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Process.terminate). +This makes processes the right tool for jobs you may need to abort, such as +long calls into external libraries: + + + +A background thread [`join()`](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Process.join)s the worker and reports how it ended — normally +(exit code `0`) or via cancellation (a negative exit code). + +:::info +[Python's multiprocessing guidelines](https://docs.python.org/3/library/multiprocessing.html#programming-guidelines) +recommend avoiding process termination or doing it only for processes which never use any shared resources. +::: From 1522f5d0049a1e7c1b5451dd678abd6630a0f6af Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Wed, 8 Jul 2026 14:15:52 +0200 Subject: [PATCH 12/19] docs: add detailed usage guides to CLI command docstrings Enhance docstrings for `debug`, `test`, `create`, `build`, `run`, and `publish` CLI commands with links to detailed usage guides and examples from the Flet documentation. --- sdk/python/packages/flet-cli/src/flet_cli/commands/build.py | 2 +- sdk/python/packages/flet-cli/src/flet_cli/commands/create.py | 2 ++ sdk/python/packages/flet-cli/src/flet_cli/commands/debug.py | 2 ++ sdk/python/packages/flet-cli/src/flet_cli/commands/publish.py | 2 ++ sdk/python/packages/flet-cli/src/flet_cli/commands/run.py | 2 ++ sdk/python/packages/flet-cli/src/flet_cli/commands/test.py | 2 ++ 6 files changed, 11 insertions(+), 1 deletion(-) diff --git a/sdk/python/packages/flet-cli/src/flet_cli/commands/build.py b/sdk/python/packages/flet-cli/src/flet_cli/commands/build.py index 033ce5e5e4..d9a6364b2d 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/commands/build.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/commands/build.py @@ -17,7 +17,7 @@ class Command(BaseBuildCommand): Android (APK/AAB), and iOS (IPA and simulator .app), with a wide range of customization options for metadata, assets, splash screens, and signing. - Detailed guide with usage examples: https://flet.dev/docs/publish + Detailed usage guide: https://flet.dev/docs/publish """ def __init__(self, parser: argparse.ArgumentParser) -> None: diff --git a/sdk/python/packages/flet-cli/src/flet_cli/commands/create.py b/sdk/python/packages/flet-cli/src/flet_cli/commands/create.py index 7349f2ddee..a257686339 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/commands/create.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/commands/create.py @@ -24,6 +24,8 @@ class Command(BaseCommand): Create a new Flet project using a predefined template. It sets up the initial directory structure, metadata, and required files to help you get started quickly. + + Detailed guide with usage examples: https://flet.dev/docs/getting-started/create-flet-app """ def add_arguments(self, parser: argparse.ArgumentParser) -> None: diff --git a/sdk/python/packages/flet-cli/src/flet_cli/commands/debug.py b/sdk/python/packages/flet-cli/src/flet_cli/commands/debug.py index fff1b7b8d3..acdcccb53d 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/commands/debug.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/commands/debug.py @@ -12,6 +12,8 @@ class Command(BaseBuildCommand): """ Run a Flet Python app in debug mode on a specified platform (desktop, web, mobile). + + Detailed usage guide: https://flet.dev/blog/flet-debug-the-new-cli-for-testing-flet-apps-on-mobile-devices """ def __init__(self, parser: argparse.ArgumentParser) -> None: diff --git a/sdk/python/packages/flet-cli/src/flet_cli/commands/publish.py b/sdk/python/packages/flet-cli/src/flet_cli/commands/publish.py index 290df19303..69e8f20d1f 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/commands/publish.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/commands/publish.py @@ -24,6 +24,8 @@ class Command(BaseCommand): """ Compile and package a Flet app as a standalone static web application. + + Detailed usage guide: https://flet.dev/docs/publish/web/static-website """ def add_arguments(self, parser: argparse.ArgumentParser) -> None: diff --git a/sdk/python/packages/flet-cli/src/flet_cli/commands/run.py b/sdk/python/packages/flet-cli/src/flet_cli/commands/run.py index e0217aa2e2..116a2a9d71 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/commands/run.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/commands/run.py @@ -28,6 +28,8 @@ class Command(BaseCommand): """ Run a Flet application in hot reload mode. + + Detailed usage guide: https://flet.dev/docs/getting-started/running-app """ def add_arguments(self, parser: argparse.ArgumentParser) -> None: diff --git a/sdk/python/packages/flet-cli/src/flet_cli/commands/test.py b/sdk/python/packages/flet-cli/src/flet_cli/commands/test.py index accaadb764..fa5945158f 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/commands/test.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/commands/test.py @@ -104,6 +104,8 @@ class Command(BaseBuildCommand): `flet build`, in test mode) so the app runs on-device with embedded Python, then runs pytest. Tests in the `tests/` directory drive the app through the `flet_app` fixture (find controls by key, tap, take/assert screenshots). + + Detailed usage guide: https://flet.dev/docs/getting-started/integration-testing """ def __init__(self, parser: argparse.ArgumentParser) -> None: From 09dc4bfcfb66d7290b1db3dbc697b1e4ca15d0fc Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Wed, 8 Jul 2026 15:38:37 +0200 Subject: [PATCH 13/19] docs: update README of build-template with installation, platform support, and usage guidance --- .../{{cookiecutter.out_dir}}/README.md | 76 +++++++++++++++---- 1 file changed, 62 insertions(+), 14 deletions(-) diff --git a/sdk/python/templates/app/extension/{{cookiecutter.out_dir}}/README.md b/sdk/python/templates/app/extension/{{cookiecutter.out_dir}}/README.md index 00c9c68e54..b5eb9fdd29 100644 --- a/sdk/python/templates/app/extension/{{cookiecutter.out_dir}}/README.md +++ b/sdk/python/templates/app/extension/{{cookiecutter.out_dir}}/README.md @@ -1,37 +1,85 @@ # {{cookiecutter.project_name}} -{{cookiecutter.control_name}} control for Flet -## Installation +{{cookiecutter.project_name}} [Flet](https://flet.dev) extension. +{% if cookiecutter.description %} +{{ cookiecutter.description }} +{% endif %} + -Add dependency to `pyproject.toml` of your Flet app: +## Platform Support -* **Git dependency** + -Link to git repository: +| Platform | iOS | Android | Web | Windows | macOS | Linux | +|-----------|-----|---------|-----|---------|-------|-------| +| Supported | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -``` +## Usage + +### Installation + +Add `{{cookiecutter.project_name}}` dependency to the `pyproject.toml` of your Flet project: + +* **From Git** + +```toml dependencies = [ - "{{cookiecutter.project_name}} @ git+https://github.com/MyGithubAccount/{{cookiecutter.project_name}}", + "{{cookiecutter.project_name}} @ git+https://github.com/MY_GITHUB_ACCOUNT/{{cookiecutter.project_name}}", "flet>={{cookiecutter.flet_version}}", ] ``` -* **uv/pip dependency** + -If the package is published on pypi.org: +* **From PyPI** -``` +```toml dependencies = [ "{{cookiecutter.project_name}}", "flet>={{cookiecutter.flet_version}}", ] ``` -Build your app: +### Run your app + +A Flet extension has two sides: its Python controls/services and the native Flutter/Dart widgets behind them. +That native code must be compiled into a Flet client before your controls can render, and the +prebuilt client that a plain `flet run` uses does **not** include this extension. + +So run your app in one of these two ways: + +**1. [`flet debug`](https://flet.dev/docs/cli/flet-debug)** — all platforms: *Windows, macOS, Linux, Web, iOS, Android* + +Compiles the extension and launches your app on the target you pick. +The simplest option, and the way to go for mobile and web: + +```bash +flet debug macos # desktop & web: no device needed +flet debug android -d # mobile: connect a device/emulator first ``` -flet build macos -v + +For iOS and Android, pass `-d ` (run `flet debug --show-devices` to list connected devices). +Edits to your **Python** code are picked up the next time you run `flet debug`. + +**2. [`flet build`](https://flet.dev/docs/cli/flet-build) once, then [`flet run`](https://flet.dev/docs/cli/flet-run)** — desktop only: *Windows, macOS, Linux* + +Build a custom client that bundles the extension **once**, then use `flet run` for a fast hot-reload loop while you edit Python: + +```bash +flet build macos # or: flet build windows / flet build linux +flet run # run from the folder where build/ was created, so it reuses that client ``` -## Documentation +`flet run` auto-detects the client under `build//`, so your Python edits hot-reload instantly. +Rebuild only when the extension's **Dart** code changes. + +### Examples + +See the [examples](examples) directory. + +### Documentation + + -[Link to documentation](https://MyGithubAccount.github.io/{{cookiecutter.project_name}}/) +Detailed documentation for this package can be found [here](https://MY_GITHUB_ACCOUNT.github.io/{{cookiecutter.project_name}}/). From 876a006fb22a1663fb5f5dd0fdf862ac1db26a70 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Wed, 8 Jul 2026 15:39:00 +0200 Subject: [PATCH 14/19] breaking changes in version folders --- CHANGELOG.md | 6 +++--- website/docs/extend/user-extensions.md | 2 +- website/docs/updates/breaking-changes/index.md | 16 ++++++++-------- ...eprecated-drag-target-event-coordinates.md} | 0 .../deprecated-video-apis.md} | 0 .../removed-spacing-border-helpers.md} | 0 .../app-files-unpacked-read-only-bundle.md} | 0 .../compile-on-by-default.md} | 0 .../data-channel-protocol-upgrade.md} | 0 .../default-bundled-python-3-14.md} | 0 .../deprecated-clear-cache-flag.md} | 0 .../removed-pyodide-version-export.md} | 0 website/sidebars.yml | 18 +++++++++--------- 13 files changed, 21 insertions(+), 21 deletions(-) rename website/docs/updates/breaking-changes/{v0-85-0-deprecated-drag-target-event-coordinates.md => v0-85-0/deprecated-drag-target-event-coordinates.md} (100%) rename website/docs/updates/breaking-changes/{v0-85-0-deprecated-video-apis.md => v0-85-0/deprecated-video-apis.md} (100%) rename website/docs/updates/breaking-changes/{v0-85-0-removed-spacing-border-helpers.md => v0-85-0/removed-spacing-border-helpers.md} (100%) rename website/docs/updates/breaking-changes/{v0-86-0-app-files-unpacked-read-only-bundle.md => v0-86-0/app-files-unpacked-read-only-bundle.md} (100%) rename website/docs/updates/breaking-changes/{v0-86-0-compile-on-by-default.md => v0-86-0/compile-on-by-default.md} (100%) rename website/docs/updates/breaking-changes/{v0-86-0-data-channel-protocol-upgrade.md => v0-86-0/data-channel-protocol-upgrade.md} (100%) rename website/docs/updates/breaking-changes/{v0-86-0-default-bundled-python-3-14.md => v0-86-0/default-bundled-python-3-14.md} (100%) rename website/docs/updates/breaking-changes/{v0-86-0-deprecated-clear-cache-flag.md => v0-86-0/deprecated-clear-cache-flag.md} (100%) rename website/docs/updates/breaking-changes/{v0-86-0-removed-pyodide-version-export.md => v0-86-0/removed-pyodide-version-export.md} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 807dcffd2f..8905eed678 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,11 +26,11 @@ ### Breaking changes -* **App files now ship unpacked in a read-only bundle, and the storage directories were reworked** (requires `serious_python` >= 4.0.0, now pinned in the `flet build` template). Your Python sources ship **unpacked inside the app bundle** next to the stdlib/site-packages (no first-launch `app.zip` extraction) on macOS/iOS/Windows/Linux; on Android they ship as a *stored* `app.zip` asset unpacked once on first launch; web is unchanged. The app directory is now **read-only**, so the Python program's **working directory** moved to a writable, app-private data dir. `FLET_APP_STORAGE_DATA` now maps to the OS *application support* dir (a `data` subdir) instead of the user's Documents folder and is the cwd; `FLET_APP_STORAGE_TEMP` now points to the OS temp dir (was the cache dir) and a new `FLET_APP_STORAGE_CACHE` exposes the cache dir. `flet run` sets the dev cwd to a hidden, git-ignored `/.flet/storage/data`. Relative **reads** of bundled files (`open("seed.json")`) must move to `__file__`/`importlib.resources` or `assets/`. See the [app files unpacked / storage dirs](/docs/updates/breaking-changes/v0-86-0-app-files-unpacked-read-only-bundle) guide by @FeodorFitsner. +* **App files now ship unpacked in a read-only bundle, and the storage directories were reworked** (requires `serious_python` >= 4.0.0, now pinned in the `flet build` template). Your Python sources ship **unpacked inside the app bundle** next to the stdlib/site-packages (no first-launch `app.zip` extraction) on macOS/iOS/Windows/Linux; on Android they ship as a *stored* `app.zip` asset unpacked once on first launch; web is unchanged. The app directory is now **read-only**, so the Python program's **working directory** moved to a writable, app-private data dir. `FLET_APP_STORAGE_DATA` now maps to the OS *application support* dir (a `data` subdir) instead of the user's Documents folder and is the cwd; `FLET_APP_STORAGE_TEMP` now points to the OS temp dir (was the cache dir) and a new `FLET_APP_STORAGE_CACHE` exposes the cache dir. `flet run` sets the dev cwd to a hidden, git-ignored `/.flet/storage/data`. Relative **reads** of bundled files (`open("seed.json")`) must move to `__file__`/`importlib.resources` or `assets/`. See the [app files unpacked / storage dirs](/docs/updates/breaking-changes/v0-86-0/app-files-unpacked-read-only-bundle) guide by @FeodorFitsner. * `flet build` and `flet publish` now bundle CPython 3.14 by default (previously 3.12, implicit via the old single-version `serious_python`). Existing apps that depend on native wheels without 3.14 binaries should pin explicitly with `--python-version 3.12` (CLI), `requires-python = ">=3.12,<3.13"` (pyproject), or `SERIOUS_PYTHON_VERSION=3.12` in the build environment ([#6577](https://github.com/flet-dev/flet/pull/6577)) by @FeodorFitsner. * Android builds now include only the ABIs the bundled Python supports, sourced per-version from python-build's manifest (`pythons..android_abis`) rather than hardcoded in flet. As of python-build `20260630`, `armeabi-v7a` (32-bit ARM) is published for **3.12, 3.13 and 3.14**, so all three build it by default; an explicit `--arch ` is validated against the selected Python's supported set and fails with a clear error otherwise ([#6578](https://github.com/flet-dev/flet/pull/6578)) by @ndonkoHenri. -* `flet build` / `flet publish` now **compile your app and packages to `.pyc` by default** (previously off). This removes per-launch bytecode recompilation — a significant cold-start win, especially on mobile where pure Python is imported from a stored zip (`zipimport`) and can't cache bytecode back to disk, so every module would otherwise recompile from source on each launch. The CLI flags gain `--no-compile-app` / `--no-compile-packages` (via `argparse.BooleanOptionalAction`; the existing `--compile-app` / `--compile-packages` still work), and `[tool.flet.compile].app` / `.packages` now default to `true`. Pass `--no-compile-*` or set them to `false` to restore the old behavior (faster iterative builds, or keeping `.py` source in the bundle). Compiled web builds were verified to load in Pyodide (bundled CPython and Pyodide share the same minor version). See the [compile-on-by-default](/docs/updates/breaking-changes/v0-86-0-compile-on-by-default) guide ([#6598](https://github.com/flet-dev/flet/pull/6598)) by @FeodorFitsner. -* Flet protocol wire format on stream-oriented transports (UDS / TCP) is incompatible with pre-0.86 servers and clients. Every packet now starts with a 4-byte little-endian length prefix and a 1-byte type discriminator (`0x00` = MsgPack control frame, `0x01` = raw DataChannel frame). WebSocket / `postMessage` / `dart_bridge` transports keep native message boundaries and only gain the type byte. The Flet CLI dev server and the in-process Python runtime are upgraded in lockstep — running `flet run` with mismatched `flet` versions across CLI and runtime is no longer supported. See the [DataChannel protocol framing upgrade](/docs/updates/breaking-changes/v0-86-0-data-channel-protocol-upgrade) guide. The `MatplotlibChartCanvas` widget transports its full / diff / clear frames via a `DataChannel` rather than `_invoke_method` arguments — visually identical, but custom code that subclassed it and overrode the apply methods may need updating by @FeodorFitsner. +* `flet build` / `flet publish` now **compile your app and packages to `.pyc` by default** (previously off). This removes per-launch bytecode recompilation — a significant cold-start win, especially on mobile where pure Python is imported from a stored zip (`zipimport`) and can't cache bytecode back to disk, so every module would otherwise recompile from source on each launch. The CLI flags gain `--no-compile-app` / `--no-compile-packages` (via `argparse.BooleanOptionalAction`; the existing `--compile-app` / `--compile-packages` still work), and `[tool.flet.compile].app` / `.packages` now default to `true`. Pass `--no-compile-*` or set them to `false` to restore the old behavior (faster iterative builds, or keeping `.py` source in the bundle). Compiled web builds were verified to load in Pyodide (bundled CPython and Pyodide share the same minor version). See the [compile-on-by-default](/docs/updates/breaking-changes/v0-86-0/compile-on-by-default) guide ([#6598](https://github.com/flet-dev/flet/pull/6598)) by @FeodorFitsner. +* Flet protocol wire format on stream-oriented transports (UDS / TCP) is incompatible with pre-0.86 servers and clients. Every packet now starts with a 4-byte little-endian length prefix and a 1-byte type discriminator (`0x00` = MsgPack control frame, `0x01` = raw DataChannel frame). WebSocket / `postMessage` / `dart_bridge` transports keep native message boundaries and only gain the type byte. The Flet CLI dev server and the in-process Python runtime are upgraded in lockstep — running `flet run` with mismatched `flet` versions across CLI and runtime is no longer supported. See the [DataChannel protocol framing upgrade](/docs/updates/breaking-changes/v0-86-0/data-channel-protocol-upgrade) guide. The `MatplotlibChartCanvas` widget transports its full / diff / clear frames via a `DataChannel` rather than `_invoke_method` arguments — visually identical, but custom code that subclassed it and overrode the apply methods may need updating by @FeodorFitsner. ### Deprecations diff --git a/website/docs/extend/user-extensions.md b/website/docs/extend/user-extensions.md index cee0030498..586d5436a3 100644 --- a/website/docs/extend/user-extensions.md +++ b/website/docs/extend/user-extensions.md @@ -617,7 +617,7 @@ The receiving side's queue is **unbounded by default**. If a producer outpaces t - **First-party reference widget**: `MatplotlibChartCanvas` — a complete migration from `_invoke_method` PNG dispatch to a 1-byte-opcode data channel, including backpressure ack and the GPU / CPU rendering strategies. [Python](https://github.com/flet-dev/flet/blob/main/sdk/python/packages/flet-charts/src/flet_charts/matplotlib_chart_canvas.py) · [Dart](https://github.com/flet-dev/flet/blob/main/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/matplotlib_chart_canvas.dart) - **Full design / performance notes** (wire format, cross-mode operation, concurrency model, empirical numbers): [`dedicated-data-channels.md`](https://github.com/flet-dev/serious-python/blob/main/docs/dedicated-data-channels.md) in `flet-dev/serious-python`. -- **Wire-format protocol upgrade** for anyone implementing a custom Flet backend or sidecar: [Flet protocol framing upgraded for DataChannel support](/docs/updates/breaking-changes/v0-86-0-data-channel-protocol-upgrade). +- **Wire-format protocol upgrade** for anyone implementing a custom Flet backend or sidecar: [Flet protocol framing upgraded for DataChannel support](/docs/updates/breaking-changes/v0-86-0/data-channel-protocol-upgrade). ## Boot screen diff --git a/website/docs/updates/breaking-changes/index.md b/website/docs/updates/breaking-changes/index.md index 243078106e..fc5deeb182 100644 --- a/website/docs/updates/breaking-changes/index.md +++ b/website/docs/updates/breaking-changes/index.md @@ -26,22 +26,22 @@ Each guide explains the change, the reason for it, and how to migrate your code. #### Breaking changes -- [Default bundled Python version is now 3.14](/docs/updates/breaking-changes/v0-86-0-default-bundled-python-3-14) -- [App and packages are compiled to `.pyc` by default](/docs/updates/breaking-changes/v0-86-0-compile-on-by-default) -- [`flet.version.pyodide_version` and `PYODIDE_VERSION` removed](/docs/updates/breaking-changes/v0-86-0-removed-pyodide-version-export) -- [Flet protocol framing upgraded for DataChannel support](/docs/updates/breaking-changes/v0-86-0-data-channel-protocol-upgrade) +- [Default bundled Python version is now 3.14](/docs/updates/breaking-changes/v0-86-0/default-bundled-python-3-14) +- [App and packages are compiled to `.pyc` by default](/docs/updates/breaking-changes/v0-86-0/compile-on-by-default) +- [`flet.version.pyodide_version` and `PYODIDE_VERSION` removed](/docs/updates/breaking-changes/v0-86-0/removed-pyodide-version-export) +- [Flet protocol framing upgraded for DataChannel support](/docs/updates/breaking-changes/v0-86-0/data-channel-protocol-upgrade) #### Deprecations -- [`flet build --clear-cache` flag deprecated](/docs/updates/breaking-changes/v0-86-0-deprecated-clear-cache-flag) +- [`flet build --clear-cache` flag deprecated](/docs/updates/breaking-changes/v0-86-0/deprecated-clear-cache-flag) ### Released in Flet 0.85.0 #### Breaking changes -- [Deprecated spacing and border helper functions removed](/docs/updates/breaking-changes/v0-85-0-removed-spacing-border-helpers) +- [Deprecated spacing and border helper functions removed](/docs/updates/breaking-changes/v0-85-0/removed-spacing-border-helpers) #### Deprecations -- [`DragTargetEvent` coordinate fields deprecated](/docs/updates/breaking-changes/v0-85-0-deprecated-drag-target-event-coordinates) -- [`Video` control APIs deprecated](/docs/updates/breaking-changes/v0-85-0-deprecated-video-apis) +- [`DragTargetEvent` coordinate fields deprecated](/docs/updates/breaking-changes/v0-85-0/deprecated-drag-target-event-coordinates) +- [`Video` control APIs deprecated](/docs/updates/breaking-changes/v0-85-0/deprecated-video-apis) diff --git a/website/docs/updates/breaking-changes/v0-85-0-deprecated-drag-target-event-coordinates.md b/website/docs/updates/breaking-changes/v0-85-0/deprecated-drag-target-event-coordinates.md similarity index 100% rename from website/docs/updates/breaking-changes/v0-85-0-deprecated-drag-target-event-coordinates.md rename to website/docs/updates/breaking-changes/v0-85-0/deprecated-drag-target-event-coordinates.md diff --git a/website/docs/updates/breaking-changes/v0-85-0-deprecated-video-apis.md b/website/docs/updates/breaking-changes/v0-85-0/deprecated-video-apis.md similarity index 100% rename from website/docs/updates/breaking-changes/v0-85-0-deprecated-video-apis.md rename to website/docs/updates/breaking-changes/v0-85-0/deprecated-video-apis.md diff --git a/website/docs/updates/breaking-changes/v0-85-0-removed-spacing-border-helpers.md b/website/docs/updates/breaking-changes/v0-85-0/removed-spacing-border-helpers.md similarity index 100% rename from website/docs/updates/breaking-changes/v0-85-0-removed-spacing-border-helpers.md rename to website/docs/updates/breaking-changes/v0-85-0/removed-spacing-border-helpers.md diff --git a/website/docs/updates/breaking-changes/v0-86-0-app-files-unpacked-read-only-bundle.md b/website/docs/updates/breaking-changes/v0-86-0/app-files-unpacked-read-only-bundle.md similarity index 100% rename from website/docs/updates/breaking-changes/v0-86-0-app-files-unpacked-read-only-bundle.md rename to website/docs/updates/breaking-changes/v0-86-0/app-files-unpacked-read-only-bundle.md diff --git a/website/docs/updates/breaking-changes/v0-86-0-compile-on-by-default.md b/website/docs/updates/breaking-changes/v0-86-0/compile-on-by-default.md similarity index 100% rename from website/docs/updates/breaking-changes/v0-86-0-compile-on-by-default.md rename to website/docs/updates/breaking-changes/v0-86-0/compile-on-by-default.md diff --git a/website/docs/updates/breaking-changes/v0-86-0-data-channel-protocol-upgrade.md b/website/docs/updates/breaking-changes/v0-86-0/data-channel-protocol-upgrade.md similarity index 100% rename from website/docs/updates/breaking-changes/v0-86-0-data-channel-protocol-upgrade.md rename to website/docs/updates/breaking-changes/v0-86-0/data-channel-protocol-upgrade.md diff --git a/website/docs/updates/breaking-changes/v0-86-0-default-bundled-python-3-14.md b/website/docs/updates/breaking-changes/v0-86-0/default-bundled-python-3-14.md similarity index 100% rename from website/docs/updates/breaking-changes/v0-86-0-default-bundled-python-3-14.md rename to website/docs/updates/breaking-changes/v0-86-0/default-bundled-python-3-14.md diff --git a/website/docs/updates/breaking-changes/v0-86-0-deprecated-clear-cache-flag.md b/website/docs/updates/breaking-changes/v0-86-0/deprecated-clear-cache-flag.md similarity index 100% rename from website/docs/updates/breaking-changes/v0-86-0-deprecated-clear-cache-flag.md rename to website/docs/updates/breaking-changes/v0-86-0/deprecated-clear-cache-flag.md diff --git a/website/docs/updates/breaking-changes/v0-86-0-removed-pyodide-version-export.md b/website/docs/updates/breaking-changes/v0-86-0/removed-pyodide-version-export.md similarity index 100% rename from website/docs/updates/breaking-changes/v0-86-0-removed-pyodide-version-export.md rename to website/docs/updates/breaking-changes/v0-86-0/removed-pyodide-version-export.md diff --git a/website/sidebars.yml b/website/sidebars.yml index 8659c4af71..d886255eb7 100644 --- a/website/sidebars.yml +++ b/website/sidebars.yml @@ -72,16 +72,16 @@ docs: Breaking changes and deprecations: _index: updates/breaking-changes/index.md v0.86.0: - App files ship unpacked in a read-only bundle; storage dirs reworked: updates/breaking-changes/v0-86-0-app-files-unpacked-read-only-bundle.md - Default bundled Python version is now 3.14: updates/breaking-changes/v0-86-0-default-bundled-python-3-14.md - App and packages are compiled to .pyc by default: updates/breaking-changes/v0-86-0-compile-on-by-default.md - flet.version.pyodide_version and PYODIDE_VERSION removed: updates/breaking-changes/v0-86-0-removed-pyodide-version-export.md - Flet protocol framing upgraded for DataChannel support: updates/breaking-changes/v0-86-0-data-channel-protocol-upgrade.md - flet build --clear-cache flag deprecated: updates/breaking-changes/v0-86-0-deprecated-clear-cache-flag.md + App files ship unpacked in a read-only bundle; storage dirs reworked: updates/breaking-changes/v0-86-0/app-files-unpacked-read-only-bundle.md + Default bundled Python version is now 3.14: updates/breaking-changes/v0-86-0/default-bundled-python-3-14.md + App and packages are compiled to .pyc by default: updates/breaking-changes/v0-86-0/compile-on-by-default.md + flet.version.pyodide_version and PYODIDE_VERSION removed: updates/breaking-changes/v0-86-0/removed-pyodide-version-export.md + Flet protocol framing upgraded for DataChannel support: updates/breaking-changes/v0-86-0/data-channel-protocol-upgrade.md + flet build --clear-cache flag deprecated: updates/breaking-changes/v0-86-0/deprecated-clear-cache-flag.md v0.85.0: - Deprecated spacing and border helper functions removed: updates/breaking-changes/v0-85-0-removed-spacing-border-helpers.md - DragTargetEvent coordinate fields deprecated: updates/breaking-changes/v0-85-0-deprecated-drag-target-event-coordinates.md - Video control APIs deprecated: updates/breaking-changes/v0-85-0-deprecated-video-apis.md + Deprecated spacing and border helper functions removed: updates/breaking-changes/v0-85-0/removed-spacing-border-helpers.md + DragTargetEvent coordinate fields deprecated: updates/breaking-changes/v0-85-0/deprecated-drag-target-event-coordinates.md + Video control APIs deprecated: updates/breaking-changes/v0-85-0/deprecated-video-apis.md Compatibility policy: updates/compatibility-policy.md Reference: _index: reference/index.md From 07687fac897036c81cf2b868a8c8403d3d3d57db Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Wed, 8 Jul 2026 15:54:04 +0200 Subject: [PATCH 15/19] update changelog --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8905eed678..72b47faebb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,8 @@ ## 0.86.0 -### New features ### New features -* Add support for **Python `multiprocessing` in packaged Flet desktop apps** built with `flet build macos`, `flet build windows`, and `flet build linux`. `multiprocessing` APIs such as `Process`, `ProcessPoolExecutor`, the `spawn`/`forkserver` start methods, and the resource tracker now work in packaged desktop apps. Previously, worker processes re-executed `sys.executable`, which pointed to the app binary itself, causing each worker to launch another GUI app instance and hang. Flet desktop runners now detect CPython multiprocessing child command lines before Flutter starts and divert them to a headless embedded Python interpreter via `dart_bridge` 1.5.0+. The Python bootstrap also runs the app module as the real `sys.modules["__main__"]` with `python -m` semantics, so top-level worker functions in `main.py` can be pickled correctly. When using `multiprocessing`, your app must follow normal Python multiprocessing rules: guard `ft.run(...)` with `if __name__ == "__main__":`, define worker functions at module top level, and do not access Flet UI objects from worker processes. See the new [Multiprocessing](https://docs.flet.dev/cookbook/multiprocessing) cookbook page. Mobile platforms remain unsupported because iOS and Android do not allow apps to spawn arbitrary child processes ([#4283](https://github.com/flet-dev/flet/issues/4283)) by @ndonkoHenri. +* Add support for **Python `multiprocessing` in packaged Flet desktop apps** built with `flet build macos`, `flet build windows`, and `flet build linux`. `multiprocessing` APIs such as `Process`, `ProcessPoolExecutor`, the `spawn`/`forkserver` start methods, and the resource tracker now work in packaged desktop apps. Previously, worker processes re-executed `sys.executable`, which pointed to the app binary itself, causing each worker to launch another GUI app instance and hang. Flet desktop runners now detect CPython multiprocessing child command lines before Flutter starts and divert them to a headless embedded Python interpreter via `dart_bridge` 1.5.0+. The Python bootstrap also runs the app module as the real `sys.modules["__main__"]` with `python -m` semantics, so top-level worker functions in `main.py` can be pickled correctly. When using `multiprocessing`, your app must follow normal Python multiprocessing rules: guard `ft.run(...)` with `if __name__ == "__main__":`, define worker functions at module top level, and do not access Flet UI objects from worker processes. See the new [Multiprocessing](https://docs.flet.dev/cookbook/multiprocessing) cookbook page. Mobile platforms remain unsupported because iOS and Android do not allow apps to spawn arbitrary child processes ([#4283](https://github.com/flet-dev/flet/issues/4283), [#6577](https://github.com/flet-dev/flet/pull/6662)) by @ndonkoHenri. * Multi-version bundled CPython support in `flet build` and `flet publish`. Pick the runtime your app ships with via the new `--python-version` flag (3.12 / 3.13 / 3.14), or let it be derived from `[project].requires-python` in your `pyproject.toml`; defaults to the latest supported stable (currently 3.14). The matching CPython-standalone build, Pyodide release (0.27.7 / 0.29.4 / 314.0.1), and Emscripten wheel platform tag are all resolved from `flet-dev/python-build`'s date-keyed manifest. Adding a future pre-release CPython line (e.g. 3.15 beta) is a one-row append with `prerelease=True` — opt-in only via an explicit `--python-version 3.15` or `requires-python = "==3.15.*"`, never the auto-resolved default. Requires `serious_python` >= 4.0.0, now pinned in the `flet build` template. See the new [Choosing a Python version](https://flet.dev/docs/publish#choosing-a-python-version) docs section ([#6577](https://github.com/flet-dev/flet/pull/6577)) by @FeodorFitsner. * Add `ft.DataChannel`: dedicated byte channels for widgets that move bulk binary data (image frames, audio buffers, ML tensors) between Dart and Python, bypassing the MsgPack control protocol. The Dart side opens a channel via `FletBackend.of(context).openDataChannel()` and announces it to Python by firing a `data_channel_open` control event with `{channel_name, channel_id}`; the Python side declares `on_data_channel_open: Optional[ft.EventHandler[ft.DataChannelOpenEvent]]` and captures the channel via `self.get_data_channel(e.channel_id)`. Backed by a dedicated `PythonBridge` per channel in embedded native mode (4–7 GiB/s on M2 Pro) and by the default `ProtocolMuxedDataChannelFactory` in dev / web modes (raw-byte frames muxed over the active Flet protocol transport with a 1-byte type discriminator). Pyodide gets zero-copy outbound sends via `postMessage` Transferable ArrayBuffer. First consumer: `flet-charts` `MatplotlibChartCanvas`, migrated from `_invoke_method` PNG dispatch to a 1-byte-opcode data channel by @FeodorFitsner. * **In-process Python transport (`dart_bridge` FFI).** `package:flet` gains a third protocol transport alongside the UDS / TCP socket servers: it can run Flet's MsgPack protocol over an in-process `dart_bridge` byte channel via a `FletApp(channelBuilder: …)` seam (the `flet` package stays Python-independent — it doesn't depend on `serious_python` or know about `PythonBridge`; the embedder wires the channel). `serious_python` >= 3.0.0 uses this seam to embed the Python interpreter **in-process** instead of talking to it over a localhost socket, and the `flet build` template migrates from sockets to the FFI transport. On Android, where the OS may keep the process alive across a back-button quit and restart only the Dart VM, the transport rebinds to the new VM's `dart_bridge` ports on a session-restart signal (`libdart_bridge` >= 1.3.0) — the Python process and its in-memory state are preserved and the Flet session is rebuilt from `REGISTER_CLIENT` by @FeodorFitsner. From 2aeb08a412ee57594cecff03d4f7afcf023e218d Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Wed, 8 Jul 2026 17:42:49 +0200 Subject: [PATCH 16/19] fix(build): splice {module_name} into the boot script via jsonEncode too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The jsonEncode refactor's comment promised "every dynamic value is spliced into the boot script through jsonEncode", but {module_name} was still inserted raw inside hand-written quotes. No live bug — the module name must already survive the cookiecutter render and find_spec(), so hostile values can't reach it — but the exception made the comment wrong and left a footgun for future entry-point changes (e.g. dotted or path-like module names). Encode moduleName like the other values; the Python template now calls _sp_run_module_as_main({module_name}) without surrounding quotes, since the JSON string literal brings its own. Addresses the review note on #6662 (discussion_r3544535152). The rendered boot script is byte-checked to compile after all substitutions. --- .../build/{{cookiecutter.out_dir}}/lib/native_runtime.dart | 2 +- .../templates/build/{{cookiecutter.out_dir}}/lib/python.dart | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/native_runtime.dart b/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/native_runtime.dart index 005ede2ca8..e3124dbd24 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/native_runtime.dart +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/native_runtime.dart @@ -110,7 +110,7 @@ Future runPython({ // backslashes (Windows paths), quotes, and non-ASCII. var script = pythonScript .replaceAll('{outLogFilename}', jsonEncode(outLogFilename)) - .replaceAll('{module_name}', moduleName) + .replaceAll('{module_name}', jsonEncode(moduleName)) .replaceAll('{argv}', jsonEncode(args.isNotEmpty ? args : [""])) .replaceAll('{host_executable}', jsonEncode(Platform.resolvedExecutable)); diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/python.dart b/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/python.dart index acbb76d63f..b24c3f7ff7 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/python.dart +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/python.dart @@ -243,7 +243,8 @@ try: sys._base_executable = _sp_host_exe # Execute the configured app module as the real __main__ module. - _sp_run_module_as_main("{module_name}") + # {module_name} is spliced in as a JSON string literal (quotes included). + _sp_run_module_as_main({module_name}) except Exception as e: ex = e traceback.print_exception(e) From 66f6dec3a9cac66255722cb30285875cd354c73f Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Wed, 8 Jul 2026 17:42:50 +0200 Subject: [PATCH 17/19] docs: drop leftover debug prints from the persistent-worker example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit calc_worker still carried two print() calls from local testing. They would also be misleading in a packaged app, where worker stdout isn't connected to the app's console log — which the cookbook page itself points out. --- .../examples/cookbook/multiprocessing/persistent_worker.py | 2 -- .../templates/build/{{cookiecutter.out_dir}}/lib/python.dart | 1 - 2 files changed, 3 deletions(-) diff --git a/sdk/python/examples/cookbook/multiprocessing/persistent_worker.py b/sdk/python/examples/cookbook/multiprocessing/persistent_worker.py index 8c861f6af1..3531c832c1 100644 --- a/sdk/python/examples/cookbook/multiprocessing/persistent_worker.py +++ b/sdk/python/examples/cookbook/multiprocessing/persistent_worker.py @@ -13,11 +13,9 @@ def calc_worker(conn) -> None: and reused by every job. Receiving `None` is the shutdown signal. """ while (job := conn.recv()) is not None: - print("entering while loop", job) started = time.perf_counter() result = sum(i * i for i in range(job)) conn.send((result, time.perf_counter() - started)) - print("closing connection") conn.close() diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/python.dart b/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/python.dart index b24c3f7ff7..b141272535 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/python.dart +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/python.dart @@ -243,7 +243,6 @@ try: sys._base_executable = _sp_host_exe # Execute the configured app module as the real __main__ module. - # {module_name} is spliced in as a JSON string literal (quotes included). _sp_run_module_as_main({module_name}) except Exception as e: ex = e From c44b7ed6286fac35fde53e9f440c9d25e96c1e22 Mon Sep 17 00:00:00 2001 From: ndonkoHenri Date: Wed, 8 Jul 2026 18:27:52 +0200 Subject: [PATCH 18/19] docs: fix relative links broken by the breaking-changes version folders Moving the v0.85/v0.86 breaking-change pages into v0-85-0/ and v0-86-0/ subfolders (876a006fb) was a pure rename, so every relative link inside them started resolving one directory too shallow and the site build failed its broken-links check ("Docusaurus found broken links!"): - "](.)" pointed at the (non-existent) version-folder index instead of the breaking-changes index -> now ../index.md - ../release-notes.md -> ../../release-notes.md - ../../{cli,reference,cookbook}/... -> ../../../{cli,reference,cookbook}/... All nine moved pages updated; every relative .md link target verified to exist, and a full local `docusaurus build` passes the broken-links check again. --- .../deprecated-drag-target-event-coordinates.md | 4 ++-- .../v0-85-0/deprecated-video-apis.md | 4 ++-- .../v0-85-0/removed-spacing-border-helpers.md | 4 ++-- .../v0-86-0/app-files-unpacked-read-only-bundle.md | 12 ++++++------ .../v0-86-0/compile-on-by-default.md | 4 ++-- .../v0-86-0/data-channel-protocol-upgrade.md | 4 ++-- .../v0-86-0/default-bundled-python-3-14.md | 4 ++-- .../v0-86-0/deprecated-clear-cache-flag.md | 14 +++++++------- .../v0-86-0/removed-pyodide-version-export.md | 4 ++-- 9 files changed, 27 insertions(+), 27 deletions(-) diff --git a/website/docs/updates/breaking-changes/v0-85-0/deprecated-drag-target-event-coordinates.md b/website/docs/updates/breaking-changes/v0-85-0/deprecated-drag-target-event-coordinates.md index 43703876cc..51a70b0207 100644 --- a/website/docs/updates/breaking-changes/v0-85-0/deprecated-drag-target-event-coordinates.md +++ b/website/docs/updates/breaking-changes/v0-85-0/deprecated-drag-target-event-coordinates.md @@ -8,7 +8,7 @@ title: "DragTargetEvent coordinate fields deprecated" This guide is accurate as of Flet 0.85.0. Later releases might add new APIs or additional migration paths. -The [breaking changes and deprecations index](.) lists the guides created for each release. +The [breaking changes and deprecations index](../index.md) lists the guides created for each release. ::: ## Summary @@ -62,4 +62,4 @@ If your code needs page-level coordinates instead, use - API documentation: [`DragTargetEvent`][flet.DragTargetEvent] - Issues and PRs: [#6387](https://github.com/flet-dev/flet/issues/6387), [#6401](https://github.com/flet-dev/flet/pull/6401) -- Release notes: [Flet 0.85.0](../release-notes.md#085x) +- Release notes: [Flet 0.85.0](../../release-notes.md#085x) diff --git a/website/docs/updates/breaking-changes/v0-85-0/deprecated-video-apis.md b/website/docs/updates/breaking-changes/v0-85-0/deprecated-video-apis.md index c49986ef81..10580e1164 100644 --- a/website/docs/updates/breaking-changes/v0-85-0/deprecated-video-apis.md +++ b/website/docs/updates/breaking-changes/v0-85-0/deprecated-video-apis.md @@ -8,7 +8,7 @@ title: "Video control APIs deprecated" This guide is accurate as of Flet 0.85.0. Later releases might add new APIs or additional migration paths. -The [breaking changes and deprecations index](.) lists the guides created for each release. +The [breaking changes and deprecations index](../index.md) lists the guides created for each release. ::: ## Summary @@ -81,4 +81,4 @@ been added to the page. - API documentation: [`Video`][flet_video.Video] - Issues and PRs: [PR #6463](https://github.com/flet-dev/flet/pull/6463) -- Release notes: [Flet 0.85.0](../release-notes.md#085x) +- Release notes: [Flet 0.85.0](../../release-notes.md#085x) diff --git a/website/docs/updates/breaking-changes/v0-85-0/removed-spacing-border-helpers.md b/website/docs/updates/breaking-changes/v0-85-0/removed-spacing-border-helpers.md index cdad6bdb9e..de24bfee25 100644 --- a/website/docs/updates/breaking-changes/v0-85-0/removed-spacing-border-helpers.md +++ b/website/docs/updates/breaking-changes/v0-85-0/removed-spacing-border-helpers.md @@ -8,7 +8,7 @@ title: "Deprecated spacing and border helper functions removed" This guide is accurate as of Flet 0.85.0. Later releases might add new APIs or additional migration paths. -The [breaking changes and deprecations index](.) lists the guides created for each release. +The [breaking changes and deprecations index](../index.md) lists the guides created for each release. ::: ## Summary @@ -74,4 +74,4 @@ card = ft.Container( - API documentation: [`Border`][flet.Border], [`BorderRadius`][flet.BorderRadius], [`Padding`][flet.Padding] - Issues and PRs: [#6425](https://github.com/flet-dev/flet/pull/6425) -- Release notes: [Flet 0.85.0](../release-notes.md#085x) +- Release notes: [Flet 0.85.0](../../release-notes.md#085x) diff --git a/website/docs/updates/breaking-changes/v0-86-0/app-files-unpacked-read-only-bundle.md b/website/docs/updates/breaking-changes/v0-86-0/app-files-unpacked-read-only-bundle.md index 8ca4d4867a..fa0b2ded40 100644 --- a/website/docs/updates/breaking-changes/v0-86-0/app-files-unpacked-read-only-bundle.md +++ b/website/docs/updates/breaking-changes/v0-86-0/app-files-unpacked-read-only-bundle.md @@ -8,7 +8,7 @@ title: "App files ship unpacked in a read-only bundle; storage dirs reworked" This guide is accurate as of Flet 0.86.0. Later releases might add new APIs or additional migration paths. -The [breaking changes and deprecations index](.) lists the guides created for each release. +The [breaking changes and deprecations index](../index.md) lists the guides created for each release. ::: ## Summary @@ -114,8 +114,8 @@ ignored by Git automatically; the old visible `storage/` directory is no longer ## References -- Environment variables: [`FLET_APP_STORAGE_DATA`](../../reference/environment-variables.md#flet_app_storage_data), - [`FLET_APP_STORAGE_CACHE`](../../reference/environment-variables.md#flet_app_storage_cache), - [`FLET_APP_STORAGE_TEMP`](../../reference/environment-variables.md#flet_app_storage_temp) -- Cookbook: [Read and write files](../../cookbook/read-and-write-files.md) -- Release notes: [Flet 0.86.0](../release-notes.md) +- Environment variables: [`FLET_APP_STORAGE_DATA`](../../../reference/environment-variables.md#flet_app_storage_data), + [`FLET_APP_STORAGE_CACHE`](../../../reference/environment-variables.md#flet_app_storage_cache), + [`FLET_APP_STORAGE_TEMP`](../../../reference/environment-variables.md#flet_app_storage_temp) +- Cookbook: [Read and write files](../../../cookbook/read-and-write-files.md) +- Release notes: [Flet 0.86.0](../../release-notes.md) diff --git a/website/docs/updates/breaking-changes/v0-86-0/compile-on-by-default.md b/website/docs/updates/breaking-changes/v0-86-0/compile-on-by-default.md index e62778e70d..bf5ee97629 100644 --- a/website/docs/updates/breaking-changes/v0-86-0/compile-on-by-default.md +++ b/website/docs/updates/breaking-changes/v0-86-0/compile-on-by-default.md @@ -8,7 +8,7 @@ title: "App and packages are compiled to .pyc by default" This guide is accurate as of Flet 0.86.0. Later releases might add new APIs or additional migration paths. -The [breaking changes and deprecations index](.) lists the guides created for each release. +The [breaking changes and deprecations index](../index.md) lists the guides created for each release. ::: ## Summary @@ -75,4 +75,4 @@ resolution order is CLI flag → `[tool.flet..compile]` → - API documentation: [Compilation and cleanup](/docs/publish#compilation-and-cleanup) - Issues and PRs: [#6598](https://github.com/flet-dev/flet/pull/6598) -- Release notes: [Flet 0.86.0](../release-notes.md) +- Release notes: [Flet 0.86.0](../../release-notes.md) diff --git a/website/docs/updates/breaking-changes/v0-86-0/data-channel-protocol-upgrade.md b/website/docs/updates/breaking-changes/v0-86-0/data-channel-protocol-upgrade.md index 588165486f..c0ea0ded5b 100644 --- a/website/docs/updates/breaking-changes/v0-86-0/data-channel-protocol-upgrade.md +++ b/website/docs/updates/breaking-changes/v0-86-0/data-channel-protocol-upgrade.md @@ -8,7 +8,7 @@ title: "Flet protocol framing upgraded for DataChannel support" This guide is accurate as of Flet 0.86.0. Later releases might add new APIs or additional migration paths. -The [breaking changes and deprecations index](.) lists the guides created for each release. +The [breaking changes and deprecations index](../index.md) lists the guides created for each release. ::: ## Summary @@ -108,4 +108,4 @@ the channel directly. ## References - API: `flet.DataChannel` (Python) and `FletBackend.openDataChannel` (Dart) — see the module docstrings in `flet/data_channel.py`; dedicated reference pages will be added in a follow-up. -- Release notes: [Flet 0.86.0](../release-notes.md) +- Release notes: [Flet 0.86.0](../../release-notes.md) diff --git a/website/docs/updates/breaking-changes/v0-86-0/default-bundled-python-3-14.md b/website/docs/updates/breaking-changes/v0-86-0/default-bundled-python-3-14.md index c9a8d8cff3..61ad2e3d8d 100644 --- a/website/docs/updates/breaking-changes/v0-86-0/default-bundled-python-3-14.md +++ b/website/docs/updates/breaking-changes/v0-86-0/default-bundled-python-3-14.md @@ -8,7 +8,7 @@ title: "Default bundled Python version is now 3.14" This guide is accurate as of Flet 0.86.0. Later releases might add new APIs or additional migration paths. -The [breaking changes and deprecations index](.) lists the guides created for each release. +The [breaking changes and deprecations index](../index.md) lists the guides created for each release. ::: ## Summary @@ -86,4 +86,4 @@ job. - API documentation: [Choosing a Python version](/docs/publish#choosing-a-python-version) - Issues and PRs: [#6577](https://github.com/flet-dev/flet/pull/6577) -- Release notes: [Flet 0.86.0](../release-notes.md) +- Release notes: [Flet 0.86.0](../../release-notes.md) diff --git a/website/docs/updates/breaking-changes/v0-86-0/deprecated-clear-cache-flag.md b/website/docs/updates/breaking-changes/v0-86-0/deprecated-clear-cache-flag.md index a10fd99812..a3aec1aef1 100644 --- a/website/docs/updates/breaking-changes/v0-86-0/deprecated-clear-cache-flag.md +++ b/website/docs/updates/breaking-changes/v0-86-0/deprecated-clear-cache-flag.md @@ -8,15 +8,15 @@ title: "flet build --clear-cache flag deprecated" This guide is accurate as of Flet 0.86.0. Later releases might add new APIs or additional migration paths. -The [breaking changes and deprecations index](.) lists the guides created for each release. +The [breaking changes and deprecations index](../index.md) lists the guides created for each release. ::: ## Summary Flet 0.86.0 deprecated the `--clear-cache` flag of -[`flet build`](../../cli/flet-build.md) and [`flet debug`](../../cli/flet-debug.md). +[`flet build`](../../../cli/flet-build.md) and [`flet debug`](../../../cli/flet-debug.md). -Use the new [`flet clean`](../../cli/flet-clean.md) command instead. +Use the new [`flet clean`](../../../cli/flet-clean.md) command instead. ## Context @@ -24,7 +24,7 @@ Use the new [`flet clean`](../../cli/flet-clean.md) command instead. and only when the build template hash had changed — so it was a no-op in the common case and never reset the rest of the `build` directory. -The new [`flet clean`](../../cli/flet-clean.md) command deletes the entire +The new [`flet clean`](../../../cli/flet-clean.md) command deletes the entire `build` directory (the Flutter bootstrap project, cached artifacts, hash stamps, and generated output) unconditionally, giving a predictable, full reset before the next build. @@ -44,7 +44,7 @@ flet clean flet build web ``` -`flet clean` accepts an optional [path](../../cli/flet-clean.md#python_app_path) to the app directory (defaults to the +`flet clean` accepts an optional [path](../../../cli/flet-clean.md#python_app_path) to the app directory (defaults to the current directory), so you can target another project with `flet clean path/to/app`. ## Timeline @@ -54,6 +54,6 @@ current directory), so you can target another project with `flet clean path/to/a ## References -- CLI documentation: [`flet clean`](../../cli/flet-clean.md), [`flet build`](../../cli/flet-build.md) +- CLI documentation: [`flet clean`](../../../cli/flet-clean.md), [`flet build`](../../../cli/flet-build.md) - Issues and PRs: [#6233](https://github.com/flet-dev/flet/issues/6233) -- Release notes: [Flet 0.86.0](../release-notes.md) +- Release notes: [Flet 0.86.0](../../release-notes.md) diff --git a/website/docs/updates/breaking-changes/v0-86-0/removed-pyodide-version-export.md b/website/docs/updates/breaking-changes/v0-86-0/removed-pyodide-version-export.md index ec6e96f870..cbc344ca77 100644 --- a/website/docs/updates/breaking-changes/v0-86-0/removed-pyodide-version-export.md +++ b/website/docs/updates/breaking-changes/v0-86-0/removed-pyodide-version-export.md @@ -8,7 +8,7 @@ title: "flet.version.pyodide_version and PYODIDE_VERSION removed" This guide is accurate as of Flet 0.86.0. Later releases might add new APIs or additional migration paths. -The [breaking changes and deprecations index](.) lists the guides created for each release. +The [breaking changes and deprecations index](../index.md) lists the guides created for each release. ::: ## Summary @@ -75,4 +75,4 @@ print(f"Default Pyodide: {release.pyodide}") - API documentation: [Choosing a Python version](/docs/publish#choosing-a-python-version) - Issues and PRs: [#6577](https://github.com/flet-dev/flet/pull/6577) -- Release notes: [Flet 0.86.0](../release-notes.md) +- Release notes: [Flet 0.86.0](../../release-notes.md) From 42b5268606f5e150d932a2531c2b20763c546b01 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Wed, 8 Jul 2026 19:57:55 -0700 Subject: [PATCH 19/19] Bump python build release and serious_python Update the pinned python-build release date to 20260708 and raise the build template's `serious_python` dependency to 4.3.0 to keep the Python packaging stack in sync. --- .../packages/flet-cli/src/flet_cli/utils/python_versions.py | 2 +- .../templates/build/{{cookiecutter.out_dir}}/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/python/packages/flet-cli/src/flet_cli/utils/python_versions.py b/sdk/python/packages/flet-cli/src/flet_cli/utils/python_versions.py index 839244a804..6c550e8f8c 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/utils/python_versions.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/utils/python_versions.py @@ -28,7 +28,7 @@ # python-build release this flet pins. Keep in sync with serious_python's # `pythonReleaseDate` (lib/src/python_versions.dart) — both should track the # same python-build release. -PYTHON_BUILD_RELEASE_DATE = "20260701" +PYTHON_BUILD_RELEASE_DATE = "20260708" RELEASE_DATE_ENV = "FLET_PYTHON_BUILD_RELEASE_DATE" MANIFEST_PATH_ENV = "FLET_PYTHON_BUILD_MANIFEST" diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml b/sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml index cbd7147eec..1128e20407 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml @@ -18,7 +18,7 @@ dependencies: flet: path: ../../../../../packages/flet - serious_python: 4.2.1 + serious_python: 4.3.0 # MsgPack codec used by the dart_bridge FletBackendChannel implementation # in lib/main.dart — matches the wire format flet's existing socket