Skip to content

feat: re-implement delocate for repairing macOS wheels#3114

Merged
messense merged 15 commits into
PyO3:mainfrom
messense:delocate
Apr 4, 2026
Merged

feat: re-implement delocate for repairing macOS wheels#3114
messense merged 15 commits into
PyO3:mainfrom
messense:delocate

Conversation

@messense

@messense messense commented Apr 2, 2026

Copy link
Copy Markdown
Member

This PR implements native macOS wheel repair in maturin, providing the Rust equivalent of Python's delocate tool.

What it does

When building macOS wheels, maturin now automatically:

  • Audits Mach-O binaries for external .dylib dependencies
  • Bundles non-system libraries into a .dylibs/ directory inside the wheel
  • Rewrites install names to use @loader_path-relative references
  • Ad-hoc codesigns all modified binaries (required on Apple Silicon)

Key features

  • Universal2 support: Analyzes each architecture slice separately to discover arch-specific dependencies, then verifies bundled dylibs contain all required architecture slices
  • Smart dependency filtering: Skips system libraries (/usr/lib/, /System/), libpython, and Python.framework (including free-threaded PythonT.framework)
  • Transitive dependency handling: Only bundles libraries actually reachable from non-skipped dependencies
  • Cross-compilation support: Works with SDK sysroots (e.g., osxcross)
  • Pure-Rust signing: Uses arwen-codesign for cross-compilation scenarios; native codesign CLI on macOS

Implementation

messense added 4 commits April 2, 2026 19:02
Add arwen = 0.0.5 (Mach-O patching) and arwen-codesign = 0.0.1-alpha.1
(pure Rust ad-hoc codesigning) as optional dependencies behind a new
'auditwheel' Cargo feature. Add 'auditwheel' to the 'full' feature list.

These enable cross-platform wheel repair:
- macOS: Mach-O install name rewriting and ad-hoc codesigning
- Works from any host OS (pure Rust, no macOS tools needed)
Add src/auditwheel/macos.rs with MacOSRepairer that uses arwen for
Mach-O install name/rpath manipulation and arwen-codesign for pure-Rust
ad-hoc code signing. No macOS-only tool dependencies.

Key behavior:
- Filters system libraries (/usr/lib/*, /System/*) and libpython
- Rewrites LC_LOAD_DYLIB to @loader_path-relative names
- Sets LC_ID_DYLIB to /DLC/<libs_dir>/<name> (matching delocate)
- Removes absolute rpaths, keeps @loader_path/@executable_path
- Ad-hoc codesigns all modified binaries (cross-platform)
- Uses .dylibs directory (matching delocate convention)
Update auditwheel/mod.rs to export MacOSRepairer (feature-gated behind
'auditwheel'). Wire it into make_repairer() in build_context/repair.rs
so macOS builds use MacOSRepairer for wheel repair instead of returning
None.
The is_libpython check now recognizes Python.framework paths in addition
to traditional libpython3.*.dylib files. This fixes pyo3-bin bindings
that link against /Library/Frameworks/Python.framework/Versions/X/Python.

Also adds should_bundle_library() to properly error on missing non-system
dependencies instead of silently skipping them.
messense added 2 commits April 2, 2026 20:48
arwen's MachoContainer caches parsed load command offsets, which become
stale after modifications that change install name lengths. Re-parsing
between each operation ensures correct offsets are used.

This fixes corruption when changing install names to longer strings,
which shifts subsequent load commands in the binary.
The is_libpython() check only recognized Python.framework but
free-threaded Python builds (3.13t, 3.14t) use PythonT.framework.
This caused the framework to be bundled and the binary patched to
reference a non-existent hashed library name.
The pure-Rust arwen-codesign library requires an LC_CODE_SIGNATURE load
command which older dylibs (e.g., Homebrew's libintl) don't have. Use
Apple's codesign CLI directly on macOS for reliability, keeping the
pure-Rust implementation for cross-compilation from other platforms.
Use reachability analysis to avoid bundling libraries that are only
transitive dependencies of skipped libraries (e.g., libintl via
Python.framework). Only bundle libs reachable from the artifact
without going through a skipped library.
@messense messense requested a review from Copilot April 3, 2026 13:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a macOS wheel “repair” implementation (delocate equivalent) and wires it into the build flow under a new auditwheel feature, including ad-hoc Mach-O signing support.

Changes:

  • Introduce MacOSRepairer to audit/patch Mach-O wheels and bundle external .dylib dependencies.
  • Add a macos_sign helper module for ad-hoc signing on macOS (via codesign) and non-macOS (via arwen-codesign).
  • Add auditwheel Cargo feature + dependencies and enable it in the full feature set.

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/build_context/repair.rs Enables selecting MacOSRepairer for macOS targets when auditwheel feature is on.
src/auditwheel/mod.rs Adds macOS modules and re-exports MacOSRepairer behind auditwheel feature gating.
src/auditwheel/macos_sign.rs New module implementing ad-hoc Mach-O signing for macOS/cross builds.
src/auditwheel/macos.rs New WheelRepairer implementation for macOS (delocate-like audit/patch).
Cargo.toml Adds optional deps (arwen, arwen-codesign) and introduces/enables auditwheel feature.

Comment thread src/auditwheel/macos.rs
Comment thread src/auditwheel/macos.rs
Comment thread src/auditwheel/macos.rs Outdated
Comment thread src/auditwheel/macos.rs
Comment thread Cargo.toml
Comment thread Cargo.toml
Comment thread src/auditwheel/macos.rs Outdated
Comment thread src/auditwheel/macos.rs Outdated
Extract reachable_libs() as a pure helper so the BFS reachability
logic can be tested with synthetic dependency graphs. Add four test
cases covering: skipped transitive deps, shared deps via non-skipped
paths, transitive chains, and mid-chain skip blocking.

Also document the /DLC/ synthetic install ID convention (matching
Python's delocate tool).
@messense messense marked this pull request as ready for review April 4, 2026 00:16
…rification

For universal2 builds, lddtree can only analyze one architecture slice at a
time from a fat binary. This caused dependencies unique to one architecture
to be missed during wheel repair.

Changes:
- Add thin_artifacts field to BuildArtifact storing per-arch (path, linked_paths)
- MacOSRepairer::audit() now analyzes each thin binary separately, tracking
  which architectures require each library
- Add AuditResult struct to cleanly return audit output including arch requirements
- Add verify_universal_archs() using fat-macho to verify grafted dylibs contain
  all required architecture slices before patching
- Clear error messages when a thin dylib is used but multiple archs need it

This mirrors Python delocate's check_archs behavior - it assumes external dylibs
on the build system are already fat/universal.
@messense messense requested a review from Copilot April 4, 2026 02:00
@messense messense linked an issue Apr 4, 2026 that may be closed by this pull request
2 tasks

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 10 changed files in this pull request and generated 6 comments.

Comment thread src/auditwheel/macos.rs Outdated
Comment thread src/auditwheel/macos.rs Outdated
Comment thread src/auditwheel/macos.rs
Comment thread src/build_context/repair.rs Outdated
Comment thread src/auditwheel/macos.rs
Comment thread src/auditwheel/macos_sign.rs
- Add bounds check for thin_artifacts count in universal2 audit
- Fix rpath removal to only strip absolute paths (starts with '/')
- Optimize patch_macho to use in-memory buffer, write once at end
- Use archs.iter().cloned() instead of archs.clone() for efficiency
- Add comprehensive tests for verify_universal_archs function
@messense messense requested a review from Copilot April 4, 2026 02:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 10 changed files in this pull request and generated 5 comments.

Comment thread src/compile.rs
Comment thread src/compile.rs
Comment thread src/auditwheel/macos_sign.rs
Comment thread src/auditwheel/macos.rs
Comment thread Cargo.toml
- Add ThinArtifact struct with arch, path, linked_paths fields
- Remove index-based UNIVERSAL2_ARCHS mapping in macos.rs
- Use HashSet for O(n) linked_paths deduplication instead of O(n²)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 12 changed files in this pull request and generated 3 comments.

Comment thread src/compile.rs
Comment thread src/auditwheel/macos.rs
Comment thread src/auditwheel/macos.rs
@messense messense merged commit a8393eb into PyO3:main Apr 4, 2026
66 checks passed
@messense messense deleted the delocate branch April 4, 2026 13:20
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.

Audit and repair macOS wheels support

2 participants