diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 64512d60..68b70064 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -138,6 +138,34 @@ jobs: flutter test integration_test/throughput_test.dart -d macos --dart-define=EXPECTED_PYTHON_VERSION=${{ matrix.python_version }} flutter test integration_test/memory_test.dart -d macos --dart-define=EXPECTED_PYTHON_VERSION=${{ matrix.python_version }} + - name: Check bundled native libraries are not linker-signed + working-directory: "src/serious_python/example/bridge_example" + run: | + # Xcode distribution signing mis-signs linker-signed libraries (see + # src/serious_python_darwin/darwin/linker_signatures.sh), so none may + # reach the app bundle. + app=$(find build/macos/Build/Products -maxdepth 2 -type d -name '*.app' | head -n 1) + if [ -z "$app" ]; then + echo "::error::No built app under build/macos/Build/Products" + exit 1 + fi + total=0 + linker_signed=0 + while IFS= read -r bin; do + total=$((total + 1)) + for arch in $(lipo -archs "$bin" 2>/dev/null); do + case $(codesign -d --verbose=1 --architecture "$arch" "$bin" 2>&1) in + *"CodeDirectory "*linker-signed*) + echo "::error::Linker-signed ($arch slice): ${bin#"$app"/}" + linker_signed=$((linker_signed + 1)) + break + ;; + esac + done + done < <(find "$app/Contents/Resources" -type f \( -name '*.so' -o -name '*.so.*' -o -name '*.dylib' \)) + echo "Checked $total bundled native libraries, $linker_signed linker-signed." + [ "$total" -gt 0 ] && [ "$linker_signed" -eq 0 ] + bridge_example_ios: name: Test Bridge example on iOS (${{ matrix.build_system }}, Python ${{ matrix.python_version }}) if: ${{ !startsWith(github.ref, 'refs/tags/') }} diff --git a/src/serious_python/CHANGELOG.md b/src/serious_python/CHANGELOG.md index 80b9d3ab..d09e85fd 100644 --- a/src/serious_python/CHANGELOG.md +++ b/src/serious_python/CHANGELOG.md @@ -1,3 +1,8 @@ +## 4.7.2 + +* **macOS:** fix App Store Connect error `90238` ("does not satisfy its designated Requirement") for bundled native modules after Xcode distribution signing ([#250](https://github.com/flet-dev/serious-python/issues/250)). See `serious_python_darwin` 4.7.2. +* **iOS/macOS (CocoaPods):** fail `pod install` when a Python runtime preparation script fails, preventing builds from continuing with an incomplete runtime. See `serious_python_darwin` 4.7.2. + ## 4.7.1 * Fix macOS crashes during native scientific imports and NumPy operations by giving the asynchronous Python worker at least **8 MiB** of stack space, via `dart_bridge` 1.10.0. ([dart-bridge#21](https://github.com/flet-dev/dart-bridge/pull/21), [#85](https://github.com/flet-dev/serious-python/issues/85)) diff --git a/src/serious_python/README.md b/src/serious_python/README.md index 9609d010..95e661cc 100644 --- a/src/serious_python/README.md +++ b/src/serious_python/README.md @@ -319,6 +319,20 @@ produces a working app, but it cannot produce complete SDK-origin receipts. **Us the SwiftPM path for App Store submissions** until that path is replaced with real vendored XCFramework declarations. +#### macOS native module signatures + +On macOS, the native modules under `stdlib/`, `site-packages/` and `app/` ship as +plain `.so`/`.dylib` files, which Xcode's distribution signing (the Organizer or +`xcodebuild -exportArchive`) re-signs with your certificate along with the rest of +the app. A module carrying the signature the linker gave it comes out of that +step with a designated requirement naming a different identifier than its new +signature: codesign does not carry a linker signature's identifier over, but Xcode +can build the requirement from it. App Store Connect rejects such an upload with +error 90238 ("does not satisfy its designated Requirement"). The macOS build +therefore replaces linker signatures with regular ad-hoc ones while staging these +trees. The `SERIOUS_PYTHON_SITE_PACKAGES` and `SERIOUS_PYTHON_APP` directories +themselves are left unchanged. + ### Linux / Windows specifics The CPython runtime (`libpython3.so` + `libpython.so` on Linux; `python3.dll` + `python.dll` on Windows), `libdart_bridge`, the stdlib, and native modules are copied next to your app's executable at build time. `PYTHONHOME` is the executable's directory. On Windows, extension modules (`.pyd`) and their dependent DLLs live in `/DLLs/`, which is added to `sys.path`. diff --git a/src/serious_python_darwin/CHANGELOG.md b/src/serious_python_darwin/CHANGELOG.md index 81d6dd41..76a0b2b9 100644 --- a/src/serious_python_darwin/CHANGELOG.md +++ b/src/serious_python_darwin/CHANGELOG.md @@ -1,3 +1,8 @@ +## 4.7.2 + +* **macOS:** fix App Store Connect error `90238` ("does not satisfy its designated Requirement") caused by linker-signed native modules ([#250](https://github.com/flet-dev/serious-python/issues/250)). Staging now replaces linker signatures on `.so` and `.dylib` files in the stdlib, site-packages and app with regular ad-hoc signatures. This preserves their signing identifiers when Xcode re-signs them for distribution, avoiding a mismatch with their designated requirements. +* **iOS/macOS (CocoaPods):** `pod install` now stops when a Python runtime preparation script exits with an error. Previously, the podspec ignored these failures, allowing builds to continue despite native module signing errors, provider integrity mismatches, or provider signature verification failures in `require` mode. + ## 4.7.1 * Fix macOS crashes during native scientific imports and NumPy operations by giving the asynchronous Python worker at least **8 MiB** of stack space, via `dart_bridge` 1.10.0. ([dart-bridge#21](https://github.com/flet-dev/dart-bridge/pull/21), [#85](https://github.com/flet-dev/serious-python/issues/85)) diff --git a/src/serious_python_darwin/darwin/linker_signatures.sh b/src/serious_python_darwin/darwin/linker_signatures.sh new file mode 100644 index 00000000..08647204 --- /dev/null +++ b/src/serious_python_darwin/darwin/linker_signatures.sh @@ -0,0 +1,60 @@ +# Signature normalization for the Mach-O libraries bundled into a macOS app. +# +# replace_linker_signatures ... +# +# Re-signs ad-hoc every `.so`, `.so.*` and `.dylib` under the given directories +# that has an architecture slice carrying the signature the linker attached +# when it built the file. +# +# A linker signature names the code after the output file, extension included +# (`_ssl.cpython-314-darwin.so`), and codesign never carries metadata over from +# one: re-signing the file derives a fresh identifier from the file name minus +# its extension (`_ssl.cpython-314-darwin`). Xcode's distribution signing +# (Organizer, `xcodebuild -exportArchive`) can write the designated requirement +# from the identifier it read off the existing signature, so a linker-signed +# library ends up with a requirement its own new signature does not satisfy, +# and App Store Connect rejects the upload (error 90238, "does not satisfy its +# designated Requirement"). A regular ad-hoc signature's identifier is carried +# over by re-signing, so the requirement and the signature agree. +# +# Every slice of a universal file is inspected, one file at a time: `codesign +# -d` reports a single slice unless `--architecture` selects one, which slice +# that is depends on the build machine, and given several paths it stops at the +# first one that is not signed. The linker signs arm64 slices and usually +# leaves x86_64 slices unsigned, so the linker signature is often on a slice +# other than the reported one. `codesign --verify` accepts linker signatures, +# so it cannot tell them apart. A file with any linker-signed slice is +# re-signed, all slices at once. Anything else -- already re-signed, unsigned, +# or not Mach-O at all -- is left untouched, so a repeated run only re-checks. +# Ad-hoc signing is deterministic, so re-signing a fresh copy of the same file +# reproduces the same bytes. Paths containing newlines are not supported. +# +# Returns non-zero, after printing the error, when a directory cannot be listed +# or a file cannot be re-signed. +replace_linker_signatures() { + local dir files bin archs arch err + for dir in "$@"; do + [ -d "$dir" ] || continue + if ! files=$(find "$dir" -type f \( -name '*.so' -o -name '*.so.*' -o -name '*.dylib' \)); then + echo "replace_linker_signatures: cannot list $dir" >&2 + return 1 + fi + while IFS= read -r bin; do + [ -n "$bin" ] || continue + archs=$(lipo -archs "$bin" 2>/dev/null) || continue + for arch in $archs; do + case $(codesign -d --verbose=1 --architecture "$arch" "$bin" 2>&1) in + *"CodeDirectory "*linker-signed*) ;; + *) continue ;; + esac + if ! err=$(codesign --force --sign - "$bin" 2>&1); then + echo "replace_linker_signatures: codesign failed for $bin: $err" >&2 + return 1 + fi + break + done + done </dev/null)" != "$pb_id" ]; then echo "$pb_id" > "$marker" fi +# ---- stdlib native module signatures ---------------------------------------- +# Outside the extraction guard, so an already-extracted dist is covered as well. +# The provider xcframeworks are not touched. +. "$script_dir/linker_signatures.sh" +replace_linker_signatures "$dist/stdlib" || exit 1 + # ---- flet-dev/dart-bridge (xcframework, same archive for macOS + iOS) ----- # Separate cache guard so a stale $dist from before this change still picks # up the new artifact on first re-prepare. diff --git a/src/serious_python_darwin/darwin/serious_python_darwin.podspec b/src/serious_python_darwin/darwin/serious_python_darwin.podspec index 447fd213..207c32c2 100644 --- a/src/serious_python_darwin/darwin/serious_python_darwin.podspec +++ b/src/serious_python_darwin/darwin/serious_python_darwin.podspec @@ -55,6 +55,7 @@ Pod::Spec.new do |s| dist_macos = "dist_macos" prepare_command = <<-CMD + set -e ./symlink_pod.sh ./prepare_ios.sh #{python_version} #{python_full_version} #{python_build_date} #{dart_bridge_version} ./prepare_macos.sh #{python_version} #{python_full_version} #{python_build_date} #{dart_bridge_version} @@ -62,6 +63,7 @@ Pod::Spec.new do |s| CMD puts `#{prepare_command}` +raise "serious_python_darwin: preparing the Python runtime failed (exit status #{$?.exitstatus}); see the errors above" unless $?.success? # iOS frameworks s.ios.script_phase = { diff --git a/src/serious_python_darwin/darwin/sync_site_packages.sh b/src/serious_python_darwin/darwin/sync_site_packages.sh index ed065943..e98e6d6d 100755 --- a/src/serious_python_darwin/darwin/sync_site_packages.sh +++ b/src/serious_python_darwin/darwin/sync_site_packages.sh @@ -3,6 +3,8 @@ script_dir=$(cd "$(dirname "$0")" && pwd -P) # Provider-signature + provider-integrity checks (see xcframework_verify.sh). # Sourced unconditionally so both the iOS and macOS branches can use it. source $script_dir/xcframework_verify.sh +# Linker-signature replacement for the macOS resource trees. +source "$script_dir/linker_signatures.sh" # App sources are arch- and platform-independent; stage them as a bare `app/` # resource bundle into BOTH dist trees, regardless of whether site-packages @@ -16,6 +18,7 @@ if [[ -n "$SERIOUS_PYTHON_APP" && -d "$SERIOUS_PYTHON_APP" ]]; then mkdir -p "$app_dist/app" rsync -a --exclude '.pod' "$SERIOUS_PYTHON_APP/" "$app_dist/app/" done + replace_linker_signatures "$script_dir/dist_macos/app" || exit 1 fi if [[ -n "$SERIOUS_PYTHON_SITE_PACKAGES" && -d "$SERIOUS_PYTHON_SITE_PACKAGES" ]]; then @@ -136,6 +139,9 @@ if [[ -n "$SERIOUS_PYTHON_SITE_PACKAGES" && -d "$SERIOUS_PYTHON_SITE_PACKAGES" ] # file. .pod is only needed by package_command.dart at packaging # time to invoke this sync script; it does not belong in the bundle. rsync -av --delete --exclude '.pod' "$SERIOUS_PYTHON_SITE_PACKAGES/" "$dist/site-packages/" + # The staged copy is re-signed, never the source; each sync restores the + # linker-signed files and re-signing them reproduces the same bytes. + replace_linker_signatures "$dist/site-packages" || exit 1 # macOS has no framework-ization step -- its .so's load flat from the # resource tree -- so nothing here should ever touch the provider