From 5321f24d10347910d838c4cf91697629d715535f Mon Sep 17 00:00:00 2001 From: konstin Date: Sun, 26 Apr 2026 16:35:41 +0800 Subject: [PATCH] Migrate from patchelf to arwen Use the arwen crate (a Rust port of patchelf) to read/modify ELF rpath, soname and DT_NEEDED entries instead of shelling out to patchelf. Removes the runtime dependency on the patchelf binary. The 'patchelf' optional extra in pyproject.toml is kept (empty) for backwards compatibility but is now deprecated. --- Cargo.toml | 2 +- pyproject.toml | 3 +- src/auditwheel/audit.rs | 5 ++ src/auditwheel/linux.rs | 76 +++++++++++++++++++------- src/auditwheel/mod.rs | 4 +- src/auditwheel/patchelf.rs | 105 ------------------------------------ src/auditwheel/policy.rs | 1 + src/auditwheel/repair.rs | 3 ++ src/build_context/repair.rs | 63 ++++++++++++---------- src/sbom.rs | 4 +- 10 files changed, 110 insertions(+), 156 deletions(-) delete mode 100644 src/auditwheel/patchelf.rs 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")]