use std::collections::HashSet;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use eyre::{WrapErr, bail};
use serde_json::json;
use super::api::BottleFile;
use super::prefix;
use super::relocate;
use super::resolve::ResolvedFormula;
use crate::file::{ExtractOptions, ExtractionFormat};
use crate::result::Result;
use crate::ui::progress_report::SingleReport;
pub(super) const LINK_DIRS: &[&str] = &["bin", "sbin", "include", "lib", "share", "Frameworks"];
const KEG_ONLY_MARKER: &str = ".mise-keg-only";
struct RecordRepair {
version: String,
keg: PathBuf,
destination: PathBuf,
}
pub(super) fn keg_path(name: &str, pkg_version: &str) -> PathBuf {
prefix::cellar().join(name).join(pkg_version)
}
pub(super) fn keg_installed(name: &str, pkg_version: &str) -> bool {
keg_path(name, pkg_version).exists() && linked_version(name).as_deref() == Some(pkg_version)
}
pub(super) fn linked_version(name: &str) -> Option<String> {
let opt = prefix::prefix().join("opt").join(name);
record_keg(name, &opt).map(|(version, _)| version)
}
pub(super) fn strict_package_root(name: &str) -> Result<PathBuf> {
if let Some(prefix) = std::env::var_os("MISE_SYSTEM_BREW_PREFIX")
&& prefix.to_str().is_none()
{
bail!("the prefix cannot be printed as a single UTF-8 path; use a compatible prefix path");
}
let prefix = prefix::prefix();
let prefix = if prefix.is_absolute() {
prefix
} else {
std::env::current_dir()
.wrap_err_with(|| {
format!(
"cannot resolve relative Homebrew prefix {}",
prefix.display()
)
})?
.join(prefix)
};
let opt = prefix.join("opt").join(name);
let rack = prefix.join("Cellar").join(name);
let metadata =
std::fs::symlink_metadata(&opt).map_err(|err| package_root_io_error(name, &opt, err))?;
if !metadata.is_symlink() {
bail!(
"invalid opt record for brew:{name} at {}: expected a symbolic link; inspect it and restore the formula's correct opt link",
opt.display()
);
}
let target =
std::fs::canonicalize(&opt).map_err(|err| package_root_io_error(name, &opt, err))?;
let canonical_rack =
std::fs::canonicalize(&rack).map_err(|err| package_root_io_error(name, &rack, err))?;
let metadata =
std::fs::metadata(&target).map_err(|err| package_root_io_error(name, &target, err))?;
if !metadata.is_dir() || target.parent() != Some(canonical_rack.as_path()) {
bail!(
"invalid opt record for brew:{name} at {}: target {} must be a directory directly inside {}; inspect it and restore the formula's correct opt link",
opt.display(),
target.display(),
canonical_rack.display()
);
}
Ok(opt)
}
fn package_root_io_error(name: &str, path: &Path, err: std::io::Error) -> eyre::Report {
let context = if err.kind() == std::io::ErrorKind::NotFound {
format!(
"brew:{name} has no usable installed opt link: {}; install or reconcile it with `mise bootstrap packages apply brew:{name}`",
path.display()
)
} else {
format!(
"cannot inspect brew:{name} at {}; check access or the reported filesystem condition",
path.display()
)
};
eyre::Report::new(err).wrap_err(context)
}
pub(super) fn linked_state(name: &str) -> Option<(String, bool)> {
let opt = prefix::prefix().join("opt").join(name);
let active = record_keg(name, &opt).or_else(|| {
record_needs_replacement(name, &opt)
.then(|| record_keg(name, &prefix::linked_keg_record(name)))?
})?;
Some((active.0, pending_record_repair(name).is_some()))
}
pub(super) fn repair_link_record(name: &str, dry_run: bool) -> Result<bool> {
let Some(repair) = pending_record_repair(name) else {
return Ok(false);
};
let record = if repair.destination == prefix::linked_keg_record(name) {
"linked-keg record"
} else {
"opt record"
};
if dry_run {
miseprintln!("repair {name}/{}: {record}", repair.version);
return Ok(true);
}
crate::file::create_dir_all(repair.destination.parent().unwrap())?;
crate::file::make_symlink(
&relative_target(&repair.keg, &repair.destination),
&repair.destination,
)
.wrap_err_with(|| {
format!(
"failed to repair Homebrew {record}: {}",
repair.destination.display()
)
})?;
Ok(true)
}
fn pending_record_repair(name: &str) -> Option<RecordRepair> {
let opt = prefix::prefix().join("opt").join(name);
let linked = prefix::linked_keg_record(name);
if let Some((version, keg)) = record_keg(name, &opt) {
if keg.join(KEG_ONLY_MARKER).is_file() {
return None;
}
if record_needs_replacement(name, &linked) && has_public_link_into(&keg) {
return Some(RecordRepair {
version,
keg,
destination: linked,
});
}
return None;
}
if record_needs_replacement(name, &opt)
&& let Some((version, keg)) = record_keg(name, &linked)
{
return Some(RecordRepair {
version,
keg,
destination: opt,
});
}
None
}
fn record_keg(name: &str, record: &Path) -> Option<(String, PathBuf)> {
let target = record_target(name, record)?.canonicalize().ok()?;
let rack = prefix::cellar().join(name).canonicalize().ok()?;
if target.parent()? != rack || !target.is_dir() {
return None;
}
let version = target.file_name()?.to_string_lossy().to_string();
Some((version.clone(), keg_path(name, &version)))
}
fn record_target(name: &str, record: &Path) -> Option<PathBuf> {
let target = resolved_symlink_target(record)?;
let rack = prefix::cellar().join(name).canonicalize().ok()?;
(target.parent()? == rack).then_some(target)
}
fn record_needs_replacement(name: &str, path: &Path) -> bool {
match path.symlink_metadata() {
Err(err) if err.kind() == std::io::ErrorKind::NotFound => true,
Ok(metadata) if metadata.file_type().is_symlink() => {
record_target(name, path).is_some() && record_keg(name, path).is_none()
}
Err(_) | Ok(_) => false,
}
}
fn has_public_link_into(keg: &Path) -> bool {
LINK_DIRS.iter().any(|dir| {
let root = keg.join(dir);
root.exists()
&& walkdir::WalkDir::new(root)
.follow_links(false)
.into_iter()
.filter_map(|entry| entry.ok())
.any(|entry| {
entry
.path()
.strip_prefix(keg)
.ok()
.map(|relative| prefix::prefix().join(relative))
.is_some_and(|link| symlink_points_to(&link, entry.path()))
})
})
}
fn symlink_points_to(link: &Path, target: &Path) -> bool {
resolved_symlink_target(link).as_ref() == Some(&resolved_path(target))
}
pub(super) fn installed_versions(name: &str) -> Vec<String> {
let dir = prefix::cellar().join(name);
let mut versions: Vec<String> = crate::file::ls(&dir)
.unwrap_or_default()
.into_iter()
.filter(|p| p.is_dir())
.filter_map(|p| {
let name = p.file_name()?.to_string_lossy().to_string();
(!name.starts_with(".mise-")).then_some(name)
})
.collect();
versions.sort();
let opt_target = std::fs::read_link(prefix::prefix().join("opt").join(name))
.ok()
.and_then(|t| t.file_name().map(|f| f.to_string_lossy().to_string()));
if let Some(active) = opt_target
&& let Some(pos) = versions.iter().position(|v| v == &active)
{
versions.swap(0, pos);
}
versions
}
pub(super) struct PreparedBottle {
name: String,
pkg_version: String,
keg: PathBuf,
staged_keg: PathBuf,
_staging_lock: fslock::LockFile,
staging: tempfile::TempDir,
keg_only: bool,
}
fn create_staging_dir(
rack: &Path,
pkg_version: &str,
) -> Result<(tempfile::TempDir, fslock::LockFile)> {
crate::file::create_dir_all(rack)?;
let _rack_lock = crate::lock_file::LockFile::at(&rack.join(".mise-staging.lock")).lock()?;
remove_abandoned_staging_dirs(rack)?;
let staging = tempfile::Builder::new()
.prefix(&format!(".mise-extract-{pkg_version}-"))
.tempdir_in(rack)?;
let staging_lock = crate::lock_file::LockFile::at(&staging.path().join(".mise-lock")).lock()?;
Ok((staging, staging_lock))
}
fn remove_abandoned_staging_dirs(rack: &Path) -> Result<()> {
for entry in rack.read_dir()? {
let entry = entry?;
let path = entry.path();
let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
continue;
};
if !name.starts_with(".mise-extract-") && !name.starts_with(".mise-tmp-") {
continue;
}
let metadata = match path.symlink_metadata() {
Ok(metadata) => metadata,
Err(err) if err.kind() == ErrorKind::NotFound => continue,
Err(err) => return Err(err.into()),
};
if !metadata.is_dir() {
continue;
}
let lock_path = path.join(".mise-lock");
let lock_metadata = match lock_path.symlink_metadata() {
Ok(metadata) => metadata,
Err(err) if err.kind() == ErrorKind::NotFound => continue,
Err(err) => return Err(err.into()),
};
if !lock_metadata.is_file() {
continue;
}
let mut lock = match fslock::LockFile::open(&lock_path) {
Ok(lock) => lock,
Err(err) if err.kind() == ErrorKind::NotFound => continue,
Err(err) => return Err(err.into()),
};
if !lock.try_lock()? {
continue;
}
drop(lock);
if let Err(err) = crate::file::remove_all(&path) {
let disappeared = err.chain().any(|cause| {
cause
.downcast_ref::<std::io::Error>()
.is_some_and(|err| err.kind() == ErrorKind::NotFound)
});
if !disappeared {
return Err(err);
}
}
}
Ok(())
}
fn commit_lock_path() -> PathBuf {
prefix::prefix()
.join("var/homebrew/locks")
.join("mise-bootstrap.lock")
}
pub(super) fn commit_lock(pr: &dyn SingleReport) -> Result<fslock::LockFile> {
crate::lock_file::LockFile::at(&commit_lock_path()).lock_with_notice(&|| {
pr.set_message("waiting for another brew install".to_string());
})
}
pub(super) fn prepare_bottle(
rf: &ResolvedFormula,
tag: &str,
bottle: &BottleFile,
tarball: &Path,
closure: &[ResolvedFormula],
pr: &dyn SingleReport,
) -> Result<PreparedBottle> {
let name = &rf.formula.name;
let pkg_version = rf.formula.pkg_version()?;
let keg = keg_path(name, &pkg_version);
let rack = keg.parent().unwrap().to_path_buf();
let (staging, staging_lock) = create_staging_dir(&rack, &pkg_version)?;
let staged_keg = staging.path().join(name).join(&pkg_version);
let prepared = PreparedBottle {
name: name.clone(),
pkg_version,
keg,
staged_keg,
_staging_lock: staging_lock,
staging,
keg_only: rf.formula.keg_only_for_target(),
};
pr.set_message("extract".to_string());
crate::file::untar(
tarball,
prepared.staging.path(),
ExtractionFormat::TarGz,
&ExtractOptions {
strip_components: 0,
pr: Some(pr),
preserve_mtime: true,
},
)
.wrap_err_with(|| format!("failed to extract bottle for {name}"))?;
if !prepared.staged_keg.exists() {
bail!(
"unexpected bottle layout for {name}: missing {name}/{} in archive",
prepared.pkg_version
);
}
let skip_linkage = bottle.cellar == ":any_skip_relocation"
&& (cfg!(target_os = "macos")
|| bottled_by_homebrew_at_least(&prepared.staged_keg, (5, 1, 15)));
pr.set_message("relocate".to_string());
let report = relocate::relocate_keg(&prepared.staged_keg, name, skip_linkage)?;
if cfg!(target_os = "macos") && !report.changed_machos.is_empty() {
pr.set_message("codesign".to_string());
relocate::codesign(&report.changed_machos)
.wrap_err_with(|| format!("failed to re-sign relocated binaries for {name}"))?;
}
write_receipt(rf, tag, &prepared.staged_keg, &report, closure, true)?;
Ok(prepared)
}
pub(super) fn install_prepared(prepared: PreparedBottle, pr: &dyn SingleReport) -> Result<()> {
pr.set_message("link".to_string());
let _commit_lock = commit_lock(pr)?;
if keg_installed(&prepared.name, &prepared.pkg_version) {
return Ok(());
}
if prepared.keg.exists() {
crate::file::remove_all(&prepared.keg)?;
}
crate::file::rename(&prepared.staged_keg, &prepared.keg)?;
if let Err(err) = link_keg(&prepared.name, &prepared.pkg_version, prepared.keg_only) {
if let Err(rm_err) = crate::file::remove_all(&prepared.keg) {
warn!(
"failed to remove {} after link failure: {rm_err}\n\
remove it manually, then re-run `mise bootstrap packages apply`",
prepared.keg.display()
);
}
return Err(err);
}
Ok(())
}
fn bottled_by_homebrew_at_least(keg: &Path, min: (u64, u64, u64)) -> bool {
let Ok(receipt) = crate::file::read_to_string(keg.join("INSTALL_RECEIPT.json")) else {
return false;
};
let Ok(json) = serde_json::from_str::<serde_json::Value>(&receipt) else {
return false;
};
let Some(version) = json.get("homebrew_version").and_then(|v| v.as_str()) else {
return false;
};
let mut parts = version
.split(['.', '-', ' '])
.map(|p| p.parse::<u64>().unwrap_or(0));
let v = (
parts.next().unwrap_or(0),
parts.next().unwrap_or(0),
parts.next().unwrap_or(0),
);
v >= min
}
pub(super) fn write_receipt(
rf: &ResolvedFormula,
tag: &str,
keg: &Path,
report: &relocate::RelocationReport,
closure: &[ResolvedFormula],
poured_from_bottle: bool,
) -> Result<()> {
let by_name = super::resolve::formulae_by_name(closure);
let declared: HashSet<&str> = rf
.formula
.dependencies_for(tag)
.iter()
.filter_map(|d| by_name.get(d.as_str()))
.map(|dep| dep.formula.name.as_str())
.collect();
let runtime_dependencies: Vec<serde_json::Value> = closure
.iter()
.filter(|other| declared.contains(other.formula.name.as_str()))
.filter_map(|dep| {
let pkg_version = dep.formula.pkg_version().ok()?;
Some(json!({
"full_name": dep.formula.name,
"version": dep.formula.versions.stable,
"revision": dep.formula.revision,
"pkg_version": pkg_version,
"declared_directly": true,
}))
})
.collect();
let changed_files: Vec<String> = report
.changed_files
.iter()
.filter_map(|p| p.strip_prefix(keg).ok())
.map(|p| p.to_string_lossy().to_string())
.collect();
let receipt = json!({
"homebrew_version": "5.1.15 (mise)",
"used_options": [],
"unused_options": [],
"built_as_bottle": poured_from_bottle,
"poured_from_bottle": poured_from_bottle,
"loaded_from_api": true,
"installed_as_dependency": !rf.on_request,
"installed_on_request": rf.on_request,
"changed_files": changed_files,
"time": std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0),
"source_modified_time": 0,
"compiler": "clang",
"aliases": rf.formula.aliases,
"runtime_dependencies": runtime_dependencies,
"source": {
"spec": "stable",
"versions": {
"stable": rf.formula.versions.stable,
"head": null,
"version_scheme": 0,
},
"path": null,
"tap": rf.formula.tap.as_deref().unwrap_or("homebrew/core"),
"tap_git_head": null,
},
"arch": if cfg!(target_arch = "aarch64") { "arm64" } else { "x86_64" },
"built_on": {},
});
crate::file::write(
keg.join("INSTALL_RECEIPT.json"),
serde_json::to_string(&receipt)?,
)?;
Ok(())
}
fn relative_target(dest: &Path, link: &Path) -> PathBuf {
let link_dir = link.parent().unwrap();
let mut common = 0;
let dest_parts: Vec<_> = dest.components().collect();
let link_parts: Vec<_> = link_dir.components().collect();
while common < dest_parts.len()
&& common < link_parts.len()
&& dest_parts[common] == link_parts[common]
{
common += 1;
}
let mut out = PathBuf::new();
for _ in common..link_parts.len() {
out.push("..");
}
for part in &dest_parts[common..] {
out.push(part);
}
out
}
fn can_overwrite(dest: &Path) -> bool {
let Ok(meta) = dest.symlink_metadata() else {
return true; };
if brew_owned_ancestor(dest).is_some() {
return true;
}
if !meta.is_symlink() {
return false;
}
points_into_cellar(dest)
}
fn points_into_cellar(link: &Path) -> bool {
let Some(target) = resolved_symlink_target(link) else {
return false;
};
let cellar = prefix::cellar()
.canonicalize()
.unwrap_or_else(|_| prefix::cellar());
let opt = prefix::prefix()
.join("opt")
.canonicalize()
.unwrap_or_else(|_| prefix::prefix().join("opt"));
target.starts_with(cellar) || target.starts_with(opt)
}
fn resolved_symlink_target(link: &Path) -> Option<PathBuf> {
let target = std::fs::read_link(link).ok()?;
let target = if target.is_absolute() {
target
} else {
link.parent()?.join(target)
};
Some(resolved_path(&target))
}
fn resolved_path(path: &Path) -> PathBuf {
let target = lexical_normalize(path);
match (target.parent(), target.file_name()) {
(Some(parent), Some(name)) => parent
.canonicalize()
.unwrap_or_else(|_| parent.to_path_buf())
.join(name),
_ => target,
}
}
pub(super) fn lexical_normalize(path: &Path) -> PathBuf {
let mut out = PathBuf::new();
for part in path.components() {
match part {
std::path::Component::CurDir => {}
std::path::Component::ParentDir => {
out.pop();
}
other => out.push(other.as_os_str()),
}
}
out
}
fn brew_owned_ancestor(dest: &Path) -> Option<PathBuf> {
let prefix_path = prefix::prefix();
let mut ancestors: Vec<&Path> = dest
.ancestors()
.skip(1)
.take_while(|p| *p != prefix_path && p.starts_with(&prefix_path))
.collect();
ancestors.reverse(); for anc in ancestors {
if anc
.symlink_metadata()
.map(|m| m.is_symlink())
.unwrap_or(false)
{
return points_into_cellar(anc).then(|| anc.to_path_buf());
}
}
None
}
fn materialize_brew_dirs(dest: &Path) -> Result<()> {
while let Some(link_dir) = brew_owned_ancestor(dest) {
let raw_target = std::fs::read_link(&link_dir)?;
let staging = link_dir.parent().unwrap().join(format!(
".mise-materialize-{}",
link_dir.file_name().unwrap().to_string_lossy()
));
let staged = (|| -> Result<()> {
if staging.exists() {
crate::file::remove_all(&staging)?;
}
crate::file::create_dir_all(&staging)?;
let target = lexical_normalize(&link_dir.parent().unwrap().join(&raw_target));
if target.is_dir() {
for entry in std::fs::read_dir(&target)? {
let entry = entry?;
let child_link = link_dir.join(entry.file_name());
crate::file::make_symlink(
&relative_target(&entry.path(), &child_link),
&staging.join(entry.file_name()),
)?;
}
}
Ok(())
})();
if let Err(err) = staged {
let _ = crate::file::remove_all(&staging);
return Err(err);
}
if let Err(err) = crate::file::remove_file(&link_dir) {
let _ = crate::file::remove_all(&staging);
return Err(err);
}
if let Err(err) = crate::file::rename(&staging, &link_dir) {
let _ = crate::file::make_symlink(&raw_target, &link_dir);
let _ = crate::file::remove_all(&staging);
return Err(err);
}
}
Ok(())
}
pub(super) fn link_keg(name: &str, pkg_version: &str, keg_only: bool) -> Result<()> {
let prefix_path = prefix::prefix();
let keg = keg_path(name, pkg_version);
if keg_only {
crate::file::write(keg.join(KEG_ONLY_MARKER), "")?;
}
let opt_link = prefix_path.join("opt").join(name);
let mut conflicts: Vec<PathBuf> = vec![];
let mut links: Vec<(PathBuf, PathBuf)> = vec![];
if keg_only {
debug!(
"{name} is keg-only, not linking into {}",
prefix_path.display()
);
} else {
for dir in LINK_DIRS {
let src_root = keg.join(dir);
if !src_root.exists() {
continue;
}
for entry in walkdir::WalkDir::new(&src_root).follow_links(false) {
let entry = entry?;
if entry.file_type().is_dir() {
continue;
}
let rel = entry.path().strip_prefix(&keg)?;
let dest = prefix_path.join(rel);
if !can_overwrite(&dest) {
conflicts.push(dest);
} else {
links.push((dest, entry.path().to_path_buf()));
}
}
}
let linked = prefix::linked_keg_record(name);
if can_overwrite(&linked) {
links.push((linked, keg.clone()));
} else {
conflicts.push(linked);
}
}
if can_overwrite(&opt_link) {
links.push((opt_link.clone(), keg.clone()));
} else {
conflicts.push(opt_link);
}
if !conflicts.is_empty() {
bail!(
"cannot link {name}: these files already exist and were not created by mise or brew:\n{}\n\
Remove or rename them, then re-run `mise bootstrap packages apply`",
conflicts
.iter()
.map(|p| format!(" {}", p.display()))
.collect::<Vec<_>>()
.join("\n"),
);
}
let mut created: Vec<PathBuf> = vec![];
let mut replaced: Vec<(PathBuf, PathBuf)> = vec![];
let mut failure: Option<eyre::Report> = None;
for (dest, target) in &links {
let made = (|| -> Result<()> {
materialize_brew_dirs(dest)?;
crate::file::create_dir_all(dest.parent().unwrap())?;
if dest.symlink_metadata().is_ok() {
if let Ok(prev) = std::fs::read_link(dest) {
replaced.push((dest.clone(), prev));
}
crate::file::remove_file(dest)?;
}
crate::file::make_symlink(&relative_target(target, dest), dest)?;
Ok(())
})();
if let Err(err) = made {
failure = Some(err);
break;
}
created.push(dest.clone());
}
if let Some(err) = failure {
for dest in created {
let _ = crate::file::remove_file(&dest);
}
for (dest, prev) in replaced {
let _ = crate::file::make_symlink(&prev, &dest);
}
return Err(err);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::system::packages::brew::package_root;
use std::os::unix::fs::{PermissionsExt, symlink};
use tokio::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::const_new(());
struct BrewPrefixGuard {
previous: Option<String>,
}
impl BrewPrefixGuard {
fn set(prefix: &Path) -> Self {
let previous = crate::env::var("MISE_SYSTEM_BREW_PREFIX").ok();
crate::env::set_var("MISE_SYSTEM_BREW_PREFIX", prefix);
Self { previous }
}
}
impl Drop for BrewPrefixGuard {
fn drop(&mut self) {
match &self.previous {
Some(previous) => crate::env::set_var("MISE_SYSTEM_BREW_PREFIX", previous),
None => crate::env::remove_var("MISE_SYSTEM_BREW_PREFIX"),
}
}
}
#[test]
fn removes_abandoned_locking_staging_directories() {
let rack = tempfile::tempdir().unwrap();
let extract = rack.path().join(".mise-extract-1.0-abandoned");
crate::file::create_dir_all(&extract).unwrap();
drop(
crate::lock_file::LockFile::at(&extract.join(".mise-lock"))
.lock()
.unwrap(),
);
remove_abandoned_staging_dirs(rack.path()).unwrap();
assert!(!extract.exists());
}
#[test]
fn preserves_legacy_lockless_staging_directories() {
let rack = tempfile::tempdir().unwrap();
let extract = rack.path().join(".mise-extract-1.0-legacy");
let legacy_tmp = rack.path().join(".mise-tmp-1.0");
crate::file::create_dir_all(&extract).unwrap();
crate::file::create_dir_all(&legacy_tmp).unwrap();
remove_abandoned_staging_dirs(rack.path()).unwrap();
assert!(extract.exists());
assert!(legacy_tmp.exists());
}
#[test]
fn preserves_locked_staging_until_its_owner_finishes() {
let rack = tempfile::tempdir().unwrap();
let active = rack.path().join(".mise-extract-1.0-active");
crate::file::create_dir_all(&active).unwrap();
let held = crate::lock_file::LockFile::at(&active.join(".mise-lock"))
.lock()
.unwrap();
remove_abandoned_staging_dirs(rack.path()).unwrap();
assert!(active.exists());
drop(held);
remove_abandoned_staging_dirs(rack.path()).unwrap();
assert!(!active.exists());
}
fn write_lib_keg(prefix: &Path, name: &str, version: &str) -> Result<PathBuf> {
let keg = prefix.join("Cellar").join(name).join(version);
crate::file::create_dir_all(keg.join("lib"))?;
crate::file::write(keg.join("lib").join(format!("lib{name}.1.dylib")), version)?;
crate::file::make_symlink(
Path::new(&format!("lib{name}.1.dylib")),
&keg.join("lib").join(format!("lib{name}.dylib")),
)?;
crate::file::create_dir_all(keg.join("include").join(name))?;
crate::file::write(keg.join("include").join(name).join("header.h"), version)?;
crate::file::make_symlink(
Path::new("header.h"),
&keg.join("include").join(name).join("alias.h"),
)?;
Ok(keg)
}
fn brew_style_link(prefix: &Path, name: &str, version: &str) -> Result<()> {
let cellar_rel = Path::new("../Cellar").join(name).join(version);
crate::file::create_dir_all(prefix.join("opt"))?;
crate::file::make_symlink(
&Path::new("../Cellar").join(name).join(version),
&prefix.join("opt").join(name),
)?;
crate::file::create_dir_all(prefix.join("lib"))?;
for lib in [format!("lib{name}.dylib"), format!("lib{name}.1.dylib")] {
crate::file::make_symlink(
&cellar_rel.join("lib").join(&lib),
&prefix.join("lib").join(&lib),
)?;
}
crate::file::create_dir_all(prefix.join("include"))?;
crate::file::make_symlink(
&cellar_rel.join("include").join(name),
&prefix.join("include").join(name),
)?;
Ok(())
}
fn canonical_tempdir() -> Result<(tempfile::TempDir, PathBuf)> {
let tmp = tempfile::tempdir()?;
let path = tmp.path().canonicalize()?;
Ok((tmp, path))
}
#[test]
fn test_upgrade_over_brew_file_links() -> Result<()> {
let _lock = ENV_LOCK.blocking_lock();
let (_tmp, prefix) = canonical_tempdir()?;
let _guard = BrewPrefixGuard::set(&prefix);
write_lib_keg(&prefix, "foo", "1.0")?;
brew_style_link(&prefix, "foo", "1.0")?;
write_lib_keg(&prefix, "foo", "2.0")?;
link_keg("foo", "2.0", false)?;
let lib_link = prefix.join("lib").join("libfoo.dylib");
assert!(lib_link.symlink_metadata()?.is_symlink());
assert_eq!(std::fs::read_to_string(&lib_link)?, "2.0");
Ok(())
}
#[test]
fn test_upgrade_over_brew_dir_symlink() -> Result<()> {
let _lock = ENV_LOCK.blocking_lock();
let (_tmp, prefix) = canonical_tempdir()?;
let _guard = BrewPrefixGuard::set(&prefix);
let old_keg = write_lib_keg(&prefix, "foo", "1.0")?;
brew_style_link(&prefix, "foo", "1.0")?;
write_lib_keg(&prefix, "foo", "2.0")?;
link_keg("foo", "2.0", false)?;
let header = prefix.join("include").join("foo").join("header.h");
assert_eq!(std::fs::read_to_string(&header)?, "2.0");
assert_eq!(
std::fs::read_to_string(prefix.join("include").join("foo").join("alias.h"))?,
"2.0"
);
assert_eq!(
std::fs::read_to_string(old_keg.join("include").join("foo").join("header.h"))?,
"1.0"
);
Ok(())
}
#[test]
fn test_upgrade_over_link_whose_cellar_target_leaves_the_cellar() -> Result<()> {
let _lock = ENV_LOCK.blocking_lock();
let (_tmp, prefix) = canonical_tempdir()?;
let _guard = BrewPrefixGuard::set(&prefix);
for version in ["1.0", "2.0"] {
let keg = prefix.join("Cellar").join("foo").join(version);
crate::file::create_dir_all(keg.join("lib"))?;
crate::file::make_symlink(
Path::new("/usr/lib/libSystem.B.dylib"),
&keg.join("lib").join("libsys.dylib"),
)?;
}
crate::file::create_dir_all(prefix.join("opt"))?;
crate::file::make_symlink(
Path::new("../Cellar/foo/1.0"),
&prefix.join("opt").join("foo"),
)?;
crate::file::create_dir_all(prefix.join("lib"))?;
crate::file::make_symlink(
Path::new("../Cellar/foo/1.0/lib/libsys.dylib"),
&prefix.join("lib").join("libsys.dylib"),
)?;
link_keg("foo", "2.0", false)?;
assert_eq!(
std::fs::read_link(prefix.join("lib").join("libsys.dylib"))?,
PathBuf::from("../Cellar/foo/2.0/lib/libsys.dylib")
);
Ok(())
}
#[test]
fn test_foreign_regular_file_still_conflicts() -> Result<()> {
let _lock = ENV_LOCK.blocking_lock();
let (_tmp, prefix) = canonical_tempdir()?;
let _guard = BrewPrefixGuard::set(&prefix);
write_lib_keg(&prefix, "foo", "2.0")?;
crate::file::create_dir_all(prefix.join("include").join("foo"))?;
crate::file::write(prefix.join("include").join("foo").join("header.h"), "mine")?;
let err = link_keg("foo", "2.0", false).unwrap_err();
assert!(err.to_string().contains("not created by mise or brew"));
assert_eq!(
std::fs::read_to_string(prefix.join("include").join("foo").join("header.h"))?,
"mine"
);
Ok(())
}
#[test]
fn test_materialize_shared_dir_owned_by_other_keg() -> Result<()> {
let _lock = ENV_LOCK.blocking_lock();
let (_tmp, prefix) = canonical_tempdir()?;
let _guard = BrewPrefixGuard::set(&prefix);
let other = prefix.join("Cellar").join("other").join("1.0");
crate::file::create_dir_all(other.join("share").join("xml"))?;
crate::file::write(other.join("share").join("xml").join("other.dtd"), "other")?;
crate::file::create_dir_all(prefix.join("share"))?;
crate::file::make_symlink(
Path::new("../Cellar/other/1.0/share/xml"),
&prefix.join("share").join("xml"),
)?;
let keg = prefix.join("Cellar").join("foo").join("2.0");
crate::file::create_dir_all(keg.join("share").join("xml"))?;
crate::file::write(keg.join("share").join("xml").join("foo.dtd"), "foo")?;
link_keg("foo", "2.0", false)?;
let xml = prefix.join("share").join("xml");
assert!(!xml.symlink_metadata()?.is_symlink());
assert_eq!(std::fs::read_to_string(xml.join("other.dtd"))?, "other");
assert_eq!(std::fs::read_to_string(xml.join("foo.dtd"))?, "foo");
assert!(!other.join("share").join("xml").join("foo.dtd").exists());
Ok(())
}
#[test]
fn test_nested_relative_link_is_brew_owned() -> Result<()> {
let _lock = ENV_LOCK.blocking_lock();
let (_tmp, prefix) = canonical_tempdir()?;
let _guard = BrewPrefixGuard::set(&prefix);
let keg = prefix.join("Cellar/foo/1.0/lib");
crate::file::create_dir_all(&keg)?;
crate::file::write(keg.join("libfoo.1.dylib"), "")?;
let lib = prefix.join("lib");
crate::file::create_dir_all(&lib)?;
crate::file::make_symlink(Path::new("../Cellar/foo/1.0/lib"), &lib.join("foo"))?;
let nested = lib.join("libfoo.dylib");
crate::file::make_symlink(Path::new("foo/libfoo.1.dylib"), &nested)?;
assert!(can_overwrite(&nested));
Ok(())
}
#[test]
fn test_link_keg_maintains_homebrew_linked_record() -> Result<()> {
let _lock = ENV_LOCK.blocking_lock();
let (_tmp, prefix) = canonical_tempdir()?;
let _guard = BrewPrefixGuard::set(&prefix);
write_lib_keg(&prefix, "foo", "1.0")?;
link_keg("foo", "1.0", false)?;
let linked = prefix::linked_keg_record("foo");
assert_eq!(
std::fs::read_link(&linked)?,
PathBuf::from("../../../Cellar/foo/1.0")
);
write_lib_keg(&prefix, "foo", "2.0")?;
link_keg("foo", "2.0", false)?;
assert_eq!(
std::fs::read_link(&linked)?,
PathBuf::from("../../../Cellar/foo/2.0")
);
let bar_keg = write_lib_keg(&prefix, "bar", "1.0")?;
link_keg("bar", "1.0", true)?;
assert!(prefix.join("opt/bar").is_symlink());
assert!(prefix::linked_keg_record("bar").symlink_metadata().is_err());
assert_eq!(linked_state("bar"), Some(("1.0".to_string(), false)));
crate::file::make_symlink(
&bar_keg.join("lib/libbar.1.dylib"),
&prefix.join("lib/libbar.1.dylib"),
)?;
assert!(!repair_link_record("bar", false)?);
assert!(prefix::linked_keg_record("bar").symlink_metadata().is_err());
let linked = prefix::linked_keg_record("bar");
crate::file::create_dir_all(linked.parent().unwrap())?;
crate::file::make_symlink(Path::new("../../../Cellar/bar/1.0"), &linked)?;
std::fs::remove_file(prefix.join("opt/bar"))?;
assert_eq!(linked_state("bar"), Some(("1.0".to_string(), true)));
assert!(repair_link_record("bar", false)?);
assert_eq!(
std::fs::read_link(prefix.join("opt/bar"))?,
PathBuf::from("../Cellar/bar/1.0")
);
Ok(())
}
#[test]
fn test_repairs_active_records_without_relinking_the_keg() -> Result<()> {
let _lock = ENV_LOCK.blocking_lock();
let (_tmp, prefix) = canonical_tempdir()?;
let _guard = BrewPrefixGuard::set(&prefix);
write_lib_keg(&prefix, "foo", "1.0")?;
brew_style_link(&prefix, "foo", "1.0")?;
let public = prefix.join("lib/libfoo.dylib");
let public_target = std::fs::read_link(&public)?;
assert_eq!(linked_state("foo"), Some(("1.0".to_string(), true)));
assert!(repair_link_record("foo", false)?);
assert_eq!(std::fs::read_link(&public)?, public_target);
assert_eq!(
std::fs::read_link(prefix::linked_keg_record("foo"))?,
PathBuf::from("../../../Cellar/foo/1.0")
);
crate::file::remove_file(prefix.join("opt/foo"))?;
assert_eq!(linked_state("foo"), Some(("1.0".to_string(), true)));
assert!(repair_link_record("foo", false)?);
assert_eq!(
std::fs::read_link(prefix.join("opt/foo"))?,
PathBuf::from("../Cellar/foo/1.0")
);
assert_eq!(std::fs::read_link(&public)?, public_target);
Ok(())
}
#[test]
fn test_repairs_dangling_owned_records_but_not_foreign_records() -> Result<()> {
let _lock = ENV_LOCK.blocking_lock();
let (_tmp, prefix) = canonical_tempdir()?;
let _guard = BrewPrefixGuard::set(&prefix);
write_lib_keg(&prefix, "foo", "1.0")?;
brew_style_link(&prefix, "foo", "1.0")?;
let linked = prefix::linked_keg_record("foo");
crate::file::create_dir_all(linked.parent().unwrap())?;
crate::file::make_symlink(Path::new("../../../Cellar/foo/0.9"), &linked)?;
assert_eq!(linked_state("foo"), Some(("1.0".to_string(), true)));
assert!(repair_link_record("foo", false)?);
assert_eq!(
std::fs::read_link(&linked)?,
PathBuf::from("../../../Cellar/foo/1.0")
);
let opt = prefix.join("opt/foo");
crate::file::make_symlink(Path::new("../Cellar/foo/0.9"), &opt)?;
assert_eq!(linked_state("foo"), Some(("1.0".to_string(), true)));
assert!(repair_link_record("foo", false)?);
assert_eq!(
std::fs::read_link(&opt)?,
PathBuf::from("../Cellar/foo/1.0")
);
crate::file::make_symlink(Path::new("/custom/missing-foo"), &linked)?;
assert_eq!(linked_state("foo"), Some(("1.0".to_string(), false)));
assert!(!repair_link_record("foo", false)?);
assert_eq!(
std::fs::read_link(&linked)?,
PathBuf::from("/custom/missing-foo")
);
Ok(())
}
#[tokio::test]
async fn test_manager_repairs_linked_record_without_repouring() -> Result<()> {
use std::os::unix::fs::MetadataExt;
use crate::system::packages::{
InstallOpts, PackageRequest, PackageState, SystemPackageManager,
};
let _lock = ENV_LOCK.lock().await;
let (_tmp, prefix) = canonical_tempdir()?;
let _guard = BrewPrefixGuard::set(&prefix);
let keg = write_lib_keg(&prefix, "foo", "1.0")?;
crate::file::write(keg.join("INSTALL_RECEIPT.json"), "{}")?;
brew_style_link(&prefix, "foo", "1.0")?;
let public = prefix.join("lib/libfoo.dylib");
let request = PackageRequest {
name: "foo".to_string(),
version: None,
tap_url: None,
desired: crate::system::packages::PackageDesiredState::Present,
};
let keg_inode = keg.metadata()?.ino();
let receipt_modified = keg.join("INSTALL_RECEIPT.json").metadata()?.modified()?;
let public_inode = public.symlink_metadata()?.ino();
let manager = super::super::BrewManager::new();
let mismatched = PackageRequest {
version: Some("2.0".to_string()),
..request.clone()
};
let err = manager
.install(std::slice::from_ref(&mismatched), &InstallOpts::default())
.await
.unwrap_err();
assert!(err.to_string().contains("pin via the formula name"));
assert!(prefix::linked_keg_record("foo").symlink_metadata().is_err());
let status = manager.installed(std::slice::from_ref(&request)).await?;
assert_eq!(
status[0].state,
PackageState::NeedsRepair {
installed: "1.0".to_string()
}
);
manager
.install(std::slice::from_ref(&request), &InstallOpts::default())
.await?;
assert_eq!(keg.metadata()?.ino(), keg_inode);
assert_eq!(
keg.join("INSTALL_RECEIPT.json").metadata()?.modified()?,
receipt_modified
);
assert_eq!(public.symlink_metadata()?.ino(), public_inode);
assert_eq!(
std::fs::read_link(prefix::linked_keg_record("foo"))?,
PathBuf::from("../../../Cellar/foo/1.0")
);
assert_eq!(
manager.installed(std::slice::from_ref(&request)).await?[0].state,
PackageState::Installed {
version: "1.0".to_string()
}
);
Ok(())
}
#[test]
fn test_does_not_infer_a_linked_record_without_public_links() -> Result<()> {
let _lock = ENV_LOCK.blocking_lock();
let (_tmp, prefix) = canonical_tempdir()?;
let _guard = BrewPrefixGuard::set(&prefix);
write_lib_keg(&prefix, "foo", "1.0")?;
crate::file::create_dir_all(prefix.join("opt"))?;
crate::file::make_symlink(Path::new("../Cellar/foo/1.0"), &prefix.join("opt/foo"))?;
assert_eq!(linked_state("foo"), Some(("1.0".to_string(), false)));
assert!(!repair_link_record("foo", false)?);
assert!(prefix::linked_keg_record("foo").symlink_metadata().is_err());
Ok(())
}
#[test]
fn test_runtime_loader_does_not_make_glibc_look_linked() -> Result<()> {
let _lock = ENV_LOCK.blocking_lock();
let (_tmp, prefix) = canonical_tempdir()?;
let _guard = BrewPrefixGuard::set(&prefix);
let keg = prefix.join("Cellar/glibc/1.0");
crate::file::create_dir_all(keg.join("lib"))?;
crate::file::write(keg.join("lib/ld-linux-x86-64.so.2"), "")?;
crate::file::create_dir_all(prefix.join("opt"))?;
crate::file::make_symlink(Path::new("../Cellar/glibc/1.0"), &prefix.join("opt/glibc"))?;
crate::file::create_dir_all(prefix.join("lib"))?;
crate::file::make_symlink(
&keg.join("lib/ld-linux-x86-64.so.2"),
&prefix.join("lib/ld.so"),
)?;
assert_eq!(linked_state("glibc"), Some(("1.0".to_string(), false)));
assert!(!repair_link_record("glibc", false)?);
assert!(
prefix::linked_keg_record("glibc")
.symlink_metadata()
.is_err()
);
Ok(())
}
#[test]
fn test_foreign_linked_record_blocks_linking_before_changes() -> Result<()> {
let _lock = ENV_LOCK.blocking_lock();
let (_tmp, prefix) = canonical_tempdir()?;
let _guard = BrewPrefixGuard::set(&prefix);
write_lib_keg(&prefix, "foo", "1.0")?;
let linked = prefix::linked_keg_record("foo");
crate::file::create_dir_all(linked.parent().unwrap())?;
crate::file::write(&linked, "foreign")?;
let err = link_keg("foo", "1.0", false).unwrap_err();
assert!(err.to_string().contains("not created by mise or brew"));
assert_eq!(crate::file::read_to_string(&linked)?, "foreign");
assert!(prefix.join("opt/foo").symlink_metadata().is_err());
Ok(())
}
#[test]
fn test_foreign_opt_file_blocks_linking_before_changes() -> Result<()> {
let _lock = ENV_LOCK.blocking_lock();
let (_tmp, prefix) = canonical_tempdir()?;
let _guard = BrewPrefixGuard::set(&prefix);
write_lib_keg(&prefix, "foo", "1.0")?;
let opt = prefix.join("opt/foo");
crate::file::create_dir_all(opt.parent().unwrap())?;
crate::file::write(&opt, "foreign")?;
let err = link_keg("foo", "1.0", false).unwrap_err();
assert!(err.to_string().contains("not created by mise or brew"));
assert_eq!(crate::file::read_to_string(&opt)?, "foreign");
assert!(prefix.join("lib/libfoo.dylib").symlink_metadata().is_err());
Ok(())
}
#[test]
fn test_relative_target() {
assert_eq!(
relative_target(
Path::new("/opt/homebrew/Cellar/jq/1.7/bin/jq"),
Path::new("/opt/homebrew/bin/jq"),
),
PathBuf::from("../Cellar/jq/1.7/bin/jq")
);
assert_eq!(
relative_target(
Path::new("/opt/homebrew/Cellar/jq/1.7"),
Path::new("/opt/homebrew/opt/jq"),
),
PathBuf::from("../Cellar/jq/1.7")
);
}
fn query_keg(prefix: &Path, name: &str, version: &str) -> Result<PathBuf> {
let keg = prefix.join("Cellar").join(name).join(version);
std::fs::create_dir_all(&keg)?;
std::fs::create_dir_all(prefix.join("opt"))?;
Ok(keg)
}
fn assert_query_target(name: &str, opt: &Path, keg: &Path) -> Result<()> {
let root = package_root(name)?;
assert_eq!(root, opt);
assert!(root.is_absolute());
assert_eq!(root.canonicalize()?, keg.canonicalize()?);
Ok(())
}
#[test]
fn package_root_normalizes_plain_versioned_and_qualified_names() -> Result<()> {
let _lock = ENV_LOCK.blocking_lock();
let (_tmp, prefix) = canonical_tempdir()?;
let _guard = BrewPrefixGuard::set(&prefix);
for name in ["widget", "openssl@3", "widget@latest", "widget@1.2"] {
let keg = query_keg(&prefix, name, "opaque-active")?;
let opt = prefix.join("opt").join(name);
symlink(&keg, &opt)?;
for request in [
name.to_string(),
format!("homebrew/core/{name}"),
format!("owner/tap/{name}"),
format!("another/tap/{name}"),
] {
assert_query_target(&request, &opt, &keg)?;
}
}
Ok(())
}
#[test]
fn package_root_rejects_invalid_identifiers_before_filesystem_lookup() -> Result<()> {
let _lock = ENV_LOCK.blocking_lock();
let (_tmp, prefix) = canonical_tempdir()?;
let _guard = BrewPrefixGuard::set(&prefix.join("missing-prefix"));
for name in [
"",
".",
"..",
"/widget",
"../widget",
"owner/widget",
"a/b/c/d",
"a//widget",
"a/b/widget/",
"a/./widget",
"../b/widget",
"a/../widget",
"a/b/..",
"a/b/.",
"widget:name",
"a:b/c/widget",
"a/b/widget:name",
"widget\\name",
"a\\b/c/widget",
" widget",
"wid get",
"a/ b/widget",
"widget\n",
"widget\r",
"wid\tget",
"a/b/wid\0get",
"a/b/wid\u{7f}get",
"wid\u{a0}get",
] {
let error = format!("{:#}", package_root(name).unwrap_err());
assert!(error.contains("brew:"), "{name:?}: {error}");
assert!(!error.contains("missing-prefix"), "{name:?}: {error}");
}
Ok(())
}
#[test]
fn package_root_rejects_the_cask_namespace() -> Result<()> {
let _lock = ENV_LOCK.blocking_lock();
let (_tmp, prefix) = canonical_tempdir()?;
let _guard = BrewPrefixGuard::set(&prefix);
let keg = query_keg(&prefix, "widget", "active")?;
symlink(keg, prefix.join("opt/widget"))?;
let error = format!("{:#}", package_root("homebrew/cask/widget").unwrap_err());
assert!(error.contains("cask"), "{error}");
Ok(())
}
#[test]
fn package_root_accepts_relative_opt_without_receipts_or_public_links() -> Result<()> {
let _lock = ENV_LOCK.blocking_lock();
let (_tmp, prefix) = canonical_tempdir()?;
let _guard = BrewPrefixGuard::set(&prefix);
let keg = query_keg(&prefix, "widget", "active")?;
let opt = prefix.join("opt/widget");
symlink("../Cellar/widget/active", &opt)?;
assert_query_target("widget", &opt, &keg)?;
assert!(!prefix.join("bin").exists());
assert!(!prefix.join("var/homebrew/linked/widget").exists());
assert_eq!(std::fs::read_dir(keg)?.count(), 0);
Ok(())
}
#[test]
fn package_root_follows_only_the_active_opaque_version_and_preserves_records() -> Result<()> {
let _lock = ENV_LOCK.blocking_lock();
let (_tmp, prefix) = canonical_tempdir()?;
let _guard = BrewPrefixGuard::set(&prefix);
let old = query_keg(&prefix, "widget", "old-channel")?;
let new = query_keg(&prefix, "widget", "2099.12.31")?;
let opt = prefix.join("opt/widget");
std::fs::write(old.join("payload"), "old")?;
std::fs::write(new.join("payload"), "new")?;
symlink(&old, &opt)?;
assert_query_target("widget", &opt, &old)?;
assert_eq!(std::fs::read_link(&opt)?, old);
std::fs::remove_file(&opt)?;
symlink(&new, &opt)?;
assert_query_target("widget", &opt, &new)?;
assert_eq!(std::fs::read_link(&opt)?, new);
assert_eq!(std::fs::read_to_string(old.join("payload"))?, "old");
assert_eq!(std::fs::read_to_string(new.join("payload"))?, "new");
Ok(())
}
#[test]
fn package_root_preserves_symlinked_prefix_and_spaces() -> Result<()> {
let _lock = ENV_LOCK.blocking_lock();
let (_tmp, base) = canonical_tempdir()?;
let prefix = base.join("prefix with spaces");
let alias = base.join("prefix alias");
let keg = query_keg(&prefix, "widget", "active")?;
symlink(&prefix, &alias)?;
symlink(&keg, prefix.join("opt/widget"))?;
let _guard = BrewPrefixGuard::set(&alias);
assert_query_target("widget", &alias.join("opt/widget"), &keg)
}
#[test]
fn package_root_makes_relative_prefix_absolute() -> Result<()> {
let _lock = ENV_LOCK.blocking_lock();
let cwd = std::env::current_dir()?;
let tmp = tempfile::tempdir_in(&cwd)?;
let relative = tmp.path().strip_prefix(&cwd)?;
let _guard = BrewPrefixGuard::set(relative);
let keg = query_keg(tmp.path(), "widget", "active")?;
symlink("../Cellar/widget/active", tmp.path().join("opt/widget"))?;
assert_query_target("widget", &cwd.join(relative).join("opt/widget"), &keg)
}
#[test]
fn package_root_requires_opt_even_with_cellar_or_linked_keg() -> Result<()> {
let _lock = ENV_LOCK.blocking_lock();
let (_tmp, base) = canonical_tempdir()?;
for state in [
"missing-prefix",
"empty-prefix",
"cellar-only",
"linked-only",
] {
let prefix = base.join(state);
let _guard = BrewPrefixGuard::set(&prefix);
if state != "missing-prefix" {
std::fs::create_dir_all(&prefix)?;
}
if matches!(state, "cellar-only" | "linked-only") {
let keg = query_keg(&prefix, "widget", "active")?;
if state == "linked-only" {
std::fs::create_dir_all(prefix.join("var/homebrew/linked"))?;
symlink(keg, prefix.join("var/homebrew/linked/widget"))?;
}
}
let error = format!("{:#}", package_root("widget").unwrap_err());
assert!(error.contains("widget"), "{state}: {error}");
assert!(
error.contains(&prefix.display().to_string()),
"{state}: {error}"
);
assert!(error.contains("apply brew:widget"), "{state}: {error}");
assert!(!prefix.join("opt/widget").exists());
}
Ok(())
}
#[test]
fn package_root_rejects_dangling_opt_without_repairing_it() -> Result<()> {
let _lock = ENV_LOCK.blocking_lock();
let (_tmp, prefix) = canonical_tempdir()?;
let _guard = BrewPrefixGuard::set(&prefix);
std::fs::create_dir_all(prefix.join("opt"))?;
let opt = prefix.join("opt/widget");
let target = Path::new("../Cellar/widget/missing");
symlink(target, &opt)?;
let error = format!("{:#}", package_root("widget").unwrap_err());
assert!(error.contains(&opt.display().to_string()), "{error}");
assert!(error.contains("apply brew:widget"), "{error}");
assert_eq!(std::fs::read_link(opt)?, target);
Ok(())
}
#[test]
fn package_root_rejects_regular_directory_and_file_opt_records() -> Result<()> {
let _lock = ENV_LOCK.blocking_lock();
let (_tmp, base) = canonical_tempdir()?;
for directory in [true, false] {
let prefix = base.join(directory.to_string());
let _guard = BrewPrefixGuard::set(&prefix);
query_keg(&prefix, "widget", "active")?;
let opt = prefix.join("opt/widget");
if directory {
std::fs::create_dir(&opt)?;
} else {
std::fs::write(&opt, "untouched")?;
}
let error = format!("{:#}", package_root("widget").unwrap_err());
assert!(error.contains(&opt.display().to_string()), "{error}");
assert!(!opt.symlink_metadata()?.is_symlink());
if !directory {
assert_eq!(std::fs::read_to_string(opt)?, "untouched");
}
}
Ok(())
}
#[test]
fn package_root_rejects_foreign_nested_rack_and_file_targets() -> Result<()> {
let _lock = ENV_LOCK.blocking_lock();
let (_tmp, prefix) = canonical_tempdir()?;
let _guard = BrewPrefixGuard::set(&prefix);
let keg = query_keg(&prefix, "widget", "active")?;
let foreign = query_keg(&prefix, "foreign", "active")?;
std::fs::create_dir(keg.join("nested"))?;
let file = prefix.join("Cellar/widget/file");
std::fs::write(&file, "untouched")?;
let opt = prefix.join("opt/widget");
for target in [
foreign,
keg.join("nested"),
prefix.join("Cellar/widget"),
file,
] {
symlink(&target, &opt)?;
let error = format!("{:#}", package_root("widget").unwrap_err());
assert!(error.contains(&opt.display().to_string()), "{error}");
assert_eq!(std::fs::read_link(&opt)?, target);
std::fs::remove_file(&opt)?;
}
Ok(())
}
#[test]
fn package_root_accepts_outside_intermediary_resolving_to_its_keg() -> Result<()> {
let _lock = ENV_LOCK.blocking_lock();
let (_tmp, prefix) = canonical_tempdir()?;
let _guard = BrewPrefixGuard::set(&prefix);
let keg = query_keg(&prefix, "widget", "active")?;
let intermediary = prefix.join("intermediary");
symlink(&keg, &intermediary)?;
let opt = prefix.join("opt/widget");
symlink(intermediary, &opt)?;
assert_query_target("widget", &opt, &keg)
}
#[test]
fn package_root_uses_filesystem_dotdot_semantics_to_reject_foreign_target() -> Result<()> {
let _lock = ENV_LOCK.blocking_lock();
let (_tmp, prefix) = canonical_tempdir()?;
let _guard = BrewPrefixGuard::set(&prefix);
let keg = query_keg(&prefix, "widget", "active")?;
let outside = prefix.join("outside");
std::fs::create_dir_all(outside.join("child"))?;
std::fs::create_dir(outside.join("active"))?;
symlink(outside.join("child"), prefix.join("Cellar/widget/jump"))?;
let opt = prefix.join("opt/widget");
symlink("../Cellar/widget/jump/../active", &opt)?;
assert_ne!(opt.canonicalize()?, keg);
assert!(package_root("widget").is_err());
Ok(())
}
#[test]
fn package_root_uses_filesystem_dotdot_semantics_to_accept_local_target() -> Result<()> {
let _lock = ENV_LOCK.blocking_lock();
let (_tmp, prefix) = canonical_tempdir()?;
let _guard = BrewPrefixGuard::set(&prefix);
let keg = query_keg(&prefix, "widget", "active")?;
std::fs::create_dir(prefix.join("outside"))?;
std::fs::create_dir(prefix.join("Cellar/widget/child"))?;
symlink(
prefix.join("Cellar/widget/child"),
prefix.join("outside/jump"),
)?;
let opt = prefix.join("opt/widget");
symlink("../outside/jump/../active", &opt)?;
assert_query_target("widget", &opt, &keg)
}
#[test]
fn package_root_preserves_symlink_loop_io_error_and_path() -> Result<()> {
let _lock = ENV_LOCK.blocking_lock();
let (_tmp, prefix) = canonical_tempdir()?;
let _guard = BrewPrefixGuard::set(&prefix);
query_keg(&prefix, "widget", "active")?;
let opt = prefix.join("opt/widget");
symlink("widget", &opt)?;
let expected = opt.canonicalize().unwrap_err();
let error = package_root("widget").unwrap_err();
assert!(format!("{error:#}").contains(&opt.display().to_string()));
let io = error
.chain()
.find_map(|cause| cause.downcast_ref::<std::io::Error>());
assert_eq!(
io.expect("retain underlying filesystem error")
.raw_os_error(),
expected.raw_os_error()
);
assert_eq!(std::fs::read_link(opt)?, Path::new("widget"));
Ok(())
}
#[test]
fn package_root_preserves_permission_denial_and_path() -> Result<()> {
if nix::unistd::geteuid().is_root() {
return Ok(());
}
let _lock = ENV_LOCK.blocking_lock();
let (_tmp, prefix) = canonical_tempdir()?;
let _guard = BrewPrefixGuard::set(&prefix);
let keg = query_keg(&prefix, "widget", "active")?;
let opt = prefix.join("opt/widget");
symlink(&keg, &opt)?;
let rack = prefix.join("Cellar/widget");
let permissions = rack.metadata()?.permissions();
std::fs::set_permissions(&rack, std::fs::Permissions::from_mode(0o0))?;
let result = package_root("widget");
std::fs::set_permissions(&rack, permissions)?;
let error = result.unwrap_err();
assert!(format!("{error:#}").contains(&opt.display().to_string()));
let io = error
.chain()
.find_map(|cause| cause.downcast_ref::<std::io::Error>());
assert_eq!(
io.expect("retain underlying filesystem error").kind(),
std::io::ErrorKind::PermissionDenied
);
Ok(())
}
}