feat: re-implement delocate for repairing macOS wheels#3114
Merged
Conversation
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.
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.
17fa846 to
2c5abc0
Compare
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.
Contributor
There was a problem hiding this comment.
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
MacOSRepairerto audit/patch Mach-O wheels and bundle external.dylibdependencies. - Add a
macos_signhelper module for ad-hoc signing on macOS (viacodesign) and non-macOS (viaarwen-codesign). - Add
auditwheelCargo feature + dependencies and enable it in thefullfeature 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. |
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).
…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.
2 tasks
- 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
- 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²)
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.
This PR implements native macOS wheel repair in maturin, providing the Rust equivalent of Python's
delocatetool.What it does
When building macOS wheels, maturin now automatically:
.dylibdependencies.dylibs/directory inside the wheel@loader_path-relative referencesKey features
/usr/lib/,/System/), libpython, and Python.framework (including free-threadedPythonT.framework)Implementation
MacOSRepairerimplements theWheelRepairertrait introduced in refactor: introduceWheelRepairertrait #3112arwencrate for Mach-O install name/rpath manipulation