use crate::compile::BuildArtifact;
use crate::util::hash_file;
use anyhow::{Context, Result};
use std::borrow::Borrow;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use fs_err as fs;
pub struct AuditedArtifact {
pub artifact: BuildArtifact,
pub external_libs: Vec<lddtree::Library>,
pub arch_requirements: HashMap<PathBuf, HashSet<String>>,
}
impl Borrow<BuildArtifact> for AuditedArtifact {
fn borrow(&self) -> &BuildArtifact {
&self.artifact
}
}
pub struct GraftedLib {
pub original_name: String,
pub aliases: Vec<String>,
pub new_name: String,
pub dest_path: PathBuf,
pub needed: Vec<String>,
pub rpath: Vec<String>,
pub required_archs: HashSet<String>,
}
pub struct AuditResult {
pub policy: super::Policy,
pub external_libs: Vec<lddtree::Library>,
pub arch_requirements: HashMap<PathBuf, HashSet<String>>,
}
impl AuditResult {
pub fn new(policy: super::Policy, external_libs: Vec<lddtree::Library>) -> Self {
Self {
policy,
external_libs,
arch_requirements: HashMap::new(),
}
}
}
pub trait WheelRepairer {
fn audit(&self, artifact: &BuildArtifact, ld_paths: Vec<PathBuf>) -> Result<AuditResult>;
fn patch(
&self,
audited: &[AuditedArtifact],
grafted: &[GraftedLib],
libs_dir: &Path,
artifact_dir: &Path,
) -> Result<()>;
fn patch_editable(&self, _audited: &[AuditedArtifact]) -> Result<()> {
Ok(())
}
fn init_py_patch(&self, _libs_dir_name: &str, _depth: usize) -> Option<String> {
None
}
fn libs_dir(&self, dist_name: &str) -> PathBuf {
PathBuf::from(format!("{dist_name}.libs"))
}
}
pub fn prepare_grafted_libs(
audited: &[AuditedArtifact],
temp_dir: &Path,
arch_requirements: Option<&HashMap<PathBuf, HashSet<String>>>,
) -> Result<(Vec<GraftedLib>, HashSet<PathBuf>)> {
let mut grafted = Vec::new();
let mut libs_copied = HashSet::new();
let mut realpath_to_idx: HashMap<PathBuf, usize> = HashMap::new();
for lib in audited.iter().flat_map(|a| &a.external_libs) {
let source_path = lib.realpath.clone().with_context(|| {
format!(
"Cannot repair wheel, because required library {} could not be located.",
lib.path.display()
)
})?;
if let Some(&idx) = realpath_to_idx.get(&source_path) {
let existing: &mut GraftedLib = &mut grafted[idx];
if lib.name != existing.original_name && !existing.aliases.contains(&lib.name) {
existing.aliases.push(lib.name.clone());
}
continue;
}
let new_name = hashed_lib_name(&lib.name, &source_path)?;
let dest_path = temp_dir.join(&new_name);
fs::copy(&source_path, &dest_path)?;
let mut perms = fs::metadata(&dest_path)?.permissions();
#[allow(clippy::permissions_set_readonly_false)]
perms.set_readonly(false);
fs::set_permissions(&dest_path, perms)?;
let idx = grafted.len();
realpath_to_idx.insert(source_path.clone(), idx);
libs_copied.insert(source_path.clone());
let required_archs = arch_requirements
.and_then(|reqs| reqs.get(&source_path))
.cloned()
.unwrap_or_default();
grafted.push(GraftedLib {
original_name: lib.name.clone(),
aliases: Vec::new(),
new_name,
dest_path,
needed: lib.needed.clone(),
rpath: lib.rpath.clone(),
required_archs,
});
}
Ok((grafted, libs_copied))
}
pub(crate) fn leaf_filename(lib_name: &str) -> &str {
Path::new(lib_name)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(lib_name)
}
pub(crate) fn hashed_lib_name(lib_name: &str, lib_path: &Path) -> Result<String> {
let short_hash = &hash_file(lib_path)
.with_context(|| format!("Failed to hash library {}", lib_path.display()))?[..8];
let leaf = leaf_filename(lib_name);
Ok(if let Some(pos) = leaf.find('.') {
let (stem, ext) = leaf.split_at(pos);
if stem.ends_with(&format!("-{short_hash}")) {
leaf.to_string()
} else {
format!("{stem}-{short_hash}{ext}")
}
} else {
format!("{leaf}-{short_hash}")
})
}
pub fn log_grafted_libs(libs_copied: &HashSet<PathBuf>, libs_dir: &Path) {
let mut grafted_paths: Vec<&PathBuf> = libs_copied.iter().collect();
grafted_paths.sort();
eprintln!(
"🖨 Copied external shared libraries to package {} directory:",
libs_dir.display()
);
for lib_path in &grafted_paths {
eprintln!(" {}", lib_path.display());
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn test_leaf_filename() {
assert_eq!(leaf_filename("libfoo.so.1"), "libfoo.so.1");
assert_eq!(leaf_filename("/usr/local/lib/libfoo.dylib"), "libfoo.dylib");
assert_eq!(leaf_filename("@rpath/libfoo.dylib"), "libfoo.dylib");
}
#[test]
fn test_hashed_lib_name() {
let tmp_dir = tempfile::tempdir().unwrap();
let lib_path = tmp_dir.path().join("libfoo.so.1");
{
let mut f = fs_err::File::create(&lib_path).unwrap();
f.write_all(b"fake library content").unwrap();
}
let name = hashed_lib_name("libfoo.so.1", &lib_path).unwrap();
assert!(name.starts_with("libfoo-"));
assert!(name.ends_with(".so.1"));
assert_eq!(name.len(), "libfoo-".len() + 8 + ".so.1".len());
let name2 = hashed_lib_name(&name, &lib_path).unwrap();
assert_eq!(name, name2);
}
#[test]
fn test_hashed_lib_name_macos_path() {
let tmp_dir = tempfile::tempdir().unwrap();
let lib_path = tmp_dir.path().join("libbar.dylib");
{
let mut f = fs_err::File::create(&lib_path).unwrap();
f.write_all(b"fake dylib content").unwrap();
}
let name = hashed_lib_name("/usr/local/lib/libbar.dylib", &lib_path).unwrap();
assert!(name.starts_with("libbar-"));
assert!(name.ends_with(".dylib"));
}
}