diff --git a/Cargo.lock b/Cargo.lock index aa04372cf..c654d39fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -85,7 +85,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" dependencies = [ - "object", + "object 0.37.3", ] [[package]] @@ -97,6 +97,31 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arwen" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d44cbd9bd79165abe331ebabb9dd4d59a5dc93791be33ff15ebd71baaadc85ba" +dependencies = [ + "clap", + "goblin", + "object 0.38.1", + "scroll", + "thiserror 2.0.18", +] + +[[package]] +name = "arwen-codesign" +version = "0.0.1-alpha.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35d7a19757bfe3658d5a95bf25a0492f29ebb21933549bdbfa4075c895510124" +dependencies = [ + "goblin", + "scroll", + "sha2", + "tempfile", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -1016,6 +1041,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.3.2" @@ -1174,7 +1205,7 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -1182,6 +1213,9 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash 0.2.0", +] [[package]] name = "heck" @@ -1635,6 +1669,8 @@ name = "maturin" version = "1.12.6" dependencies = [ "anyhow", + "arwen", + "arwen-codesign", "base64", "bytesize", "cargo-config2", @@ -1879,6 +1915,20 @@ dependencies = [ "memchr", ] +[[package]] +name = "object" +version = "0.38.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271638cd5fa9cca89c4c304675ca658efc4e64a66c716b7cfe1afb4b9611dbbc" +dependencies = [ + "crc32fast", + "flate2", + "hashbrown 0.16.1", + "indexmap", + "memchr", + "ruzstd", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -2632,6 +2682,15 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "ruzstd" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ff0cc5e135c8870a775d3320910cd9b564ec036b4dc0b8741629020be63f01" +dependencies = [ + "twox-hash", +] + [[package]] name = "same-file" version = "1.0.6" diff --git a/Cargo.toml b/Cargo.toml index 0c12af032..386b0b9a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -148,6 +148,10 @@ which = { version = "8.0.0", optional = true } memmap2 = "0.9.9" reflink-copy = "0.1.29" +# auditwheel repair (macOS delocate) +arwen = { version = "0.0.5", optional = true } +arwen-codesign = { version = "0.0.1-alpha.1", optional = true } + [dev-dependencies] expect-test = "1.4.1" fs4 = { version = "0.13.1", features = ["fs-err3"] } @@ -165,7 +169,9 @@ which = "8.0.0" [features] default = ["full", "rustls"] -full = ["cli-completion", "cross-compile", "scaffolding", "upload", "sbom"] +full = ["cli-completion", "cross-compile", "scaffolding", "upload", "sbom", "auditwheel"] + +auditwheel = ["dep:arwen", "dep:arwen-codesign"] cli-completion = ["dep:clap_complete_command"] diff --git a/src/auditwheel/macos.rs b/src/auditwheel/macos.rs new file mode 100644 index 000000000..f6469a84b --- /dev/null +++ b/src/auditwheel/macos.rs @@ -0,0 +1,309 @@ +//! macOS/Mach-O wheel audit and repair (delocate equivalent). +//! +//! This module implements [`WheelRepairer`] for macOS Mach-O binaries, +//! providing the Rust equivalent of [delocate](https://github.com/matthew-brett/delocate). +//! +//! Uses `arwen` for Mach-O install name / rpath manipulation and +//! pure-Rust signing helpers from `macos_sign` for both thin and fat binaries. + +use super::Policy; +use super::audit::relpath; +use super::macos_sign::ad_hoc_sign; +use super::repair::{AuditedArtifact, GraftedLib, WheelRepairer, leaf_filename}; +use crate::compile::BuildArtifact; +use anyhow::{Context, Result, bail}; +use arwen::macho::MachoContainer; +use lddtree::Library; +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; + +/// macOS/Mach-O wheel repairer (delocate equivalent). +/// +/// Bundles external `.dylib` files and rewrites Mach-O install names +/// and rpaths so that `@loader_path`-relative references resolve to +/// the bundled copies in the `.dylibs/` directory. +pub struct MacOSRepairer; + +impl WheelRepairer for MacOSRepairer { + fn audit( + &self, + artifact: &BuildArtifact, + ld_paths: Vec, + ) -> Result<(Policy, Vec)> { + let ext_libs = find_external_libs(&artifact.path, ld_paths)?; + Ok((Policy::default(), ext_libs)) + } + + fn patch( + &self, + artifacts: &[AuditedArtifact], + grafted: &[GraftedLib], + libs_dir: &Path, + artifact_dir: &Path, + ) -> Result<()> { + // Build a lookup from all known install names → new leaf name. + let mut name_map: BTreeMap<&str, &str> = BTreeMap::new(); + for lib in grafted { + name_map.insert(lib.original_name.as_str(), lib.new_name.as_str()); + for alias in &lib.aliases { + name_map.insert(alias.as_str(), lib.new_name.as_str()); + } + } + + // 1. Patch each grafted library: set install id, rewrite cross-references, + // remove absolute rpaths, then ad-hoc codesign. + for lib in grafted { + let new_install_id = format!("/DLC/{}/{}", libs_dir.display(), lib.new_name); + + // Collect rpaths to remove (all non-relative rpaths). + let rpaths_to_remove: Vec<&str> = lib + .rpath + .iter() + .filter(|r| !r.starts_with("@loader_path") && !r.starts_with("@executable_path")) + .map(String::as_str) + .collect(); + + // Collect install name changes for cross-references between grafted libs. + let install_name_changes: Vec<(&str, String)> = lib + .needed + .iter() + .filter_map(|n| { + name_map + .get(n.as_str()) + .map(|new| (n.as_str(), format!("@loader_path/{new}"))) + }) + .collect(); + + patch_macho( + &lib.dest_path, + &install_name_changes, + Some(&new_install_id), + &rpaths_to_remove, + )?; + + ad_hoc_sign(&lib.dest_path)?; + } + + // 2. Patch each artifact: rewrite references to grafted libs using + // @loader_path-relative names. + let rel = relpath(libs_dir, artifact_dir); + for audited in artifacts { + let install_name_changes: Vec<(&str, String)> = name_map + .iter() + .map(|(old, new)| { + let relative = Path::new("@loader_path").join(&rel).join(new); + (*old, relative.to_string_lossy().into_owned()) + }) + .collect(); + + if !install_name_changes.is_empty() { + patch_macho(&audited.artifact.path, &install_name_changes, None, &[])?; + ad_hoc_sign(&audited.artifact.path)?; + } + } + + Ok(()) + } + + fn libs_dir(&self, dist_name: &str) -> PathBuf { + PathBuf::from(format!("{dist_name}.dylibs")) + } +} + +/// Check if a library path is a macOS system library that should not be bundled. +/// +/// System libraries live under `/usr/lib/` and `/System/`. Notably, +/// `/usr/local/lib/` (Homebrew) is NOT considered system — those libs +/// get bundled, matching Python delocate behaviour. +fn is_system_library(path: &Path) -> bool { + let s = path.to_string_lossy(); + s.starts_with("/usr/lib/") || s.starts_with("/System/") +} + +/// Check if a library name refers to libpython or Python.framework, which should never be bundled. +/// +/// This catches both: +/// - Traditional libpython: `/usr/local/lib/libpython3.12.dylib`, `@rpath/libpython3.10.dylib` +/// - Python framework: `/Library/Frameworks/Python.framework/Versions/3.14/Python` +/// - Free-threaded Python framework: `/Library/Frameworks/PythonT.framework/Versions/3.14/PythonT` +fn is_libpython(name: &str) -> bool { + // Check for Python.framework or PythonT.framework (macOS framework-style Python) + // PythonT.framework is used by free-threaded Python builds (e.g., Python 3.13t, 3.14t) + if name.contains("Python.framework") || name.contains("PythonT.framework") { + return true; + } + // Check for traditional libpython dylib + let leaf = leaf_filename(name); + leaf.starts_with("libpython3") +} + +/// Decide whether a dependency should be bundled or ignored. +/// +/// Unlike Linux, unresolved non-system Mach-O dependencies must fail the repair +/// because the resulting wheel would still be broken on another machine. +fn should_bundle_library(lib: &Library) -> Result { + if is_system_library(&lib.path) || is_libpython(&lib.name) { + return Ok(false); + } + + if lib.realpath.is_none() { + bail!( + "Cannot repair wheel, because required library {} could not be located.", + lib.path.display() + ); + } + + Ok(true) +} + +/// Find external shared library dependencies for a macOS artifact. +fn find_external_libs(artifact: impl AsRef, ld_paths: Vec) -> Result> { + let analyzer = if ld_paths.is_empty() { + lddtree::DependencyAnalyzer::default() + } else { + lddtree::DependencyAnalyzer::default().library_paths(ld_paths) + }; + let deps = analyzer + .analyze(artifact.as_ref()) + .context("Failed to analyze Mach-O dependencies")?; + + let mut ext_libs = Vec::new(); + for (_, lib) in deps.libraries { + if should_bundle_library(&lib)? { + ext_libs.push(lib); + } + } + Ok(ext_libs) +} + +/// Batch Mach-O patching: apply changes, re-parsing between operations to handle +/// offset shifts from install name changes. +fn patch_macho( + file: &Path, + install_name_changes: &[(&str, String)], + new_install_id: Option<&str>, + rpaths_to_remove: &[&str], +) -> Result<()> { + // Change install ID first (this can shift load command offsets) + if let Some(id) = new_install_id { + let data = fs_err::read(file)?; + let mut container = + MachoContainer::parse(&data).context("Failed to parse Mach-O for install_id change")?; + match container.change_install_id(id) { + Ok(()) => { + fs_err::write(file, &container.data)?; + } + Err(arwen::macho::MachoError::DylibIdMissing) => {} + Err(e) => return Err(e).context("Failed to change install id"), + } + } + + // Change install names (each can shift offsets, so re-parse between each) + for (old, new) in install_name_changes { + let data = fs_err::read(file)?; + let mut container = MachoContainer::parse(&data) + .context("Failed to parse Mach-O for install_name change")?; + match container.change_install_name(old, new) { + Ok(()) => { + fs_err::write(file, &container.data)?; + } + Err(arwen::macho::MachoError::DylibNameMissing(_)) => {} + Err(e) => { + return Err(e) + .with_context(|| format!("Failed to change install name {old} -> {new}")); + } + } + } + + // Remove rpaths + for rpath in rpaths_to_remove { + let data = fs_err::read(file)?; + let mut container = + MachoContainer::parse(&data).context("Failed to parse Mach-O for rpath removal")?; + match container.remove_rpath(rpath) { + Ok(()) => { + fs_err::write(file, &container.data)?; + } + Err(arwen::macho::MachoError::RpathMissing(_)) => {} + Err(e) => return Err(e).with_context(|| format!("Failed to remove rpath {rpath}")), + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn library(name: &str, path: &str, realpath: Option<&str>) -> Library { + Library { + name: name.to_string(), + path: PathBuf::from(path), + realpath: realpath.map(PathBuf::from), + needed: Vec::new(), + rpath: Vec::new(), + } + } + + #[test] + fn test_is_system_library() { + assert!(is_system_library(Path::new("/usr/lib/libSystem.B.dylib"))); + assert!(is_system_library(Path::new( + "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation" + ))); + assert!(!is_system_library(Path::new("/usr/local/lib/libfoo.dylib"))); + assert!(!is_system_library(Path::new( + "/opt/homebrew/lib/libbar.dylib" + ))); + } + + #[test] + fn test_is_libpython() { + // Traditional libpython dylibs + assert!(is_libpython("libpython3.12.dylib")); + assert!(is_libpython("/usr/local/lib/libpython3.11.dylib")); + assert!(is_libpython("@rpath/libpython3.10.dylib")); + // Python.framework (macOS framework-style Python) + assert!(is_libpython( + "/Library/Frameworks/Python.framework/Versions/3.14/Python" + )); + assert!(is_libpython( + "/opt/homebrew/Frameworks/Python.framework/Versions/3.12/Python" + )); + // PythonT.framework (free-threaded Python builds, e.g., 3.13t, 3.14t) + assert!(is_libpython( + "/Library/Frameworks/PythonT.framework/Versions/3.14/PythonT" + )); + assert!(is_libpython( + "/opt/homebrew/Frameworks/PythonT.framework/Versions/3.13/PythonT" + )); + // Non-Python libraries + assert!(!is_libpython("libfoo.dylib")); + assert!(!is_libpython("libpython2.7.dylib")); + } + + #[test] + fn test_missing_non_system_dependency_errors() { + let err = + should_bundle_library(&library("@rpath/libfoo.dylib", "@rpath/libfoo.dylib", None)) + .unwrap_err(); + + assert_eq!( + err.to_string(), + "Cannot repair wheel, because required library @rpath/libfoo.dylib could not be located." + ); + } + + #[test] + fn test_missing_system_dependency_is_ignored() { + assert!( + !should_bundle_library(&library( + "/usr/lib/libSystem.B.dylib", + "/usr/lib/libSystem.B.dylib", + None, + )) + .unwrap() + ); + } +} diff --git a/src/auditwheel/macos_sign.rs b/src/auditwheel/macos_sign.rs new file mode 100644 index 000000000..87e5ffd59 --- /dev/null +++ b/src/auditwheel/macos_sign.rs @@ -0,0 +1,238 @@ +//! Pure-Rust ad-hoc code signing for Mach-O binaries. +//! +//! This module provides ad-hoc code signing for both thin and fat (universal) Mach-O +//! binaries. On macOS, it uses Apple's `codesign` CLI tool directly. For cross-compilation +//! from other platforms, it uses `arwen-codesign` as a pure-Rust fallback. +//! +//! The signatures produced are functionally equivalent to those created by Apple's +//! `codesign -s -` command and pass all verification checks. + +use anyhow::{Context, Result}; +#[cfg(not(target_os = "macos"))] +use arwen_codesign::adhoc_sssspSAnsOpSonsOptions; +#[cfg(not(target_os = "macos"))] +use fat_macho::{Error as FatMachoError, FatReader, FatWriter}; +use std::path::Path; +#[cfg(target_os = "macos")] +use std::process::Command; +#[cfg(not(target_os = "macos"))] +use tempfile::NamedTempFile; + +/// Ad-hoc codesign Mach-O bytes, handling both thin and fat (universal) binaries. +/// +/// For fat binaries, each architecture slice is signed individually and then +/// the slices are reassembled into a new fat binary. This approach requires that +/// each thin slice has an existing `LC_CODE_SIGNATURE` load command (which is the +/// case for binaries produced by modern Apple toolchains with `-Wl,-adhoc_codesign`). +#[cfg(not(target_os = "macos"))] +fn ad_hoc_sign_macho_bytes(data: Vec, identifier: &str) -> Result> { + match FatReader::new(&data) { + Ok(reader) => { + let mut writer = FatWriter::new(); + for arch in reader.iter_arches() { + let arch = arch.with_context(|| { + format!("Failed to iterate fat Mach-O slices for {identifier}") + })?; + let signed = sign_thin_macho_slice(arch.slice(&data).to_vec(), identifier)?; + writer.add(signed).with_context(|| { + format!("Failed to rebuild fat Mach-O slices for {identifier}") + })?; + } + + let mut rebuilt = Vec::new(); + writer + .write_to(&mut rebuilt) + .with_context(|| format!("Failed to write fat Mach-O for {identifier}"))?; + Ok(rebuilt) + } + Err(FatMachoError::NotFatBinary) => sign_thin_macho_slice(data, identifier), + Err(err) => { + Err(err).with_context(|| format!("Failed to parse fat Mach-O for {identifier}")) + } + } +} + +#[cfg(not(target_os = "macos"))] +fn sign_thin_macho_slice(data: Vec, identifier: &str) -> Result> { + adhoc_sign(data, &AdhocSignOptions::new(identifier)) + .with_context(|| format!("Failed to ad-hoc codesign Mach-O slice {identifier}")) +} + +/// Ad-hoc codesign a Mach-O file at the given path. +/// +/// On macOS, uses Apple's `codesign` CLI tool directly. For cross-compilation +/// from other platforms, uses the pure-Rust `arwen-codesign` library. +#[cfg(target_os = "macos")] +pub(crate) fn ad_hoc_sign(path: &Path) -> Result<()> { + let output = Command::new("codesign") + .args(["-s", "-", "-f"]) + .arg(path) + .output() + .context("Failed to run codesign command")?; + + if !output.status.success() { + anyhow::bail!( + "codesign failed for {}: {}", + path.display(), + String::from_utf8_lossy(&output.stderr) + ); + } + Ok(()) +} + +/// Ad-hoc codesign a Mach-O file at the given path. +/// +/// Uses the pure-Rust `arwen-codesign` library for cross-compilation scenarios +/// where Apple's `codesign` tool is not available. +#[cfg(not(target_os = "macos"))] +pub(crate) fn ad_hoc_sign(path: &Path) -> Result<()> { + let data = fs_err::read(path)?; + let identifier = path + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or("unknown"); + + let signed = ad_hoc_sign_macho_bytes(data, identifier)?; + let metadata = fs_err::metadata(path)?; + let parent = path.parent().unwrap_or_else(|| Path::new(".")); + let mut temp = NamedTempFile::new_in(parent)?; + use std::io::Write; + temp.write_all(&signed)?; + temp.as_file().sync_all()?; + fs_err::set_permissions(temp.path(), metadata.permissions())?; + temp.persist(path) + .map_err(|err| err.error) + .with_context(|| format!("Failed to persist signed Mach-O {}", path.display()))?; + Ok(()) +} + +#[cfg_attr(target_os = "macos", cfg(test))] +mod tests { + use super::*; + use fat_macho::{FatReader, FatWriter}; + use std::process::Command; + + /// Check if the given bytes represent a fat (universal) Mach-O binary. + /// + /// Fat binaries use magic `0xcafebabe` (big-endian) or `0xbebafeca` (little-endian). + fn is_fat_macho(data: &[u8]) -> bool { + matches!( + data.get(..4), + Some([0xca, 0xfe, 0xba, 0xbe] | [0xbe, 0xba, 0xfe, 0xca]) + ) + } + + /// Compile a minimal Mach-O binary for the given architecture. + /// Returns the path to the compiled binary. + fn compile_thin_macho(dir: &Path, arch: &str) -> std::path::PathBuf { + /// Minimal C source that compiles to a tiny Mach-O executable. + const MINIMAL_C_SOURCE: &str = "int main(){return 0;}"; + + let src = dir.join("main.c"); + let out = dir.join(format!("main_{arch}")); + fs_err::write(&src, MINIMAL_C_SOURCE).unwrap(); + + let status = Command::new("clang") + .args([ + "-arch", + arch, + // Ensure LC_CODE_SIGNATURE is present even when cross-compiling + "-Wl,-adhoc_codesign", + "-o", + ]) + .arg(&out) + .arg(&src) + .status() + .expect("Failed to run clang"); + assert!(status.success(), "clang failed for {arch}"); + out + } + + #[test] + fn signs_thin_binary_and_verifies() { + let temp_dir = tempfile::tempdir().unwrap(); + let thin = compile_thin_macho(temp_dir.path(), "arm64"); + + ad_hoc_sign(&thin).unwrap(); + + let output = Command::new("codesign") + .args(["--verify", "--verbose"]) + .arg(&thin) + .output() + .unwrap(); + assert!( + output.status.success(), + "codesign --verify failed for thin binary: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + + #[test] + fn signs_thin_x86_64_binary_and_verifies() { + let temp_dir = tempfile::tempdir().unwrap(); + let thin = compile_thin_macho(temp_dir.path(), "x86_64"); + + ad_hoc_sign(&thin).unwrap(); + + let output = Command::new("codesign") + .args(["--verify", "--verbose"]) + .arg(&thin) + .output() + .unwrap(); + assert!( + output.status.success(), + "codesign --verify failed for thin x86_64 binary: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + + #[test] + fn signs_fat_binary_from_thin_slices() { + // Test the fat binary signing flow by building thin binaries, + // manually creating a fat binary with FatWriter, and signing it. + // This simulates what happens when bundling dylibs from different + // architectures into a universal binary. + let temp_dir = tempfile::tempdir().unwrap(); + let arm64 = compile_thin_macho(temp_dir.path(), "arm64"); + let x86_64 = compile_thin_macho(temp_dir.path(), "x86_64"); + + // Read the thin binaries (each has its own LC_CODE_SIGNATURE) + let arm64_data = fs_err::read(&arm64).unwrap(); + let x86_64_data = fs_err::read(&x86_64).unwrap(); + + // Build fat binary from self-contained thin slices + let mut writer = FatWriter::new(); + writer.add(arm64_data).unwrap(); + writer.add(x86_64_data).unwrap(); + + let mut fat = Vec::new(); + writer.write_to(&mut fat).unwrap(); + + // Verify it's a fat binary + assert!(is_fat_macho(&fat), "Expected fat binary"); + + // Write to file and sign with codesign CLI + let fat_path = temp_dir.path().join("universal"); + fs_err::write(&fat_path, &fat).unwrap(); + + ad_hoc_sign(&fat_path).unwrap(); + + // Verify both slices are present after signing + let signed = fs_err::read(&fat_path).unwrap(); + let reader = FatReader::new(&signed).unwrap(); + assert!(reader.extract("arm64").is_some(), "arm64 slice missing"); + assert!(reader.extract("x86_64").is_some(), "x86_64 slice missing"); + + // Verify with codesign + let output = Command::new("codesign") + .args(["--verify", "--verbose"]) + .arg(&fat_path) + .output() + .unwrap(); + assert!( + output.status.success(), + "codesign --verify failed for fat binary: {}", + String::from_utf8_lossy(&output.stderr) + ); + } +} diff --git a/src/auditwheel/mod.rs b/src/auditwheel/mod.rs index 43776c474..0688c9d69 100644 --- a/src/auditwheel/mod.rs +++ b/src/auditwheel/mod.rs @@ -1,5 +1,9 @@ mod audit; mod linux; +#[cfg(feature = "auditwheel")] +mod macos; +#[cfg(feature = "auditwheel")] +mod macos_sign; mod musllinux; pub mod patchelf; mod platform_tag; @@ -12,6 +16,8 @@ mod whichprovides; pub use audit::*; pub use linux::ElfRepairer; +#[cfg(feature = "auditwheel")] +pub use macos::MacOSRepairer; pub use platform_tag::PlatformTag; pub use policy::Policy; pub use repair::{AuditedArtifact, WheelRepairer, log_grafted_libs, prepare_grafted_libs}; diff --git a/src/build_context/repair.rs b/src/build_context/repair.rs index 0ba445ec6..929facf8b 100644 --- a/src/build_context/repair.rs +++ b/src/build_context/repair.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "auditwheel")] +use crate::auditwheel::MacOSRepairer; #[cfg(feature = "sbom")] use crate::auditwheel::get_sysroot_path; use crate::auditwheel::{ @@ -48,8 +50,14 @@ impl BuildContext { allow_linking_libpython, })) } else if self.project.target.is_macos() { - // TODO: MacOSRepairer (Phase 2) - None + #[cfg(feature = "auditwheel")] + { + Some(Box::new(MacOSRepairer)) + } + #[cfg(not(feature = "auditwheel"))] + { + None + } } else { None }