use crate::error::Result;
use std::os::unix::fs::symlink;
use std::path::{Path, PathBuf};
use tracing::{debug, warn};
const LINK_DIRS: &[&str] = &[
"bin", "sbin", "lib", "include", "share", "etc", "var", "opt",
];
use crate::extract::create_dir_all_force;
pub fn link_package(
install_path: impl AsRef<Path>,
prefix: impl AsRef<Path>,
) -> Result<Vec<PathBuf>> {
let install_path = install_path.as_ref();
let prefix = prefix.as_ref();
let mut linked = Vec::new();
for dir in LINK_DIRS {
let source_dir = install_path.join(dir);
if !source_dir.exists() {
continue;
}
let target_dir = prefix.join(dir);
create_dir_all_force(&target_dir)?;
for entry in walkdir(&source_dir)? {
let entry = entry?;
let relative = entry
.strip_prefix(&source_dir)
.expect("entry should be under source_dir");
let target = target_dir.join(relative);
if let Some(parent) = target.parent() {
create_dir_all_force(parent)?;
}
let target_exists = target.exists() || target.symlink_metadata().is_ok();
if target_exists {
if let Ok(link_target) = std::fs::read_link(&target) {
let resolved = target
.parent()
.expect("target should have a parent directory")
.join(&link_target);
let matches = if let (Ok(resolved_canon), Ok(entry_canon)) =
(resolved.canonicalize(), entry.canonicalize())
{
resolved_canon == entry_canon
} else {
link_target == entry || resolved == entry
};
if matches {
continue; }
let pkg_cellar = install_path.parent();
let is_same_package = pkg_cellar.is_some_and(|cellar| {
let canon = resolved.canonicalize().ok();
canon.is_some_and(|r| r.starts_with(cellar))
});
if is_same_package {
debug!(
"Replacing stale same-package link: {} -> {}",
target.display(),
link_target.display()
);
std::fs::remove_file(&target)?;
} else {
warn!(
"Skipping {}: symlink points to {} instead of {}",
target.display(),
link_target.display(),
entry.display()
);
continue;
}
}
if !target.exists() && target.symlink_metadata().is_err() {
} else if target.is_file() && entry.is_file() {
if files_match(&target, &entry) {
debug!(
"Skipping {}: regular file with matching content",
target.display()
);
continue;
}
warn!(
"Skipping {}: regular file exists with different content",
target.display()
);
continue;
} else {
warn!(
"Skipping {}: already exists (not a symlink)",
target.display()
);
continue;
}
}
let relative_source = pathdiff::diff_paths(
&entry,
target
.parent()
.expect("target should have a parent directory"),
)
.unwrap_or_else(|| entry.to_path_buf());
debug!(
"Linking {} -> {}",
target.display(),
relative_source.display()
);
symlink(&relative_source, &target).map_err(|e| {
crate::error::Error::LinkFailed(format!(
"{} -> {}: {}",
target.display(),
relative_source.display(),
e
))
})?;
linked.push(target);
}
}
let opt_dir = prefix.join("opt");
create_dir_all_force(&opt_dir)?;
if let Some(name) = install_path.parent().and_then(|p| p.file_name()) {
let opt_link = opt_dir.join(name);
let relative = pathdiff::diff_paths(install_path, &opt_dir)
.unwrap_or_else(|| install_path.to_path_buf());
if opt_link.symlink_metadata().is_ok() {
if let Ok(link_target) = std::fs::read_link(&opt_link) {
if link_target == relative {
linked.push(opt_link);
} else {
debug!("Removing stale opt link: {}", opt_link.display());
std::fs::remove_file(&opt_link)?;
symlink(&relative, &opt_link)?;
linked.push(opt_link);
}
} else {
debug!("Replacing opt link: {}", opt_link.display());
std::fs::remove_file(&opt_link)?;
symlink(&relative, &opt_link)?;
linked.push(opt_link);
}
} else {
symlink(&relative, &opt_link)?;
linked.push(opt_link);
}
}
Ok(linked)
}
pub fn unlink_package(
install_path: impl AsRef<Path>,
prefix: impl AsRef<Path>,
) -> Result<Vec<PathBuf>> {
let install_path = install_path.as_ref();
let prefix = prefix.as_ref();
let mut unlinked = Vec::new();
for dir in LINK_DIRS {
let source_dir = install_path.join(dir);
if !source_dir.exists() {
continue;
}
let target_dir = prefix.join(dir);
if !target_dir.exists() {
continue;
}
for entry in walkdir(&source_dir)? {
let entry = entry?;
let relative = entry
.strip_prefix(&source_dir)
.expect("entry should be under source_dir");
let target = target_dir.join(relative);
if let Ok(link_target) = std::fs::read_link(&target) {
let resolved = target
.parent()
.expect("target should have a parent directory")
.join(&link_target);
if resolved.canonicalize().ok() == entry.canonicalize().ok() {
debug!("Unlinking {}", target.display());
std::fs::remove_file(&target)?;
unlinked.push(target);
}
}
}
}
let opt_dir = prefix.join("opt");
if let Some(name) = install_path.parent().and_then(|p| p.file_name()) {
let opt_link = opt_dir.join(name);
if opt_link.symlink_metadata().is_ok() {
std::fs::remove_file(&opt_link)?;
unlinked.push(opt_link);
}
}
Ok(unlinked)
}
fn walkdir(dir: &Path) -> Result<impl Iterator<Item = Result<PathBuf>>> {
fn walk_recursive(dir: &Path, results: &mut Vec<PathBuf>) -> std::io::Result<()> {
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
let ft = path.symlink_metadata()?.file_type();
if ft.is_dir() {
walk_recursive(&path, results)?;
} else if !ft.is_symlink() || !path.is_dir() {
results.push(path);
}
}
Ok(())
}
let mut results = Vec::new();
walk_recursive(dir, &mut results)?;
Ok(results.into_iter().map(Ok))
}
fn files_match(a: &Path, b: &Path) -> bool {
let meta_a = match std::fs::metadata(a) {
Ok(m) => m,
Err(_) => return false,
};
let meta_b = match std::fs::metadata(b) {
Ok(m) => m,
Err(_) => return false,
};
if meta_a.len() != meta_b.len() {
return false;
}
let Ok(mut file_a) = std::fs::File::open(a) else {
return false;
};
let Ok(mut file_b) = std::fs::File::open(b) else {
return false;
};
use std::io::Read;
let mut buf_a = [0u8; 8192];
let mut buf_b = [0u8; 8192];
loop {
let read_a = match file_a.read(&mut buf_a) {
Ok(0) => break,
Ok(n) => n,
Err(_) => return false,
};
let read_b = match file_b.read(&mut buf_b) {
Ok(n) if n == read_a => n,
_ => return false,
};
if buf_a[..read_a] != buf_b[..read_b] {
return false;
}
}
true
}
mod pathdiff {
use std::path::{Path, PathBuf};
pub fn diff_paths(path: &Path, base: &Path) -> Option<PathBuf> {
let path = path.canonicalize().ok()?;
let base = base.canonicalize().ok()?;
let mut path_iter = path.components().peekable();
let mut base_iter = base.components().peekable();
while path_iter.peek() == base_iter.peek() {
path_iter.next();
if base_iter.next().is_none() {
break;
}
}
let mut result = PathBuf::new();
for _ in base_iter {
result.push("..");
}
for component in path_iter {
result.push(component);
}
Some(result)
}
}