diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 667dca93..7a165aab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,12 +65,13 @@ jobs: fi bridge_example_macos: - name: Test Bridge example on macOS (Python ${{ matrix.python_version }}) + name: Test Bridge example on macOS (${{ matrix.build_system }}, Python ${{ matrix.python_version }}) runs-on: macos-26 strategy: fail-fast: false matrix: python_version: ['3.12', '3.13', '3.14'] + build_system: ['cocoapods', 'spm'] env: SERIOUS_PYTHON_VERSION: ${{ matrix.python_version }} steps: @@ -87,15 +88,34 @@ jobs: uses: actions/cache@v5 with: path: ~/.flet/cache - key: flet-cache-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python_version }}-${{ hashFiles('src/serious_python/lib/src/python_versions.dart', 'src/serious_python/bin/package_command.dart', 'src/serious_python_android/android/build.gradle', 'src/serious_python_darwin/darwin/prepare_macos.sh', 'src/serious_python_darwin/darwin/prepare_ios.sh', 'src/serious_python_windows/windows/CMakeLists.txt', 'src/serious_python_linux/linux/CMakeLists.txt') }} + key: flet-cache-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python_version }}-${{ hashFiles('src/serious_python/lib/src/python_versions.dart', 'src/serious_python/bin/package_command.dart', 'src/serious_python_android/android/build.gradle.kts', 'src/serious_python_darwin/darwin/prepare_macos.sh', 'src/serious_python_darwin/darwin/prepare_ios.sh', 'src/serious_python_windows/windows/CMakeLists.txt', 'src/serious_python_linux/linux/CMakeLists.txt') }} restore-keys: | flet-cache-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python_version }}- flet-cache-${{ runner.os }}-${{ runner.arch }}- + - name: Configure ${{ matrix.build_system }} + run: | + if [ "${{ matrix.build_system }}" = "spm" ]; then + flutter config --enable-swift-package-manager + else + flutter config --no-enable-swift-package-manager + fi + - name: Package + run integration test working-directory: "src/serious_python/example/bridge_example" run: | + # SPM is the package command's default: it does host-side staging and + # writes the SP_NATIVE_SET cache-bust key (we export it into the build). + # CocoaPods opts out via SERIOUS_PYTHON_DARWIN_SPM=false; the podspec + # prepare_command stages then. (The SPM job leaves the var unset so it + # exercises the default path docs/readme commands use.) + if [ "${{ matrix.build_system }}" = "cocoapods" ]; then + export SERIOUS_PYTHON_DARWIN_SPM=false + fi dart run serious_python:main package app/src --platform Darwin --python-version ${{ matrix.python_version }} + if [ "${{ matrix.build_system }}" = "spm" ]; then + export SP_NATIVE_SET="$(cat build/.serious_python_spm_key)" + fi # Each test file is invoked separately. `flutter test integration_test` # over the directory reuses one VM session, but the second file's # `runApp()` then trips "Unable to start the app on the device" — @@ -107,13 +127,14 @@ jobs: flutter test integration_test/memory_test.dart -d macos --dart-define=EXPECTED_PYTHON_VERSION=${{ matrix.python_version }} bridge_example_ios: - name: Test Bridge example on iOS (Python ${{ matrix.python_version }}) + name: Test Bridge example on iOS (${{ matrix.build_system }}, Python ${{ matrix.python_version }}) runs-on: macos-26 timeout-minutes: 25 strategy: fail-fast: false matrix: python_version: ['3.12', '3.13', '3.14'] + build_system: ['cocoapods', 'spm'] env: SERIOUS_PYTHON_VERSION: ${{ matrix.python_version }} steps: @@ -130,7 +151,7 @@ jobs: uses: actions/cache@v5 with: path: ~/.flet/cache - key: flet-cache-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python_version }}-${{ hashFiles('src/serious_python/lib/src/python_versions.dart', 'src/serious_python/bin/package_command.dart', 'src/serious_python_android/android/build.gradle', 'src/serious_python_darwin/darwin/prepare_macos.sh', 'src/serious_python_darwin/darwin/prepare_ios.sh', 'src/serious_python_windows/windows/CMakeLists.txt', 'src/serious_python_linux/linux/CMakeLists.txt') }} + key: flet-cache-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python_version }}-${{ hashFiles('src/serious_python/lib/src/python_versions.dart', 'src/serious_python/bin/package_command.dart', 'src/serious_python_android/android/build.gradle.kts', 'src/serious_python_darwin/darwin/prepare_macos.sh', 'src/serious_python_darwin/darwin/prepare_ios.sh', 'src/serious_python_windows/windows/CMakeLists.txt', 'src/serious_python_linux/linux/CMakeLists.txt') }} restore-keys: | flet-cache-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python_version }}- flet-cache-${{ runner.os }}-${{ runner.arch }}- @@ -145,17 +166,34 @@ jobs: shutdown_after_job: true wait_for_boot: true + - name: Configure ${{ matrix.build_system }} + run: | + if [ "${{ matrix.build_system }}" = "spm" ]; then + flutter config --enable-swift-package-manager + else + flutter config --no-enable-swift-package-manager + fi + - name: Package + run integration test working-directory: "src/serious_python/example/bridge_example" run: | ts() { date '+%H:%M:%S'; } + # SPM is the default; CocoaPods opts out (the SPM job leaves the var + # unset to exercise the default path docs/readme commands use). + if [ "${{ matrix.build_system }}" = "cocoapods" ]; then + export SERIOUS_PYTHON_DARWIN_SPM=false + fi echo "[$(ts)] >>> dart run serious_python:main package" # certifi is a placeholder requirement: serious_python_darwin's # sync_site_packages.sh only populates dist_ios/site-xcframeworks - # (which bundle-python-frameworks-ios.sh then requires at build - # time) when iOS-specific site-packages subdirs exist. Empty - # --requirements skips that branch and the build fails. + # (the iOS native C-extensions — embedded by the podspec script_phase + # under CocoaPods, or enumerated into extra-xcframeworks under SPM) + # when iOS-specific site-packages subdirs exist. Empty --requirements + # skips that branch and the build fails. dart run serious_python:main package app/src --platform iOS --python-version ${{ matrix.python_version }} --requirements certifi + if [ "${{ matrix.build_system }}" = "spm" ]; then + export SP_NATIVE_SET="$(cat build/.serious_python_spm_key)" + fi echo "[$(ts)] >>> flutter test integration_test (per-file)" # See macOS job for why each file runs as a separate invocation. flutter test integration_test/interactivity_test.dart --device-id ${{ steps.simulator.outputs.udid }} --dart-define=EXPECTED_PYTHON_VERSION=${{ matrix.python_version }} @@ -188,7 +226,7 @@ jobs: uses: actions/cache@v5 with: path: ~/.flet/cache - key: flet-cache-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python_version }}-${{ hashFiles('src/serious_python/lib/src/python_versions.dart', 'src/serious_python/bin/package_command.dart', 'src/serious_python_android/android/build.gradle', 'src/serious_python_darwin/darwin/prepare_macos.sh', 'src/serious_python_darwin/darwin/prepare_ios.sh', 'src/serious_python_windows/windows/CMakeLists.txt', 'src/serious_python_linux/linux/CMakeLists.txt') }} + key: flet-cache-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python_version }}-${{ hashFiles('src/serious_python/lib/src/python_versions.dart', 'src/serious_python/bin/package_command.dart', 'src/serious_python_android/android/build.gradle.kts', 'src/serious_python_darwin/darwin/prepare_macos.sh', 'src/serious_python_darwin/darwin/prepare_ios.sh', 'src/serious_python_windows/windows/CMakeLists.txt', 'src/serious_python_linux/linux/CMakeLists.txt') }} restore-keys: | flet-cache-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python_version }}- flet-cache-${{ runner.os }}-${{ runner.arch }}- @@ -286,7 +324,7 @@ jobs: uses: actions/cache@v5 with: path: ~/.flet/cache - key: flet-cache-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python_version }}-${{ hashFiles('src/serious_python/lib/src/python_versions.dart', 'src/serious_python/bin/package_command.dart', 'src/serious_python_android/android/build.gradle', 'src/serious_python_darwin/darwin/prepare_macos.sh', 'src/serious_python_darwin/darwin/prepare_ios.sh', 'src/serious_python_windows/windows/CMakeLists.txt', 'src/serious_python_linux/linux/CMakeLists.txt') }} + key: flet-cache-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python_version }}-${{ hashFiles('src/serious_python/lib/src/python_versions.dart', 'src/serious_python/bin/package_command.dart', 'src/serious_python_android/android/build.gradle.kts', 'src/serious_python_darwin/darwin/prepare_macos.sh', 'src/serious_python_darwin/darwin/prepare_ios.sh', 'src/serious_python_windows/windows/CMakeLists.txt', 'src/serious_python_linux/linux/CMakeLists.txt') }} restore-keys: | flet-cache-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python_version }}- flet-cache-${{ runner.os }}-${{ runner.arch }}- @@ -364,7 +402,7 @@ jobs: uses: actions/cache@v5 with: path: ~/.flet/cache - key: flet-cache-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python_version }}-${{ hashFiles('src/serious_python/lib/src/python_versions.dart', 'src/serious_python/bin/package_command.dart', 'src/serious_python_android/android/build.gradle', 'src/serious_python_darwin/darwin/prepare_macos.sh', 'src/serious_python_darwin/darwin/prepare_ios.sh', 'src/serious_python_windows/windows/CMakeLists.txt', 'src/serious_python_linux/linux/CMakeLists.txt') }} + key: flet-cache-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python_version }}-${{ hashFiles('src/serious_python/lib/src/python_versions.dart', 'src/serious_python/bin/package_command.dart', 'src/serious_python_android/android/build.gradle.kts', 'src/serious_python_darwin/darwin/prepare_macos.sh', 'src/serious_python_darwin/darwin/prepare_ios.sh', 'src/serious_python_windows/windows/CMakeLists.txt', 'src/serious_python_linux/linux/CMakeLists.txt') }} restore-keys: | flet-cache-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python_version }}- flet-cache-${{ runner.os }}-${{ runner.arch }}- diff --git a/src/serious_python/CHANGELOG.md b/src/serious_python/CHANGELOG.md index a8bb99fc..ab930e35 100644 --- a/src/serious_python/CHANGELOG.md +++ b/src/serious_python/CHANGELOG.md @@ -4,6 +4,7 @@ * **New `SeriousPython.prepareApp()`** — materializes the app (Android first-launch unpack) and returns the directory containing its entry point. **`SeriousPython.run()` now takes no `assetPath` argument** (it resolves the app via `prepareApp()`), sets the current directory to a writable per-app data dir (`/data`) — not the read-only bundle — so relative file writes / SQLite work, and runs `main.pyc`/`main.py` (or `appFileName`). * **Breaking change:** the `app.zip` asset convention and the runtime zip-extraction API are removed — `SeriousPython.run("app/app.zip")`, `extractAssetZip`, and `extractFileZip` no longer exist. Repackage with `dart run serious_python:main package -p ` and call `SeriousPython.run()` with no arguments. * **Android:** the runtime payload moved to `/flet/{app, stdlib.zip, sitepackages.zip, extract/}` (resolved via `getApplicationSupportDirectory()`; the custom `getFilesDir` method channel is dropped). User data in the sibling `/data` survives app updates. +* **Swift Package Manager (darwin) staging in the `package` command — on by default.** For iOS/macOS the `package` command runs the host-side equivalent of the podspec `prepare_command` (which SPM has no hook for) by resolving `serious_python_darwin`'s `darwin/` dir (`SERIOUS_PYTHON_DARWIN_DIR` override, else the project's `package_config.json`), invoking `prepare_spm.sh`, and writing the `SP_NATIVE_SET` cache-bust key to `build/.serious_python_spm_key` (overridable via `SERIOUS_PYTHON_SPM_KEY_FILE`) for the caller to export into the `flutter build` environment. SPM is Flutter's default darwin integration since 3.44, so this happens by default — set **`SERIOUS_PYTHON_DARWIN_SPM`** to a falsy value (`0`/`false`/`no`/`off`) to opt out and build with CocoaPods (the podspec stages then). See `serious_python_darwin` 4.0.0. ## 3.0.0 diff --git a/src/serious_python/bin/package_command.dart b/src/serious_python/bin/package_command.dart index c5dcca9d..a3ed797a 100644 --- a/src/serious_python/bin/package_command.dart +++ b/src/serious_python/bin/package_command.dart @@ -29,6 +29,19 @@ const flutterPackagesFlutterEnvironmentVariable = "SERIOUS_PYTHON_FLUTTER_PACKAGES"; const allowSourceDistrosEnvironmentVariable = "SERIOUS_PYTHON_ALLOW_SOURCE_DISTRIBUTIONS"; +// Swift Package Manager (darwin) host-side staging. For iOS/macOS the package +// command runs the SPM equivalent of the podspec `prepare_command` — assembling +// the dist and mapping it into the plugin's Package.swift layout — since SPM has +// no pod-install hook. SPM is Flutter's default darwin integration since 3.44, so +// this happens **by default**; set `SERIOUS_PYTHON_DARWIN_SPM` to a falsy value +// (0/false/no/off) to opt out and build with CocoaPods (e.g. `flet build` sets it +// false when the app uses a non-SPM plugin). `SERIOUS_PYTHON_DARWIN_DIR` optionally +// overrides the resolved plugin `darwin/` dir; `SERIOUS_PYTHON_SPM_KEY_FILE` overrides +// where the SP_NATIVE_SET cache-bust key is written for the caller to export into the +// `flutter build` environment. +const darwinSpmEnvironmentVariable = "SERIOUS_PYTHON_DARWIN_SPM"; +const darwinDirEnvironmentVariable = "SERIOUS_PYTHON_DARWIN_DIR"; +const spmKeyFileEnvironmentVariable = "SERIOUS_PYTHON_SPM_KEY_FILE"; // Python runtime version data — `defaultPythonVersion`, `pythonReleases`, the // `*EnvironmentVariable` names, `dartBridgeVersion`, `pythonReleaseDate` — lives @@ -564,6 +577,17 @@ class PackageCommand extends Command { } await appStagingDir.create(recursive: true); await copyDirectory(tempDir, appStagingDir, tempDir.path, []); + + // Swift Package Manager (darwin) host-side staging: the podspec + // prepare_command doesn't run under SPM, so assemble the dist and map it + // into the plugin's Package.swift layout here (app is now staged). SPM is + // Flutter's default darwin integration since 3.44, so this runs **by + // default**; set `SERIOUS_PYTHON_DARWIN_SPM` to a falsy value (0/false/ + // no/off) to opt out and build with CocoaPods (the podspec stages then). + if ((platform == "iOS" || platform == "Darwin") && + !_isFalsy(Platform.environment[darwinSpmEnvironmentVariable])) { + await _stageDarwinSpm(platform, currentPath); + } } } catch (e) { stdout.writeln("Error: $e"); @@ -643,6 +667,65 @@ class PackageCommand extends Command { return proc.exitCode; } + static bool _isFalsy(String? v) => + v != null && const ["0", "false", "no", "off"].contains(v.toLowerCase()); + + // Run the darwin SPM staging (prepare_spm.sh: assemble dist + map into the + // plugin's Package.swift layout) and persist the SP_NATIVE_SET cache-bust key + // for `flet build` to export into the `flutter build` environment. + Future _stageDarwinSpm(String platform, String projectPath) async { + final darwinDir = await _resolveDarwinDir(projectPath); + if (darwinDir == null) { + stdout.writeln("SPM staging skipped: could not resolve serious_python_darwin " + "(set $darwinDirEnvironmentVariable or ensure " + ".dart_tool/package_config.json is present)."); + return; + } + final spmPlatform = platform == "iOS" ? "ios" : "macos"; + final script = path.join(darwinDir, "prepare_spm.sh"); + stdout.writeln("SPM: staging $spmPlatform via $script"); + final result = await Process.run("/bin/sh", [script, spmPlatform], + workingDirectory: darwinDir); + if ((result.stderr as String).isNotEmpty) { + verbose(result.stderr as String); + } + if (result.exitCode != 0) { + throw Exception("prepare_spm.sh failed (exit ${result.exitCode}):\n" + "${result.stderr}"); + } + // stage_spm.sh prints the key as its last stdout line. + final key = (result.stdout as String) + .trim() + .split("\n") + .where((l) => l.trim().isNotEmpty) + .last + .trim(); + final keyFile = Platform.environment[spmKeyFileEnvironmentVariable] ?? + path.join(projectPath, "build", ".serious_python_spm_key"); + await File(keyFile).parent.create(recursive: true); + await File(keyFile).writeAsString(key); + stdout.writeln("SPM: SP_NATIVE_SET=$key -> $keyFile"); + } + + // Resolve serious_python_darwin's `darwin/` directory — an explicit override + // (set by flet) wins, else read the flutter project's package config. + Future _resolveDarwinDir(String projectPath) async { + final override = Platform.environment[darwinDirEnvironmentVariable]; + if (override != null && override.isNotEmpty) return override; + final pc = + File(path.join(projectPath, ".dart_tool", "package_config.json")); + if (!await pc.exists()) return null; + final data = jsonDecode(await pc.readAsString()) as Map; + for (final pkg in (data["packages"] as List)) { + if (pkg["name"] == "serious_python_darwin") { + final base = Uri.directory(path.join(projectPath, ".dart_tool")); + final root = base.resolve(pkg["rootUri"] as String).toFilePath(); + return path.join(root, "darwin"); + } + } + return null; + } + Future zipDirectoryPosix(Directory source, File dest) async { final encoder = ZipFileEncoder(); encoder.create(dest.path); diff --git a/src/serious_python/example/bridge_example/ios/Podfile.lock b/src/serious_python/example/bridge_example/ios/Podfile.lock index 3542b7d0..88e52bf8 100644 --- a/src/serious_python/example/bridge_example/ios/Podfile.lock +++ b/src/serious_python/example/bridge_example/ios/Podfile.lock @@ -1,22 +1,15 @@ PODS: - Flutter (1.0.0) - - serious_python_darwin (2.0.0): - - Flutter - - FlutterMacOS DEPENDENCIES: - Flutter (from `Flutter`) - - serious_python_darwin (from `.symlinks/plugins/serious_python_darwin/darwin`) EXTERNAL SOURCES: Flutter: :path: Flutter - serious_python_darwin: - :path: ".symlinks/plugins/serious_python_darwin/darwin" SPEC CHECKSUMS: Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 - serious_python_darwin: 6d58c9837595683a71e20114bdd4607568c98e84 PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e diff --git a/src/serious_python/example/bridge_example/ios/Runner.xcodeproj/project.pbxproj b/src/serious_python/example/bridge_example/ios/Runner.xcodeproj/project.pbxproj index 42b493f2..28b7a207 100644 --- a/src/serious_python/example/bridge_example/ios/Runner.xcodeproj/project.pbxproj +++ b/src/serious_python/example/bridge_example/ios/Runner.xcodeproj/project.pbxproj @@ -205,8 +205,6 @@ 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - B2C65F2690B6BBEF6E20F674 /* [CP] Embed Pods Frameworks */, - 1D3A8F88F6DC96CBAD5977F8 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -284,23 +282,6 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 1D3A8F88F6DC96CBAD5977F8 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -354,23 +335,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - B2C65F2690B6BBEF6E20F674 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; F3B1F0D1E68C4E672BCDCEAA /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; diff --git a/src/serious_python/example/bridge_example/macos/Podfile.lock b/src/serious_python/example/bridge_example/macos/Podfile.lock index 8a6bd931..1c915faa 100644 --- a/src/serious_python/example/bridge_example/macos/Podfile.lock +++ b/src/serious_python/example/bridge_example/macos/Podfile.lock @@ -1,23 +1,16 @@ PODS: - FlutterMacOS (1.0.0) - - serious_python_darwin (2.0.0): - - Flutter - - FlutterMacOS DEPENDENCIES: - FlutterMacOS (from `Flutter/ephemeral`) - - serious_python_darwin (from `Flutter/ephemeral/.symlinks/plugins/serious_python_darwin/darwin`) EXTERNAL SOURCES: FlutterMacOS: :path: Flutter/ephemeral - serious_python_darwin: - :path: Flutter/ephemeral/.symlinks/plugins/serious_python_darwin/darwin SPEC CHECKSUMS: FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 - serious_python_darwin: 6d58c9837595683a71e20114bdd4607568c98e84 -PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 +PODFILE CHECKSUM: 346bfb2deb41d4a6ebd6f6799f92188bde2d246f COCOAPODS: 1.14.3 diff --git a/src/serious_python/example/bridge_example/macos/Runner.xcodeproj/project.pbxproj b/src/serious_python/example/bridge_example/macos/Runner.xcodeproj/project.pbxproj index 19a7a771..76bfd1b1 100644 --- a/src/serious_python/example/bridge_example/macos/Runner.xcodeproj/project.pbxproj +++ b/src/serious_python/example/bridge_example/macos/Runner.xcodeproj/project.pbxproj @@ -27,6 +27,7 @@ 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 */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; B72E96C288A175DA4181F1E9 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B9E3AB6184BEAE07C5108EE3 /* Pods_Runner.framework */; }; E3DFE7ECAF91BB85499213B0 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E0FC039480642B362225F49B /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ @@ -82,6 +83,7 @@ 39E5122CF63AF062558CE40F /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 4DDC5FA0C150DCFDA5784BFA /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 7556ABD1A47FA4D65C4B0DFC /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 8B28C1B131F74EC8875EDFCF /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; @@ -103,6 +105,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, B72E96C288A175DA4181F1E9 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -164,6 +167,7 @@ 33CEB47122A05771004F2AC0 /* Flutter */ = { isa = PBXGroup; children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, @@ -240,8 +244,6 @@ 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, - 5019070AFE2F41F3F49E930B /* [CP] Embed Pods Frameworks */, - 583F80258F9FB1DB887F6668 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -249,6 +251,9 @@ 33CC11202044C79F0003C045 /* PBXTargetDependency */, ); name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); productName = Runner; productReference = 33CC10ED2044A3C60003C045 /* serious_python_bridge_example.app */; productType = "com.apple.product-type.application"; @@ -293,6 +298,9 @@ Base, ); mainGroup = 33CC10E42044A3C60003C045; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + ); productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -362,40 +370,6 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; - 5019070AFE2F41F3F49E930B /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 583F80258F9FB1DB887F6668 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; BBA99A121B65ED7E52DABCEE /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -814,6 +788,20 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 33CC10E52044A3C60003C045 /* Project object */; } diff --git a/src/serious_python/example/bridge_example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/src/serious_python/example/bridge_example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 1fb7fdd6..79ee10d3 100644 --- a/src/serious_python/example/bridge_example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/src/serious_python/example/bridge_example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + .sh` + `sync_site_packages.sh`) and `stage_spm.sh` maps it into the package layout — `Python-{ios,macos}.xcframework` + `dart_bridge.xcframework` as local-path binary targets, the iOS native C-extensions enumerated from `extra-xcframeworks/`, and `stdlib`/`site-packages`/`app` as `.copy` resources. On iOS the extensions ship as embedded, signed frameworks (CPython's `.fwork` finder resolves them); on macOS they load flat from the resource trees. + * The manifest reads `SP_NATIVE_SET` (a hash of the staged native set) so SwiftPM re-resolves when requirements / app / Python version change — SwiftPM caches its package graph on manifest text + environment, not on the staged dirs it enumerates. + * The **SPM path** needs Flutter **3.44** / Dart **3.11**; the plugin's minimum is unchanged because `Package.swift` is dormant on older Flutter (which uses the CocoaPods path). * `prepareApp()` returns the app dir from the `python.bundle` resource (`/app`); the app's Python sources ship unpacked as an `app` resource bundle next to `stdlib` + `site-packages` (no first-launch extraction). * Version bump aligning with the `serious_python_*` 4.0.0 release. diff --git a/src/serious_python_darwin/darwin/.gitignore b/src/serious_python_darwin/darwin/.gitignore index 0c885071..52f39d72 100644 --- a/src/serious_python_darwin/darwin/.gitignore +++ b/src/serious_python_darwin/darwin/.gitignore @@ -35,4 +35,15 @@ Icon? /Flutter/Generated.xcconfig /Flutter/ephemeral/ -/Flutter/flutter_export_environment.sh \ No newline at end of file +/Flutter/flutter_export_environment.sh + +# --- SwiftPM staging (materialized by serious_python's package step) --- +serious_python_darwin/Python-ios.xcframework +serious_python_darwin/Python-macos.xcframework +serious_python_darwin/dart_bridge.xcframework +serious_python_darwin/extra-xcframeworks/ +serious_python_darwin/.build/ +serious_python_darwin/.swiftpm/ +# resource trees are staged; keep only the committed .keep placeholders +serious_python_darwin/Sources/serious_python_darwin/Resources/*/* +!serious_python_darwin/Sources/serious_python_darwin/Resources/*/.keep diff --git a/src/serious_python_darwin/darwin/prepare_ios.sh b/src/serious_python_darwin/darwin/prepare_ios.sh index 817ae1ae..279c99b8 100755 --- a/src/serious_python_darwin/darwin/prepare_ios.sh +++ b/src/serious_python_darwin/darwin/prepare_ios.sh @@ -11,7 +11,11 @@ dist=$script_dir/dist_ios # Cross-plugin download cache; see prepare_macos.sh for the convention. cache_root="${FLET_CACHE_DIR:-$HOME/.flet/cache}" -pb_cache="$cache_root/python-build/v$python_full_version" +# Date-keyed: a re-release of the same Python version (same 3.x.y, new build +# date — e.g. a rebuild that only re-signs binaries) downloads fresh instead of +# being served stale from the previous release's cache. dart-bridge stays +# version-keyed (its re-releases bump the version). +pb_cache="$cache_root/python-build/v$python_full_version-$python_build_date" db_cache="$cache_root/dart-bridge/v$dart_bridge_version" mkdir -p "$pb_cache" "$db_cache" @@ -25,17 +29,19 @@ if [ ! -f "$python_ios_dist_path" ]; then fi # Re-extract when $dist is missing OR was assembled for a different Python -# version. The guard used to be `[ ! -d "$dist" ]`, which left a stale dist_ios -# from a previous Python version in place — e.g. bundling 3.12 under 3.14 -# site-packages, which trips C-extension ABI errors ("unknown slot ID") at -# import. A version marker keys the extracted dist to $python_full_version. -marker="$dist/.python_full_version" -if [ ! -d "$dist" ] || [ "$(cat "$marker" 2>/dev/null)" != "$python_full_version" ]; then +# version/release. The guard used to be `[ ! -d "$dist" ]`, which left a stale +# dist_ios from a previous Python version in place — e.g. bundling 3.12 under +# 3.14 site-packages, which trips C-extension ABI errors ("unknown slot ID") at +# import. The marker keys the extracted dist to the version + release date, so a +# same-version re-release (new build date) also re-extracts. +build_id="$python_full_version-$python_build_date" +marker="$dist/.python_build_id" +if [ ! -d "$dist" ] || [ "$(cat "$marker" 2>/dev/null)" != "$build_id" ]; then rm -rf "$dist" mkdir -p "$dist" tar -xzf "$python_ios_dist_path" -C "$dist" mv "$dist/python-stdlib" "$dist/stdlib" - echo "$python_full_version" > "$marker" + echo "$build_id" > "$marker" fi # ---- flet-dev/dart-bridge (xcframework) ----------------------------------- diff --git a/src/serious_python_darwin/darwin/prepare_macos.sh b/src/serious_python_darwin/darwin/prepare_macos.sh index 1e4479ce..251c0a39 100755 --- a/src/serious_python_darwin/darwin/prepare_macos.sh +++ b/src/serious_python_darwin/darwin/prepare_macos.sh @@ -12,7 +12,11 @@ dist=$script_dir/dist_macos # gradle task + flet build's external tooling already use; ~/.flet/cache is # the shared default. Tarballs land here and survive `flutter clean`. cache_root="${FLET_CACHE_DIR:-$HOME/.flet/cache}" -pb_cache="$cache_root/python-build/v$python_full_version" +# Date-keyed: a re-release of the same Python version (same 3.x.y, new build +# date — e.g. a rebuild that only re-signs binaries) downloads fresh instead of +# being served stale from the previous release's cache. dart-bridge stays +# version-keyed (its re-releases bump the version). +pb_cache="$cache_root/python-build/v$python_full_version-$python_build_date" db_cache="$cache_root/dart-bridge/v$dart_bridge_version" mkdir -p "$pb_cache" "$db_cache" @@ -27,12 +31,14 @@ if [ ! -f "$python_macos_dist_path" ]; then fi # Re-extract when $dist is missing OR was assembled for a different Python -# version. The guard used to be `[ ! -d "$dist" ]`, which left a stale dist_macos -# from a previous Python version in place — e.g. bundling 3.12 under 3.14 -# site-packages, which trips C-extension ABI errors ("unknown slot ID") at -# import. A version marker keys the extracted dist to $python_full_version. -marker="$dist/.python_full_version" -if [ ! -d "$dist" ] || [ "$(cat "$marker" 2>/dev/null)" != "$python_full_version" ]; then +# version/release. The guard used to be `[ ! -d "$dist" ]`, which left a stale +# dist_macos from a previous Python version in place — e.g. bundling 3.12 under +# 3.14 site-packages, which trips C-extension ABI errors ("unknown slot ID") at +# import. The marker keys the extracted dist to the version + release date, so a +# same-version re-release (new build date) also re-extracts. +build_id="$python_full_version-$python_build_date" +marker="$dist/.python_build_id" +if [ ! -d "$dist" ] || [ "$(cat "$marker" 2>/dev/null)" != "$build_id" ]; then rm -rf "$dist" mkdir -p "$dist" tar -xzf "$python_macos_dist_path" -C "$dist" @@ -48,7 +54,7 @@ if [ ! -d "$dist" ] || [ "$(cat "$marker" 2>/dev/null)" != "$python_full_version # unexpectedly" crash dialog. We don't need this launcher for embedded # use; libdart_bridge dlopens Python.framework's main binary directly. find "$dist/xcframeworks" -type d -name 'Python.app' -prune -exec rm -rf {} + - echo "$python_full_version" > "$marker" + echo "$build_id" > "$marker" fi # ---- flet-dev/dart-bridge (xcframework, same archive for macOS + iOS) ----- diff --git a/src/serious_python_darwin/darwin/prepare_spm.sh b/src/serious_python_darwin/darwin/prepare_spm.sh new file mode 100755 index 00000000..97aaee35 --- /dev/null +++ b/src/serious_python_darwin/darwin/prepare_spm.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -euo pipefail +# +# prepare_spm.sh +# +# Host-side equivalent of the podspec `prepare_command` for the Swift Package +# Manager build path (SPM has no pod-install hook). Downloads + extracts the +# Python/dart_bridge dist, syncs the app + site-packages into it, and maps the +# result into the SPM plugin layout. Prints SP_NATIVE_SET (the cache-bust key) +# on stdout; all progress goes to stderr so the caller can capture just the key. +# +# Version coordinates resolve from python_versions.properties (overridable via +# the same env vars the podspec honors), so the caller only passes the platform. +# +platform=${1:?usage: prepare_spm.sh } +script_dir=$(cd "$(dirname "$0")" && pwd -P) +props="$script_dir/python_versions.properties" + +prop() { grep "^$1=" "$props" 2>/dev/null | head -1 | cut -d= -f2-; } + +pyver=${SERIOUS_PYTHON_VERSION:-$(prop default_python_version)} +pyfull=${SERIOUS_PYTHON_FULL_VERSION:-$(prop "$pyver.full_version")} +builddate=${SERIOUS_PYTHON_BUILD_DATE:-$(prop python_build_release_date)} +dbver=${DART_BRIDGE_VERSION:-$(prop dart_bridge_version)} +[ -n "$pyfull" ] || { echo "prepare_spm: unknown SERIOUS_PYTHON_VERSION '$pyver'" >&2; exit 1; } + +echo "prepare_spm: $platform python=$pyfull build=$builddate dart_bridge=$dbver" >&2 +"$script_dir/prepare_$platform.sh" "$pyver" "$pyfull" "$builddate" "$dbver" >&2 +"$script_dir/sync_site_packages.sh" >&2 +SERIOUS_PYTHON_FULL_VERSION="$pyfull" "$script_dir/stage_spm.sh" "$platform" diff --git a/src/serious_python_darwin/darwin/python_versions.properties b/src/serious_python_darwin/darwin/python_versions.properties index 2fc624e2..41d4ec16 100644 --- a/src/serious_python_darwin/darwin/python_versions.properties +++ b/src/serious_python_darwin/darwin/python_versions.properties @@ -1,8 +1,8 @@ # GENERATED by `dart run serious_python:gen_version_tables` from -# python-build manifest.json (release 20260618). Do not edit by hand. +# python-build manifest.json (release 20260621). Do not edit by hand. default_python_version=3.14 dart_bridge_version=1.4.0 -python_build_release_date=20260618 +python_build_release_date=20260621 3.12.full_version=3.12.13 3.12.android_abis=arm64-v8a,x86_64,armeabi-v7a 3.13.full_version=3.13.14 diff --git a/src/serious_python_darwin/darwin/serious_python_darwin.podspec b/src/serious_python_darwin/darwin/serious_python_darwin.podspec index d66a2c95..a40434af 100644 --- a/src/serious_python_darwin/darwin/serious_python_darwin.podspec +++ b/src/serious_python_darwin/darwin/serious_python_darwin.podspec @@ -19,7 +19,7 @@ Pod::Spec.new do |s| # framework. Python.xcframework is also static, so this was always # implicitly the case — just being explicit now. s.static_framework = true - s.source_files = ['Classes/**/*'] + s.source_files = ['serious_python_darwin/Sources/serious_python_darwin/**/*.swift'] s.ios.dependency 'Flutter' s.osx.dependency 'FlutterMacOS' s.ios.deployment_target = '13.0' diff --git a/src/serious_python_darwin/darwin/serious_python_darwin/Package.swift b/src/serious_python_darwin/darwin/serious_python_darwin/Package.swift new file mode 100644 index 00000000..bd517d29 --- /dev/null +++ b/src/serious_python_darwin/darwin/serious_python_darwin/Package.swift @@ -0,0 +1,102 @@ +// swift-tools-version: 5.9 +import PackageDescription +import Foundation + +// === serious_python_darwin — Swift Package Manager manifest === +// +// Dual build system: this package builds the plugin under SwiftPM; the sibling +// `serious_python_darwin.podspec` builds the same Sources/ under CocoaPods. +// +// The Python runtime, dart_bridge, the app's native C-extensions, and the +// stdlib/site-packages/app trees are NOT committed. serious_python's `package` +// command (driven by `flet build`) materializes them into THIS package directory +// before `flutter build`, exactly as the CocoaPods `prepare_command` does: +// +// /Python-ios.xcframework, Python-macos.xcframework Python runtime (dynamic) +// /dart_bridge.xcframework FFI transport (static) +// /extra-xcframeworks/*.xcframework iOS native extensions +// (stdlib lib-dynload + site-packages) +// /Sources/serious_python_darwin/Resources/{stdlib,site-packages,app} +// +// On iOS, native extensions ship as embedded+signed frameworks (CPython's finder +// dlopen's them by their bundled path). On macOS they ride flat inside the +// site-packages / stdlib resource trees and load in place. +// +// CACHE-BUST CONTRACT: SwiftPM caches the resolved package graph keyed on this +// manifest's TEXT + the environment variables it reads — NOT on the staged dirs it +// enumerates. So the package step exports `SP_NATIVE_SET`, a hash over everything it +// staged (Python full version, dart_bridge version, the sorted extension set, the +// resource trees). Reading it here makes it a tracked key, so any change to the +// staged inputs forces re-resolution. `SERIOUS_PYTHON_VERSION` (the project's +// version-selection contract) is read for the same reason. +let env = ProcessInfo.processInfo.environment +_ = env["SP_NATIVE_SET"] +_ = env["SERIOUS_PYTHON_VERSION"] + +let pkgDir = URL(fileURLWithPath: #filePath).deletingLastPathComponent() +func staged(_ rel: String) -> Bool { + FileManager.default.fileExists(atPath: pkgDir.appendingPathComponent(rel).path) +} + +// Native binary targets + their plugin-target dependencies. All existence-guarded so +// the manifest still parses in an unstaged checkout (IDE / `dart pub get`); a real +// `flutter build` always stages first. +var binaryTargets: [Target] = [] +var deps: [Target.Dependency] = [.product(name: "FlutterFramework", package: "FlutterFramework")] + +// dart_bridge: static archive, link-only (forced in via -all_load). Multi-platform. +if staged("dart_bridge.xcframework") { + binaryTargets.append(.binaryTarget(name: "dart_bridge", path: "dart_bridge.xcframework")) + deps.append("dart_bridge") +} +// Python.framework: dynamic -> embedded + auto-signed. iOS and macOS ship separate +// xcframeworks, so each is platform-conditional. +if staged("Python-ios.xcframework") { + binaryTargets.append(.binaryTarget(name: "Python_ios", path: "Python-ios.xcframework")) + deps.append(.target(name: "Python_ios", condition: .when(platforms: [.iOS]))) +} +if staged("Python-macos.xcframework") { + binaryTargets.append(.binaryTarget(name: "Python_macos", path: "Python-macos.xcframework")) + deps.append(.target(name: "Python_macos", condition: .when(platforms: [.macOS]))) +} +// iOS native C-extensions: each staged *.xcframework -> embedded+signed framework. +let extraDir = pkgDir.appendingPathComponent("extra-xcframeworks") +if let items = try? FileManager.default.contentsOfDirectory( + at: extraDir, includingPropertiesForKeys: nil) { + for item in items.sorted(by: { $0.lastPathComponent < $1.lastPathComponent }) + where item.pathExtension == "xcframework" { + let name = item.deletingPathExtension().lastPathComponent + binaryTargets.append(.binaryTarget(name: name, path: "extra-xcframeworks/\(name).xcframework")) + deps.append(.target(name: name, condition: .when(platforms: [.iOS]))) + } +} + +let package = Package( + name: "serious_python_darwin", + platforms: [.iOS("13.0"), .macOS("11.0")], + products: [ + .library(name: "serious-python-darwin", targets: ["serious_python_darwin"]), + ], + dependencies: [ + .package(name: "FlutterFramework", path: "../FlutterFramework"), + ], + targets: [ + .target( + name: "serious_python_darwin", + dependencies: deps, + resources: [ + // Staged trees. .copy (verbatim) preserves the layout PYTHONHOME / + // PYTHONPATH expect; committed `.keep` placeholders keep these paths + // valid (and Bundle.module generated) in an unstaged checkout. + .copy("Resources/stdlib"), + .copy("Resources/site-packages"), + .copy("Resources/app"), + ], + linkerSettings: [ + // Reproduces the podspec OTHER_LDFLAGS = '-ObjC -all_load -lc++'. + .unsafeFlags(["-ObjC", "-all_load"]), + .linkedLibrary("c++"), + ] + ), + ] + binaryTargets +) diff --git a/src/serious_python_darwin/darwin/serious_python_darwin/Sources/serious_python_darwin/Resources/app/.keep b/src/serious_python_darwin/darwin/serious_python_darwin/Sources/serious_python_darwin/Resources/app/.keep new file mode 100644 index 00000000..be174385 --- /dev/null +++ b/src/serious_python_darwin/darwin/serious_python_darwin/Sources/serious_python_darwin/Resources/app/.keep @@ -0,0 +1,2 @@ +# Placeholder so the SwiftPM resource path exists in an unstaged checkout; +# serious_python_darwin's package step fills this directory before `flutter build`. diff --git a/src/serious_python_darwin/darwin/serious_python_darwin/Sources/serious_python_darwin/Resources/site-packages/.keep b/src/serious_python_darwin/darwin/serious_python_darwin/Sources/serious_python_darwin/Resources/site-packages/.keep new file mode 100644 index 00000000..be174385 --- /dev/null +++ b/src/serious_python_darwin/darwin/serious_python_darwin/Sources/serious_python_darwin/Resources/site-packages/.keep @@ -0,0 +1,2 @@ +# Placeholder so the SwiftPM resource path exists in an unstaged checkout; +# serious_python_darwin's package step fills this directory before `flutter build`. diff --git a/src/serious_python_darwin/darwin/serious_python_darwin/Sources/serious_python_darwin/Resources/stdlib/.keep b/src/serious_python_darwin/darwin/serious_python_darwin/Sources/serious_python_darwin/Resources/stdlib/.keep new file mode 100644 index 00000000..be174385 --- /dev/null +++ b/src/serious_python_darwin/darwin/serious_python_darwin/Sources/serious_python_darwin/Resources/stdlib/.keep @@ -0,0 +1,2 @@ +# Placeholder so the SwiftPM resource path exists in an unstaged checkout; +# serious_python_darwin's package step fills this directory before `flutter build`. diff --git a/src/serious_python_darwin/darwin/Classes/SeriousPythonPlugin.swift b/src/serious_python_darwin/darwin/serious_python_darwin/Sources/serious_python_darwin/SeriousPythonPlugin.swift similarity index 54% rename from src/serious_python_darwin/darwin/Classes/SeriousPythonPlugin.swift rename to src/serious_python_darwin/darwin/serious_python_darwin/Sources/serious_python_darwin/SeriousPythonPlugin.swift index 07224c3d..d676aa0b 100644 --- a/src/serious_python_darwin/darwin/Classes/SeriousPythonPlugin.swift +++ b/src/serious_python_darwin/darwin/serious_python_darwin/Sources/serious_python_darwin/SeriousPythonPlugin.swift @@ -45,25 +45,38 @@ public class SeriousPythonPlugin: NSObject, FlutterPlugin { public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { switch call.method { case "getResourcePath": - // The python.bundle that prepare_{ios,macos}.sh assembles ends up - // inside this plugin's framework bundle as a Resources subbundle. - // Dart calls this to discover the stdlib / site-packages layout - // before invoking `serious_python_run`. - guard let frameworkBundle = Bundle(for: type(of: self)).resourceURL else { - result(FlutterError(code: "FRAMEWORK_BUNDLE_ERROR", - message: "Failed to get framework resource URL", - details: nil)) - return - } - let pythonBundleURL = frameworkBundle.appendingPathComponent("python.bundle") - guard let pythonBundle = Bundle(url: pythonBundleURL), - let resourcePath = pythonBundle.resourcePath else { - result(FlutterError(code: "PYTHON_BUNDLE_ERROR", - message: "Failed to load python.bundle", - details: pythonBundleURL.path)) - return - } - result(resourcePath) + // Dart calls this to discover the stdlib / site-packages / app layout + // before invoking `serious_python_run`. The two build systems put the + // trees in different bundles: + #if SWIFT_PACKAGE + // SwiftPM: staged as `.copy` resources into Bundle.module, whose + // resourcePath contains stdlib/ site-packages/ app/ directly. + guard let resourcePath = Bundle.module.resourcePath else { + result(FlutterError(code: "PYTHON_BUNDLE_ERROR", + message: "Failed to resolve Bundle.module resourcePath", + details: nil)) + return + } + result(resourcePath) + #else + // CocoaPods: the python.bundle that prepare_{ios,macos}.sh assembles + // lives inside the plugin framework as a Resources subbundle. + guard let frameworkBundle = Bundle(for: type(of: self)).resourceURL else { + result(FlutterError(code: "FRAMEWORK_BUNDLE_ERROR", + message: "Failed to get framework resource URL", + details: nil)) + return + } + let pythonBundleURL = frameworkBundle.appendingPathComponent("python.bundle") + guard let pythonBundle = Bundle(url: pythonBundleURL), + let resourcePath = pythonBundle.resourcePath else { + result(FlutterError(code: "PYTHON_BUNDLE_ERROR", + message: "Failed to load python.bundle", + details: pythonBundleURL.path)) + return + } + result(resourcePath) + #endif default: result(FlutterMethodNotImplemented) diff --git a/src/serious_python_darwin/darwin/stage_spm.sh b/src/serious_python_darwin/darwin/stage_spm.sh new file mode 100755 index 00000000..0ebba398 --- /dev/null +++ b/src/serious_python_darwin/darwin/stage_spm.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +set -euo pipefail +# +# stage_spm.sh +# +# Host-side staging for the Swift Package Manager build path. Maps the assembled +# dist_ tree (produced by prepare_.sh + sync_site_packages.sh) +# into the SPM plugin layout under serious_python_darwin/, which Package.swift +# consumes as local-path binaryTargets + .copy resources. Prints the cache-bust +# key (SP_NATIVE_SET) on stdout — the caller exports it into the `flutter build` +# environment so SwiftPM re-resolves when the staged native set changes. +# +platform=${1:?usage: stage_spm.sh } +script_dir=$(cd "$(dirname "$0")" && pwd -P) +dist="$script_dir/dist_$platform" +pkg="$script_dir/serious_python_darwin" +res="$pkg/Sources/serious_python_darwin/Resources" + +[ -d "$dist" ] || { echo "stage_spm: $dist not found" >&2; exit 1; } + +# 1. Python runtime (dynamic framework -> embedded). Platform-specific path so a +# single shared manifest can carry both via platform-conditional binaryTargets. +rm -rf "$pkg/Python-$platform.xcframework" +cp -R "$dist/xcframeworks/Python.xcframework" "$pkg/Python-$platform.xcframework" + +# 2. dart_bridge (static, version-independent; same artifact in either dist). +rm -rf "$pkg/dart_bridge.xcframework" +cp -R "$dist/xcframeworks/dart_bridge.xcframework" "$pkg/dart_bridge.xcframework" + +# 3. iOS native C-extensions (stdlib lib-dynload + site-packages) -> enumerated +# binaryTargets. macOS has none: its .so's load flat from the resource trees. +rm -rf "$pkg/extra-xcframeworks" +if [ "$platform" = "ios" ] && [ -d "$dist/site-xcframeworks" ]; then + mkdir -p "$pkg/extra-xcframeworks" + cp -R "$dist"/site-xcframeworks/*.xcframework "$pkg/extra-xcframeworks/" 2>/dev/null || true +fi + +# 4. Resource trees (verbatim via rsync). Wipe prior content but keep the +# committed .keep placeholder so the path stays valid in a clean checkout. +for tree in stdlib site-packages app; do + dest="$res/$tree" + mkdir -p "$dest" + find "$dest" -mindepth 1 -not -name '.keep' -delete 2>/dev/null || true + [ -d "$dist/$tree" ] && rsync -a --exclude '.pod' "$dist/$tree/" "$dest/" +done + +# 5. Cache-bust key: platform + Python version + the staged native/resource set +# (path+size). Changes whenever requirements, app, or Python version change. +key_paths=("Python-$platform.xcframework" "Sources/serious_python_darwin/Resources") +[ -d "$pkg/extra-xcframeworks" ] && key_paths+=("extra-xcframeworks") +key=$( + { + echo "$platform ${SERIOUS_PYTHON_FULL_VERSION:-}" + ( cd "$pkg" && find "${key_paths[@]}" -type f -exec stat -f '%N %z' {} + \ + 2>/dev/null | sort ) + } | shasum -a 256 | cut -d' ' -f1 +) +echo "$key" diff --git a/src/serious_python_darwin/pubspec.yaml b/src/serious_python_darwin/pubspec.yaml index 68bb6234..8c2e09ee 100644 --- a/src/serious_python_darwin/pubspec.yaml +++ b/src/serious_python_darwin/pubspec.yaml @@ -5,6 +5,10 @@ repository: https://github.com/flet-dev/serious-python version: 4.0.0 environment: + # The Swift Package Manager build path needs Flutter 3.44 / Dart 3.11 (the + # FlutterFramework SwiftPM convention), but `Package.swift` is simply ignored on + # older Flutter — which uses the CocoaPods path instead — so SPM support does + # not raise the plugin's minimum. Keep the existing floor. sdk: ">=3.0.0 <4.0.0" flutter: ">=3.7.0" diff --git a/src/serious_python_linux/linux/CMakeLists.txt b/src/serious_python_linux/linux/CMakeLists.txt index aca471aa..96951110 100644 --- a/src/serious_python_linux/linux/CMakeLists.txt +++ b/src/serious_python_linux/linux/CMakeLists.txt @@ -57,7 +57,10 @@ if(DEFINED ENV{FLET_CACHE_DIR}) else() set(FLET_CACHE_DIR "$ENV{HOME}/.flet/cache") endif() -set(PB_CACHE "${FLET_CACHE_DIR}/python-build/v${PYTHON_FULL_VERSION}") +# Date-keyed so a same-version re-release (new build date) re-downloads instead +# of being served stale from a previous release's cache. dart-bridge stays +# version-keyed (its re-releases bump the version). +set(PB_CACHE "${FLET_CACHE_DIR}/python-build/v${PYTHON_FULL_VERSION}-${PYTHON_BUILD_DATE}") set(DB_CACHE "${FLET_CACHE_DIR}/dart-bridge/v${DART_BRIDGE_VERSION}") file(MAKE_DIRECTORY "${PB_CACHE}" "${DB_CACHE}") @@ -90,8 +93,23 @@ set(PYTHON_PACKAGE ${CMAKE_BINARY_DIR}/python) set(PYTHON_URL https://github.com/flet-dev/python-build/releases/download/${PYTHON_BUILD_DATE}/python-linux-dart-${PYTHON_FULL_VERSION}-${PYTHON_ARCH}.tar.gz) set(PYTHON_FILE "${PB_CACHE}/python-linux-dart-${PYTHON_FULL_VERSION}-${PYTHON_ARCH}.tar.gz") _sp_download("${PYTHON_URL}" "${PYTHON_FILE}") -if(NOT EXISTS "${PYTHON_PACKAGE}/lib/libpython3.so") +# Re-extract when the tree is missing OR was unpacked for a different +# version/release (a marker keyed to version + build date), so a same-version +# re-release replaces it instead of being skipped. +set(_py_build_id "${PYTHON_FULL_VERSION}-${PYTHON_BUILD_DATE}") +set(_py_marker "${PYTHON_PACKAGE}/.python_build_id") +set(_py_extract TRUE) +if(EXISTS "${_py_marker}") + file(READ "${_py_marker}" _py_cur) + string(STRIP "${_py_cur}" _py_cur) + if(_py_cur STREQUAL "${_py_build_id}") + set(_py_extract FALSE) + endif() +endif() +if(_py_extract) + file(REMOVE_RECURSE "${PYTHON_PACKAGE}") file(ARCHIVE_EXTRACT INPUT "${PYTHON_FILE}" DESTINATION "${PYTHON_PACKAGE}") + file(WRITE "${_py_marker}" "${_py_build_id}") endif() # ---- dart_bridge prebuilt .so --------------------------------------------- diff --git a/src/serious_python_linux/linux/python_versions.properties b/src/serious_python_linux/linux/python_versions.properties index 2fc624e2..41d4ec16 100644 --- a/src/serious_python_linux/linux/python_versions.properties +++ b/src/serious_python_linux/linux/python_versions.properties @@ -1,8 +1,8 @@ # GENERATED by `dart run serious_python:gen_version_tables` from -# python-build manifest.json (release 20260618). Do not edit by hand. +# python-build manifest.json (release 20260621). Do not edit by hand. default_python_version=3.14 dart_bridge_version=1.4.0 -python_build_release_date=20260618 +python_build_release_date=20260621 3.12.full_version=3.12.13 3.12.android_abis=arm64-v8a,x86_64,armeabi-v7a 3.13.full_version=3.13.14 diff --git a/src/serious_python_windows/windows/CMakeLists.txt b/src/serious_python_windows/windows/CMakeLists.txt index daad43ec..573388c6 100644 --- a/src/serious_python_windows/windows/CMakeLists.txt +++ b/src/serious_python_windows/windows/CMakeLists.txt @@ -60,7 +60,10 @@ elseif(DEFINED ENV{USERPROFILE}) else() set(FLET_CACHE_DIR "$ENV{HOME}/.flet/cache") endif() -set(PB_CACHE "${FLET_CACHE_DIR}/python-build/v${PYTHON_FULL_VERSION}") +# Date-keyed so a same-version re-release (new build date) re-downloads instead +# of being served stale from a previous release's cache. dart-bridge stays +# version-keyed (its re-releases bump the version). +set(PB_CACHE "${FLET_CACHE_DIR}/python-build/v${PYTHON_FULL_VERSION}-${PYTHON_BUILD_DATE}") set(DB_CACHE "${FLET_CACHE_DIR}/dart-bridge/v${DART_BRIDGE_VERSION}") file(MAKE_DIRECTORY "${PB_CACHE}" "${DB_CACHE}") @@ -102,8 +105,23 @@ function(_sp_download url dest) endfunction() _sp_download("${PYTHON_URL}" "${PYTHON_FILE}") -if(NOT EXISTS "${PYTHON_PACKAGE}/python.exe") +# Re-extract when the tree is missing OR was unpacked for a different +# version/release (a marker keyed to version + build date), so a same-version +# re-release replaces it instead of being skipped. +set(_py_build_id "${PYTHON_FULL_VERSION}-${PYTHON_BUILD_DATE}") +set(_py_marker "${PYTHON_PACKAGE}/.python_build_id") +set(_py_extract TRUE) +if(EXISTS "${_py_marker}") + file(READ "${_py_marker}" _py_cur) + string(STRIP "${_py_cur}" _py_cur) + if(_py_cur STREQUAL "${_py_build_id}") + set(_py_extract FALSE) + endif() +endif() +if(_py_extract) + file(REMOVE_RECURSE "${PYTHON_PACKAGE}") file(ARCHIVE_EXTRACT INPUT "${PYTHON_FILE}" DESTINATION "${PYTHON_PACKAGE}") + file(WRITE "${_py_marker}" "${_py_build_id}") endif() # ---- dart_bridge prebuilt DLLs -------------------------------------------- diff --git a/src/serious_python_windows/windows/python_versions.properties b/src/serious_python_windows/windows/python_versions.properties index 2fc624e2..41d4ec16 100644 --- a/src/serious_python_windows/windows/python_versions.properties +++ b/src/serious_python_windows/windows/python_versions.properties @@ -1,8 +1,8 @@ # GENERATED by `dart run serious_python:gen_version_tables` from -# python-build manifest.json (release 20260618). Do not edit by hand. +# python-build manifest.json (release 20260621). Do not edit by hand. default_python_version=3.14 dart_bridge_version=1.4.0 -python_build_release_date=20260618 +python_build_release_date=20260621 3.12.full_version=3.12.13 3.12.android_abis=arm64-v8a,x86_64,armeabi-v7a 3.13.full_version=3.13.14