diff --git a/Cargo.toml b/Cargo.toml index 57e6078f7..3228029be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -150,7 +150,7 @@ which = { version = "8.0.0", optional = true } memmap2 = "0.9.9" reflink-copy = "0.1.29" -# auditwheel repair (macOS delocate) +# auditwheel repair (ELF patching, macOS delocate) arwen = { version = "0.0.5", optional = true } [dev-dependencies] diff --git a/pyproject.toml b/pyproject.toml index 05c67cc1a..4ead14b13 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,8 @@ dynamic = ["version"] [project.optional-dependencies] zig = ["ziglang>=0.10.0"] -patchelf = ["patchelf"] +# Deprecated: The patchelf extra is not required anymore +patchelf = [] [project.urls] "Source Code" = "https://github.com/PyO3/maturin" diff --git a/src/auditwheel/audit.rs b/src/auditwheel/audit.rs index 8ac6da9ef..e8be7dc91 100644 --- a/src/auditwheel/audit.rs +++ b/src/auditwheel/audit.rs @@ -34,6 +34,10 @@ impl fmt::Display for AuditWheelMode { /// Get sysroot path from target C compiler /// /// Currently only gcc is supported, clang doesn't have a `--print-sysroot` option +#[cfg_attr( + not(any(feature = "auditwheel", feature = "sbom")), + allow(dead_code) +)] pub fn get_sysroot_path(target: &Target) -> Result { use std::process::{Command, Stdio}; @@ -86,6 +90,7 @@ pub fn get_sysroot_path(target: &Target) -> Result { Ok(PathBuf::from("/")) } +#[cfg_attr(not(feature = "auditwheel"), allow(dead_code))] pub fn relpath(to: &Path, from: &Path) -> PathBuf { let mut suffix_pos = 0; for (f, t) in from.components().zip(to.components()) { diff --git a/src/auditwheel/linux.rs b/src/auditwheel/linux.rs index 2a66d3e6c..145eafe66 100644 --- a/src/auditwheel/linux.rs +++ b/src/auditwheel/linux.rs @@ -5,16 +5,18 @@ //! //! It contains all ELF-specific logic: manylinux/musllinux compliance //! auditing, external dependency discovery via lddtree, versioned symbol -//! checking, and binary patching via `patchelf` (SONAME, DT_NEEDED, RPATH). +//! checking, and binary patching via the `arwen` crate (SONAME, DT_NEEDED, +//! RPATH). use super::audit::{get_sysroot_path, relpath}; use super::musllinux::{find_musl_libc, get_musl_version}; use super::policy::{MANYLINUX_POLICIES, MUSLLINUX_POLICIES, Policy}; use super::repair::{AuditResult, AuditedArtifact, GraftedLib, WheelRepairer}; -use super::{PlatformTag, patchelf}; +use super::PlatformTag; use crate::compile::BuildArtifact; use crate::target::{Arch, Target}; use anyhow::{Context, Result, bail}; +use arwen::elf::ElfContainer; use fs_err::File; use goblin::elf::{Elf, sym::STB_WEAK, sym::STT_FUNC}; use lddtree::Library; @@ -435,8 +437,8 @@ fn auditwheel_rs( /// Linux/ELF wheel repairer (auditwheel equivalent). /// /// Bundles external `.so` files and rewrites ELF metadata (SONAME, DT_NEEDED, -/// RPATH) using `patchelf` so that `$ORIGIN`-relative references resolve to -/// the bundled copies in the `.libs/` directory. +/// RPATH) using the `arwen` crate so that `$ORIGIN`-relative references +/// resolve to the bundled copies in the `.libs/` directory. /// /// Unlike the macOS repairer, `audit()` performs full /// manylinux/musllinux compliance checking — the returned [`Policy`] @@ -478,8 +480,6 @@ impl WheelRepairer for ElfRepairer { libs_dir: &Path, artifact_dir: &Path, ) -> Result<()> { - patchelf::verify_patchelf()?; - // Build a lookup from original name → new soname for rewriting references. let mut name_map: BTreeMap<&str, &str> = BTreeMap::new(); for l in grafted { @@ -491,55 +491,70 @@ impl WheelRepairer for ElfRepairer { // Set soname and rpath on each grafted library. for lib in grafted { - patchelf::set_soname(&lib.dest_path, &lib.new_name)?; + let contents = fs_err::read(&lib.dest_path)?; + let mut elf = ElfContainer::parse(&contents)?; + elf.set_soname(&lib.new_name)?; if !lib.rpath.is_empty() { - patchelf::set_rpath(&lib.dest_path, &"$ORIGIN".to_string())?; + elf.remove_runpath()?; + elf.add_runpath("$ORIGIN")?; + elf.force_rpath()?; } + elf.write_to_path(&lib.dest_path)?; } // Rewrite DT_NEEDED in each artifact to reference new sonames. // Only replace entries that the artifact actually depends on to avoid - // unnecessary patchelf invocations and errors when an old name is - // absent from a given binary. + // unnecessary work when an old name is absent from a given binary. for aa in audited { let artifact_deps: HashSet<&str> = aa .external_libs .iter() .map(|lib| lib.name.as_str()) .collect(); - let replacements: Vec<_> = name_map + let replacements: HashMap = name_map .iter() .filter(|(old, _)| artifact_deps.contains(**old)) - .map(|(k, v)| (*k, v.to_string())) + .map(|(k, v)| ((*k).to_string(), (*v).to_string())) .collect(); if !replacements.is_empty() { - patchelf::replace_needed(&aa.artifact.path, &replacements)?; + let contents = fs_err::read(&aa.artifact.path)?; + let mut elf = ElfContainer::parse(&contents)?; + elf.replace_needed(&replacements)?; + elf.write_to_path(&aa.artifact.path)?; } } // Update cross-references between grafted libraries for lib in grafted { - let lib_replacements: Vec<_> = lib + let lib_replacements: HashMap = lib .needed .iter() .filter_map(|n| { name_map .get(n.as_str()) - .map(|new| (n.as_str(), new.to_string())) + .map(|new| (n.clone(), (*new).to_string())) }) .collect(); if !lib_replacements.is_empty() { - patchelf::replace_needed(&lib.dest_path, &lib_replacements)?; + let contents = fs_err::read(&lib.dest_path)?; + let mut elf = ElfContainer::parse(&contents)?; + elf.replace_needed(&lib_replacements)?; + elf.write_to_path(&lib.dest_path)?; } } // Set RPATH on artifacts to find the libs directory for aa in audited { - let mut new_rpaths = patchelf::get_rpath(&aa.artifact.path)?; + let contents = fs_err::read(&aa.artifact.path)?; + let mut elf = ElfContainer::parse(&contents)?; + let mut new_rpaths = elf.get_rpath(); let new_rpath = Path::new("$ORIGIN").join(relpath(libs_dir, artifact_dir)); new_rpaths.push(new_rpath.to_str().unwrap().to_string()); let new_rpath = new_rpaths.join(":"); - patchelf::set_rpath(&aa.artifact.path, &new_rpath)?; + elf.remove_runpath()?; + elf.add_runpath(new_rpath)?; + elf.force_rpath()?; + elf.write_to_path(&aa.artifact.path)?; } Ok(()) @@ -550,7 +565,19 @@ impl WheelRepairer for ElfRepairer { if aa.artifact.linked_paths.is_empty() { continue; } - let old_rpaths = patchelf::get_rpath(&aa.artifact.path)?; + let contents = fs_err::read(&aa.artifact.path)?; + let mut elf = match ElfContainer::parse(&contents) { + Ok(elf) => elf, + Err(err) => { + eprintln!( + "⚠️ Warning: Failed to parse {}: {}", + aa.artifact.path.display(), + err + ); + continue; + } + }; + let old_rpaths = elf.get_rpath(); let mut new_rpaths = old_rpaths.clone(); for path in &aa.artifact.linked_paths { if !old_rpaths.contains(path) { @@ -562,7 +589,16 @@ impl WheelRepairer for ElfRepairer { // binary may have been partially written and is no longer a // clean cargo output. aa.artifact.cargo_output_path = None; - if let Err(err) = patchelf::set_rpath(&aa.artifact.path, &new_rpath) { + + // Pseudo-try-block + let result: arwen::elf::Result<()> = (|| { + elf.remove_runpath()?; + elf.add_runpath(new_rpath)?; + elf.force_rpath()?; + elf.write_to_path(&aa.artifact.path)?; + Ok(()) + })(); + if let Err(err) = result { eprintln!( "⚠️ Warning: Failed to set rpath for {}: {}", aa.artifact.path.display(), diff --git a/src/auditwheel/mod.rs b/src/auditwheel/mod.rs index 5ee4d5eda..61fc99699 100644 --- a/src/auditwheel/mod.rs +++ b/src/auditwheel/mod.rs @@ -1,11 +1,12 @@ mod audit; +#[cfg(feature = "auditwheel")] mod linux; #[cfg(feature = "auditwheel")] mod macos; #[cfg(feature = "auditwheel")] mod macos_sign; +#[cfg(feature = "auditwheel")] mod musllinux; -pub mod patchelf; #[cfg(feature = "auditwheel")] pub(crate) mod pe_patch; mod platform_tag; @@ -19,6 +20,7 @@ mod whichprovides; mod windows; pub use audit::*; +#[cfg(feature = "auditwheel")] pub use linux::ElfRepairer; #[cfg(feature = "auditwheel")] pub use macos::MacOSRepairer; diff --git a/src/auditwheel/patchelf.rs b/src/auditwheel/patchelf.rs deleted file mode 100644 index 2c4a142bb..000000000 --- a/src/auditwheel/patchelf.rs +++ /dev/null @@ -1,105 +0,0 @@ -use anyhow::{Context, Result, bail}; -use std::ffi::OsStr; -use std::path::Path; -use std::process::Command; - -static MISSING_PATCHELF_ERROR: &str = "Failed to execute 'patchelf', did you install it? Hint: Try `pip install maturin[patchelf]` (or just `pip install patchelf`)"; - -/// Run a patchelf command with the given arguments. -/// -/// Returns `Ok(stdout)` on success, or an error with the stderr message. -fn run_patchelf(args: &[&OsStr]) -> Result> { - let output = Command::new("patchelf") - .args(args) - .output() - .context(MISSING_PATCHELF_ERROR)?; - if !output.status.success() { - bail!( - "patchelf failed: {}", - String::from_utf8_lossy(&output.stderr) - ); - } - Ok(output.stdout) -} - -/// Verify patchelf version -pub fn verify_patchelf() -> Result<()> { - let stdout = run_patchelf(&[OsStr::new("--version")])?; - let version = String::from_utf8(stdout) - .context("Failed to parse patchelf version")? - .trim() - .to_string(); - let version = version.strip_prefix("patchelf").unwrap_or(&version).trim(); - let semver = version.parse::().context( - "Failed to parse patchelf version, auditwheel repair requires patchelf >= 0.14.0.", - )?; - if semver < semver::Version::new(0, 14, 0) { - bail!( - "patchelf {} found. auditwheel repair requires patchelf >= 0.14.0.", - version - ); - } - Ok(()) -} - -/// Replace a declared dependency on a dynamic library with another one (`DT_NEEDED`) -pub fn replace_needed, N: AsRef>( - file: impl AsRef, - old_new_pairs: &[(O, N)], -) -> Result<()> { - let mut args: Vec<&OsStr> = Vec::new(); - for (old, new) in old_new_pairs { - args.push(OsStr::new("--replace-needed")); - args.push(old.as_ref()); - args.push(new.as_ref()); - } - args.push(file.as_ref().as_os_str()); - run_patchelf(&args)?; - Ok(()) -} - -/// Change `SONAME` of a dynamic library -pub fn set_soname>(file: impl AsRef, soname: &S) -> Result<()> { - run_patchelf(&[ - OsStr::new("--set-soname"), - soname.as_ref(), - file.as_ref().as_os_str(), - ])?; - Ok(()) -} - -/// Remove a `RPATH` from executables and libraries -pub fn remove_rpath(file: impl AsRef) -> Result<()> { - run_patchelf(&[OsStr::new("--remove-rpath"), file.as_ref().as_os_str()])?; - Ok(()) -} - -/// Change the `RPATH` of executables and libraries -pub fn set_rpath>(file: impl AsRef, rpath: &S) -> Result<()> { - remove_rpath(&file)?; - run_patchelf(&[ - OsStr::new("--force-rpath"), - OsStr::new("--set-rpath"), - rpath.as_ref(), - file.as_ref().as_os_str(), - ])?; - Ok(()) -} - -/// Get the `RPATH` of executables and libraries -pub fn get_rpath(file: impl AsRef) -> Result> { - let file = file.as_ref(); - let contents = fs_err::read(file)?; - match goblin::Object::parse(&contents) { - Ok(goblin::Object::Elf(elf)) => { - let rpaths = if !elf.runpaths.is_empty() { - elf.runpaths - } else { - elf.rpaths - }; - Ok(rpaths.iter().map(|r| r.to_string()).collect()) - } - Ok(_) => bail!("'{}' is not an ELF file", file.display()), - Err(e) => bail!("Failed to parse ELF file at '{}': {}", file.display(), e), - } -} diff --git a/src/auditwheel/policy.rs b/src/auditwheel/policy.rs index e15bed194..9dc0191cf 100644 --- a/src/auditwheel/policy.rs +++ b/src/auditwheel/policy.rs @@ -98,6 +98,7 @@ impl Policy { } } + #[cfg_attr(not(feature = "auditwheel"), allow(dead_code))] pub(crate) fn fixup_musl_libc_so_name(&mut self, target_arch: Arch) { // Fixup musl libc lib_whitelist if self.name.starts_with("musllinux") && self.lib_whitelist.remove("libc.so") { diff --git a/src/auditwheel/repair.rs b/src/auditwheel/repair.rs index f70730dd3..1d802d6c4 100644 --- a/src/auditwheel/repair.rs +++ b/src/auditwheel/repair.rs @@ -63,8 +63,10 @@ pub struct GraftedLib { /// Path to the writable temporary copy (ready for patching). pub dest_path: PathBuf, /// Libraries this one depends on (from lddtree's `needed` field). + #[cfg_attr(not(feature = "auditwheel"), allow(dead_code))] pub needed: Vec, /// Runtime library search paths from the original library. + #[cfg_attr(not(feature = "auditwheel"), allow(dead_code))] pub rpath: Vec, /// **Universal2 only**: CPU architectures that require this library. /// @@ -77,6 +79,7 @@ pub struct GraftedLib { /// /// Note: Universal2 support may be removed when Apple drops x86_64 support /// (expected ~2025-2026). + #[cfg_attr(not(feature = "auditwheel"), allow(dead_code))] pub required_archs: HashSet, } diff --git a/src/build_context/repair.rs b/src/build_context/repair.rs index 872210cde..72c50f3ff 100644 --- a/src/build_context/repair.rs +++ b/src/build_context/repair.rs @@ -1,11 +1,13 @@ #[cfg(feature = "auditwheel")] use crate::auditwheel::MacOSRepairer; #[cfg(feature = "auditwheel")] +use crate::auditwheel::ElfRepairer; +#[cfg(feature = "auditwheel")] use crate::auditwheel::WindowsRepairer; #[cfg(feature = "sbom")] use crate::auditwheel::get_sysroot_path; use crate::auditwheel::{ - AuditResult, AuditWheelMode, AuditedArtifact, ElfRepairer, PlatformTag, Policy, WheelRepairer, + AuditResult, AuditWheelMode, AuditedArtifact, PlatformTag, Policy, WheelRepairer, log_grafted_libs, prepare_grafted_libs, }; #[cfg(feature = "sbom")] @@ -23,39 +25,47 @@ use super::BuildContext; impl BuildContext { /// Create the appropriate platform-specific wheel repairer. + #[cfg_attr(not(feature = "auditwheel"), allow(unused_variables))] fn make_repairer( &self, platform_tag: &[PlatformTag], python_interpreter: Option<&PythonInterpreter>, ) -> Option> { if self.project.target.is_linux() { - let mut musllinux: Vec<_> = platform_tag - .iter() - .filter(|tag| tag.is_musllinux()) - .copied() - .collect(); - musllinux.sort(); - let mut others: Vec<_> = platform_tag - .iter() - .filter(|tag| !tag.is_musllinux()) - .copied() - .collect(); - others.sort(); + #[cfg(feature = "auditwheel")] + { + let mut musllinux: Vec<_> = platform_tag + .iter() + .filter(|tag| tag.is_musllinux()) + .copied() + .collect(); + musllinux.sort(); + let mut others: Vec<_> = platform_tag + .iter() + .filter(|tag| !tag.is_musllinux()) + .copied() + .collect(); + others.sort(); - let allow_linking_libpython = self.project.bridge().is_bin(); + let allow_linking_libpython = self.project.bridge().is_bin(); - let effective_tag = if self.project.bridge().is_bin() && !musllinux.is_empty() { - Some(musllinux[0]) - } else { - others.first().or_else(|| musllinux.first()).copied() - }; + let effective_tag = if self.project.bridge().is_bin() && !musllinux.is_empty() { + Some(musllinux[0]) + } else { + others.first().or_else(|| musllinux.first()).copied() + }; - Some(Box::new(ElfRepairer { - platform_tag: effective_tag, - target: self.project.target.clone(), - manifest_path: self.project.manifest_path.clone(), - allow_linking_libpython, - })) + Some(Box::new(ElfRepairer { + platform_tag: effective_tag, + target: self.project.target.clone(), + manifest_path: self.project.manifest_path.clone(), + allow_linking_libpython, + })) + } + #[cfg(not(feature = "auditwheel"))] + { + None + } } else if self.project.target.is_macos() { #[cfg(feature = "auditwheel")] { @@ -157,8 +167,7 @@ impl BuildContext { return Ok(()); } - // Log which libraries need to be copied and which artifacts require them - // before calling patchelf, so users can see this even if patchelf is missing. + // Log which libraries need to be copied and which artifacts require them. eprintln!("🔗 External shared libraries to be copied into the wheel:"); for aa in audited.iter() { if aa.external_libs.is_empty() { diff --git a/src/sbom.rs b/src/sbom.rs index 6d9412a6a..2cce69856 100644 --- a/src/sbom.rs +++ b/src/sbom.rs @@ -1,10 +1,12 @@ use crate::BuildContext; use crate::module_writer::ModuleWriter; -use anyhow::{Context, Result, anyhow}; +use anyhow::{Context, Result}; use std::collections::HashSet; use std::path::{Path, PathBuf}; use tracing::instrument; +#[cfg(feature = "sbom")] +use anyhow::anyhow; #[cfg(feature = "sbom")] use cargo_cyclonedx::config::SbomConfig as CyclonedxConfig; #[cfg(feature = "sbom")]