#[cfg(feature = "auditwheel")]
use crate::auditwheel::MacOSRepairer;
#[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,
log_grafted_libs, prepare_grafted_libs,
};
#[cfg(feature = "sbom")]
use crate::module_writer::ModuleWriter;
use crate::module_writer::WheelWriter;
use crate::{BridgeModel, BuildArtifact, PythonInterpreter, VirtualWriter};
use anyhow::{Context, Result, bail};
use fs_err as fs;
use normpath::PathExt;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use super::BuildContext;
impl BuildContext {
fn make_repairer(
&self,
platform_tag: &[PlatformTag],
python_interpreter: Option<&PythonInterpreter>,
) -> Option<Box<dyn WheelRepairer>> {
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();
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()
};
Some(Box::new(ElfRepairer {
platform_tag: effective_tag,
target: self.project.target.clone(),
manifest_path: self.project.manifest_path.clone(),
allow_linking_libpython,
}))
} else if self.project.target.is_macos() {
#[cfg(feature = "auditwheel")]
{
Some(Box::new(MacOSRepairer {
target: self.project.target.clone(),
}))
}
#[cfg(not(feature = "auditwheel"))]
{
None
}
} else if self.project.target.is_windows() {
#[cfg(feature = "auditwheel")]
{
let is_pypy = python_interpreter
.map(|p| p.interpreter_kind.is_pypy())
.unwrap_or(false);
Some(Box::new(WindowsRepairer { is_pypy }))
}
#[cfg(not(feature = "auditwheel"))]
{
None
}
} else {
None
}
}
pub(crate) fn auditwheel(
&self,
artifact: &BuildArtifact,
platform_tag: &[PlatformTag],
python_interpreter: Option<&PythonInterpreter>,
) -> Result<AuditResult> {
if matches!(self.python.auditwheel, AuditWheelMode::Skip) {
return Ok(AuditResult::new(Policy::default(), Vec::new()));
}
if let Some(python_interpreter) = python_interpreter
&& platform_tag.is_empty()
&& self.project.target.is_linux()
&& !python_interpreter.support_portable_wheels()
{
eprintln!(
"🐍 Skipping auditwheel because {python_interpreter} does not support manylinux/musllinux wheels"
);
return Ok(AuditResult::new(Policy::default(), Vec::new()));
}
let repairer = match self.make_repairer(platform_tag, python_interpreter) {
Some(r) => r,
None => return Ok(AuditResult::new(Policy::default(), Vec::new())),
};
let ld_paths: Vec<PathBuf> = artifact.linked_paths.iter().map(PathBuf::from).collect();
repairer.audit(artifact, ld_paths)
}
fn get_artifact_dir(&self) -> PathBuf {
match self.project.bridge() {
BridgeModel::Cffi => self.project.module_name.split(".").collect::<PathBuf>(),
_ if self.project.module_name.contains(".") => {
let mut path = self.project.module_name.split(".").collect::<PathBuf>();
path.pop();
path
}
_ => PathBuf::from(&self.project.module_name),
}
}
pub(crate) fn add_external_libs(
&self,
writer: &mut VirtualWriter<WheelWriter>,
audited: &[AuditedArtifact],
) -> Result<()> {
if self.project.editable {
if let Some(repairer) =
self.make_repairer(&self.python.platform_tag, self.python.interpreter.first())
{
return repairer.patch_editable(audited);
}
return Ok(());
}
if audited.iter().all(|a| a.external_libs.is_empty()) {
return Ok(());
}
eprintln!("🔗 External shared libraries to be copied into the wheel:");
for aa in audited {
if aa.external_libs.is_empty() {
continue;
}
eprintln!(" {} requires:", aa.artifact.path.display());
for lib in &aa.external_libs {
if let Some(path) = lib.realpath.as_ref() {
eprintln!(" {} => {}", lib.name, path.display());
} else {
eprintln!(" {} => not found", lib.name);
}
}
}
match self.python.auditwheel {
AuditWheelMode::Warn => {
eprintln!(
"⚠️ Warning: Your library requires copying the above external libraries. \
Re-run with `--auditwheel=repair` to copy them into the wheel."
);
return Ok(());
}
AuditWheelMode::Check => {
bail!(
"Your library requires copying the above external libraries. \
Re-run with `--auditwheel=repair` to copy them."
);
}
_ => {}
}
let repairer = self
.make_repairer(&self.python.platform_tag, self.python.interpreter.first())
.context("No wheel repairer available for this platform")?;
let dist_name = self.project.metadata24.get_distribution_escaped();
let libs_dir = repairer.libs_dir(&dist_name);
let merged_arch_requirements: HashMap<PathBuf, HashSet<String>> = {
let mut merged: HashMap<PathBuf, HashSet<String>> = HashMap::new();
for aa in audited {
for (realpath, archs) in &aa.arch_requirements {
merged
.entry(realpath.clone())
.or_default()
.extend(archs.iter().cloned());
}
}
merged
};
let arch_requirements = if merged_arch_requirements.is_empty() {
None
} else {
Some(&merged_arch_requirements)
};
let temp_dir = writer.temp_dir()?;
let (grafted, libs_copied) =
prepare_grafted_libs(audited, temp_dir.path(), arch_requirements)?;
let artifact_dir = self.get_artifact_dir();
repairer.patch(audited, &grafted, &libs_dir, &artifact_dir)?;
for lib in &grafted {
writer.add_file_force(libs_dir.join(&lib.new_name), &lib.dest_path, true)?;
}
log_grafted_libs(&libs_copied, &libs_dir);
let depth = artifact_dir.components().count();
if !grafted.is_empty() && depth > 0 && !self.project.bridge().is_bin() {
let libs_dir_name = libs_dir.to_string_lossy().into_owned();
if let Some(patch) = repairer.init_py_patch(&libs_dir_name, depth) {
let init_py_path = artifact_dir.join("__init__.py");
writer.prepend_to(init_py_path, patch.into_bytes())?;
}
}
#[cfg(feature = "sbom")]
{
let auditwheel_sbom_enabled = self
.artifact
.sbom
.as_ref()
.and_then(|c| c.auditwheel)
.unwrap_or(true);
if auditwheel_sbom_enabled {
let sysroot =
get_sysroot_path(&self.project.target).unwrap_or_else(|_| PathBuf::from("/"));
let mut grafted_paths: Vec<PathBuf> = libs_copied.into_iter().collect();
grafted_paths.sort();
if let Some(sbom_json) = crate::auditwheel::sbom::create_auditwheel_sbom(
&self.project.metadata24.name,
&self.project.metadata24.version.to_string(),
&grafted_paths,
&sysroot,
) {
let sbom_path = self
.project
.metadata24
.get_dist_info_dir()
.join("sboms/auditwheel.cdx.json");
writer.add_bytes(&sbom_path, None, sbom_json, false)?;
}
}
}
Ok(())
}
pub(crate) fn stage_artifact(&self, artifact: &mut BuildArtifact) -> Result<()> {
let maturin_build = crate::compile::ensure_target_maturin_dir(&self.project.target_dir);
let artifact_path = &artifact.path;
let new_artifact_path = maturin_build.join(artifact_path.file_name().unwrap());
let _ = fs::remove_file(&new_artifact_path);
if fs::rename(artifact_path, &new_artifact_path).is_ok() {
if artifact_path.exists() {
tracing::debug!(
"Skipping copy-back: {} was recreated by another process",
artifact_path.display()
);
} else if let Err(err) = reflink_or_copy(&new_artifact_path, artifact_path) {
eprintln!(
"⚠️ Warning: failed to copy artifact back to {}: {err:#}. The staged artifact is available at {}",
artifact_path.display(),
new_artifact_path.display()
);
}
} else {
reflink_or_copy(artifact_path, &new_artifact_path)?;
}
artifact.path = new_artifact_path.normalize()?.into_path_buf();
Ok(())
}
}
fn reflink_or_copy(from: &Path, to: &Path) -> Result<()> {
if reflink_with_permissions(from, to).is_err() {
fs::copy(from, to)?;
}
Ok(())
}
#[cfg(target_os = "linux")]
fn reflink_with_permissions(from: &Path, to: &Path) -> std::io::Result<()> {
reflink_copy::reflink(from, to)?;
let perms = fs::metadata(from)?.permissions();
fs::set_permissions(to, perms)?;
Ok(())
}
#[cfg(not(target_os = "linux"))]
fn reflink_with_permissions(from: &Path, to: &Path) -> std::io::Result<()> {
reflink_copy::reflink(from, to)
}