Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/') }}
Expand Down
5 changes: 5 additions & 0 deletions src/serious_python/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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))
Expand Down
14 changes: 14 additions & 0 deletions src/serious_python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<X.Y>.so` on Linux; `python3.dll` + `python<XY>.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 `<exe-dir>/DLLs/`, which is added to `sys.path`.
Expand Down
5 changes: 5 additions & 0 deletions src/serious_python_darwin/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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))
Expand Down
60 changes: 60 additions & 0 deletions src/serious_python_darwin/darwin/linker_signatures.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Signature normalization for the Mach-O libraries bundled into a macOS app.
#
# replace_linker_signatures <dir>...
#
# 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 <<EOF
$files
EOF
done
}
6 changes: 6 additions & 0 deletions src/serious_python_darwin/darwin/prepare_macos.sh
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ if [ ! -d "$dist" ] || [ "$(cat "$marker" 2>/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
Comment thread
ndonkoHenri marked this conversation as resolved.

# ---- 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,15 @@ 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}
./sync_site_packages.sh
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 = {
Expand Down
6 changes: 6 additions & 0 deletions src/serious_python_darwin/darwin/sync_site_packages.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading