Skip to content

fix(darwin): re-sign linker-signed macOS native modules while staging - #251

Merged
FeodorFitsner merged 5 commits into
mainfrom
fix/macos-resign-linker-signed
Sep 24, 2026
Merged

FeodorFitsner merged 5 commits into
mainfrom
fix/macos-resign-linker-signed

Conversation

@ndonkoHenri

Copy link
Copy Markdown
Collaborator

Fixes #250.

On macOS, App Store Connect rejects apps distributed from Xcode (Organizer or xcodebuild -exportArchive) with 90238: Invalid signature … does not satisfy its designated Requirement on the bundled Python native modules.

Cause

  • The stdlib lib-dynload modules 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.
  • Xcode's distribution signing re-signs them with --preserve-metadata=identifier, which ignores linker signatures, so codesign derives 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.
  • Files that were already re-signed ad-hoc before reaching Xcode (e.g. by delocate in Pillow's wheel build) are unaffected, which is why only some files fail.

Fix

  • New darwin/linker_signatures.sh with replace_linker_signatures <dir>.... It re-signs ad-hoc every .so, .so.* and .dylib that 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.
  • It runs on the staged dist_macos copies, which both CocoaPods and SwiftPM build from: prepare_macos.sh for the stdlib (outside the extraction guard, so already-extracted dists are covered) and sync_site_packages.sh for site-packages and the app. SERIOUS_PYTHON_SITE_PACKAGES, SERIOUS_PYTHON_APP and the provider XCFrameworks are not modified.
  • CI: a new step in bridge_example_macos fails if any .so/.dylib in the built app is still linker-signed, on both the CocoaPods and SwiftPM legs.
  • README subsection and 4.7.2 changelog entries (darwin and umbrella). No version bumps.

Verification

Repro of the reporter's setup (Python 3.14, Pillow, their env vars):

before after
linker-signed Mach-O files in the built app 63/86 0/86 (CocoaPods and SwiftPM)
identifiers Xcode records at export ending in .so yes none
Xcode 26.5-style signing (the reporter's requirement format) satisfies the requirement 23/86 86/86
  • App Store Connect: patched builds made through CocoaPods and through SwiftPM both finished processing with no errors. A control upload with deliberately mismatched requirements reproduced the reporter's 90238 text word for word.
  • The reporter confirmed that the equivalent Xcode build-phase workaround fixes their upload on Xcode 26.5; TestFlight install and app launch work.
  • Local run of the bridge_example_macos CI steps on both build systems: all integration tests pass, and the new check passes (it fails on an unpatched build).
  • iOS packaging on both build systems is unchanged; the provider checks pass.
  • shellcheck reports no new findings; actionlint is clean.

Notes

  • codesign --verify exits 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.
  • Checking a file costs roughly 13 ms, and ad-hoc signing is deterministic, so re-signing freshly synced copies reproduces the same bytes and the SwiftPM cache key stays stable.
  • Running the example locally on Xcode 27 needs its macOS deployment target raised to 12.0 (Xcode 27 rejects 11.0); that is unrelated to this change and not part of this PR.

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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Medium severity

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.

Comment thread src/serious_python_darwin/darwin/prepare_macos.sh
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.
@FeodorFitsner
FeodorFitsner merged commit 0d4b181 into main Sep 24, 2026
108 of 111 checks passed
@FeodorFitsner
FeodorFitsner deleted the fix/macos-resign-linker-signed branch September 24, 2026 23:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Problems with macOS codesign with serious_python 4.7.0

3 participants