fix(darwin): re-sign linker-signed macOS native modules while staging - #251
Merged
Merged
Conversation
Python extensions from python-build (the stdlib lib-dynload modules) and from many wheels keep the ad-hoc signature the linker attached, whose identifier is the file name including `.so`. Xcode's distribution signing re-signs them with --preserve-metadata=identifier, which ignores linker signatures, so codesign derives a new identifier without the extension, while Xcode can write the designated requirement from the old one. The result, reported with Xcode 26.5, is a signature that fails its own requirement, and App Store Connect rejects the build with 90238 "does not satisfy its designated Requirement" on every such file. prepare_macos.sh (stdlib) and sync_site_packages.sh (site-packages, app) now give every still-linker-signed .so/.dylib a regular ad-hoc signature, whose identifier re-signing preserves. Only the staged dist_macos copies are touched; the source directories and the provider xcframeworks are left as they are. CocoaPods and SwiftPM both build from dist_macos. Refs #250
After the integration tests, check every .so/.dylib under the built app's Contents/Resources and fail if any still carries a linker signature, on both the CocoaPods and SwiftPM legs.
ndonkoHenri
requested review from
FeodorFitsner
and
a balanced review from Copilot
September 23, 2026 19:02
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
CocoaPods can ignore normalization failures and continue with linker-signed stdlib modules.
Get a fresh assessment by requesting another Copilot review.
Review effort: Balanced
Findings: 1
Open (1)
What changed in this PR
Normalizes linker-signed macOS native modules during staging to prevent App Store Connect error 90238.
Changes:
- Re-signs affected stdlib, app, and site-package libraries.
- Adds CI validation for bundled native libraries.
- Documents the fix in README and changelogs.
| File | Description |
|---|---|
.github/workflows/ci.yml |
Checks macOS app bundles for linker signatures. |
src/serious_python/README.md |
Documents macOS signature handling. |
src/serious_python/CHANGELOG.md |
Adds umbrella 4.7.2 release notes. |
src/serious_python_darwin/CHANGELOG.md |
Adds detailed Darwin release notes. |
src/serious_python_darwin/darwin/linker_signatures.sh |
Implements signature normalization. |
src/serious_python_darwin/darwin/prepare_macos.sh |
Normalizes staged stdlib modules. |
src/serious_python_darwin/darwin/sync_site_packages.sh |
Normalizes staged app and dependency modules. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The podspec ran symlink_pod.sh, prepare_ios.sh, prepare_macos.sh and sync_site_packages.sh as newline-separated commands in Ruby backticks and never checked their exit status. A script's `exit 1` was printed and then ignored, and the build went on with a partially prepared runtime. This covered a native module that could not be re-signed, a provider manifest mismatch, and a provider signature check in `require` mode, which the README documents as failing the build. The prepare commands start with `set -e` so the first failing script stops the sequence, and the podspec raises on a non-zero status, so `pod install` and the Flutter build running it fail with the error. prepare_spm.sh, used by the Swift Package Manager path, already stops at the first failure.
replace_linker_signatures asked `codesign -d` about a single slice, the one codesign reports by default for the build machine. The linker signs arm64 slices and leaves x86_64 slices unsigned unless linked with -adhoc_codesign, so python-build's universal stdlib modules carry a linker-signed arm64 slice next to an unsigned x86_64 one. On an Intel Mac the helper saw only the unsigned slice and skipped every such file, and a linker-signed x86_64 slice next to an ad-hoc arm64 one was missed on any machine. Each slice listed by `lipo -archs` is checked with `codesign -d --architecture`, and a file with any linker-signed slice is re-signed as a whole. `codesign --verify` cannot serve as a shortcut because it accepts linker signatures. The CI check inspects every slice the same way and names the offending one.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Fixes #250.
On macOS, App Store Connect rejects apps distributed from Xcode (Organizer or
xcodebuild -exportArchive) with90238: Invalid signature … does not satisfy its designated Requirementon the bundled Python native modules.Cause
lib-dynloadmodules from python-build, and many wheel extensions (e.g. Pillow's_imagingmorph), keep the ad-hoc signature the linker attached. Its identifier is the file name including.so.--preserve-metadata=identifier, which ignores linker signatures, socodesignderives a new identifier without the extension. Xcode can also write the designated requirement itself from the identifier it read off the old signature, which is what the reporter's Xcode 26.5 did (designated => anchor apple generic and identifier "_codecs_kr.cpython-314-darwin.so" …on a signature whose identifier is_codecs_kr.cpython-314-darwin). The signature then fails its own requirement.Fix
darwin/linker_signatures.shwithreplace_linker_signatures <dir>.... It re-signs ad-hoc every.so,.so.*and.dylibthat still carries a linker signature and leaves everything else alone (already re-signed, unsigned, not Mach-O). A regular ad-hoc signature's identifier survives re-signing, so the requirement and the signature agree.dist_macoscopies, which both CocoaPods and SwiftPM build from:prepare_macos.shfor the stdlib (outside the extraction guard, so already-extracted dists are covered) andsync_site_packages.shfor site-packages and the app.SERIOUS_PYTHON_SITE_PACKAGES,SERIOUS_PYTHON_APPand the provider XCFrameworks are not modified.bridge_example_macosfails if any.so/.dylibin the built app is still linker-signed, on both the CocoaPods and SwiftPM legs.Verification
Repro of the reporter's setup (Python 3.14, Pillow, their env vars):
.sobridge_example_macosCI steps on both build systems: all integration tests pass, and the new check passes (it fails on an unpatched build).Notes
codesign --verifyexits 0 even when a file fails its designated requirement, so the CI step checks for leftover linker signatures instead of relying on that exit code.