fix: cookbook self-heals dangling and stale recipe source symlinks
Path-source recipes symlink recipes/<comp>/source at the local fork. The
guard used Path::exists(), which follows symlinks, so two cases silently
failed and then aborted the build with EEXIST:
* a DANGLING symlink (left behind after the checkout moved) reports
exists()==false, so nothing was removed;
* a live symlink TO a directory reports true, but remove_dir_all()
refuses to operate on a symlink and the error was swallowed by .ok().
Every core fork (relibc, kernel, base, bootloader, installer, redoxfs,
userutils) was dangling for this reason and no build could start.
force_symlink() classifies the entry with symlink_metadata() (which does
not follow the final component) and removes it correctly. It also writes
a RELATIVE link: recipe source links are committed build state that must
survive the checkout being moved or cloned to another prefix, and an
absolute link bakes in one machine's layout.
fetch_make_symlink() now repairs a same_as link whose target no longer
matches the recipe, instead of keeping it forever.
This commit is contained in:
+105
-8
@@ -35,6 +35,91 @@ pub struct FetchResult {
|
||||
pub cached: bool,
|
||||
}
|
||||
|
||||
/// Build the shortest relative path that walks from `from` (a directory) to `to`.
|
||||
///
|
||||
/// Both inputs must already be absolute and normalized (canonicalized). Returns
|
||||
/// `None` when the two paths share no common prefix (e.g. different mount roots
|
||||
/// on platforms where that is representable), in which case the caller should
|
||||
/// fall back to an absolute link.
|
||||
fn relative_path_from(from: &Path, to: &Path) -> Option<PathBuf> {
|
||||
let mut from_parts = from.components().peekable();
|
||||
let mut to_parts = to.components().peekable();
|
||||
|
||||
// Drop the shared prefix.
|
||||
while let (Some(f), Some(t)) = (from_parts.peek(), to_parts.peek()) {
|
||||
if f == t {
|
||||
from_parts.next();
|
||||
to_parts.next();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let mut rel = PathBuf::new();
|
||||
let mut climbed = false;
|
||||
for _ in from_parts {
|
||||
rel.push("..");
|
||||
climbed = true;
|
||||
}
|
||||
let mut descended = false;
|
||||
for part in to_parts {
|
||||
rel.push(part);
|
||||
descended = true;
|
||||
}
|
||||
|
||||
if !climbed && !descended {
|
||||
// `from` == `to`; a self-referential link is never what we want.
|
||||
return None;
|
||||
}
|
||||
Some(rel)
|
||||
}
|
||||
|
||||
/// Point `link` at `target`, replacing whatever currently occupies `link`.
|
||||
///
|
||||
/// This exists because `Path::exists()` follows symlinks, so the obvious
|
||||
/// `if link.exists() { remove_dir_all(link) }` guard silently fails in the two
|
||||
/// cases that matter most here:
|
||||
///
|
||||
/// * `link` is a **dangling** symlink (e.g. left behind after the repo was
|
||||
/// moved). `exists()` reports `false`, nothing is removed, and the
|
||||
/// subsequent `symlink()` fails with `EEXIST`.
|
||||
/// * `link` is a **live symlink to a directory**. `exists()` reports `true`,
|
||||
/// but `remove_dir_all()` refuses to operate on a symlink, so the removal
|
||||
/// fails and `symlink()` again fails with `EEXIST`.
|
||||
///
|
||||
/// `symlink_metadata()` does not follow the final component, so it classifies
|
||||
/// the entry itself and lets us pick the correct removal call.
|
||||
///
|
||||
/// The link is written as a **relative** path whenever the two live under a
|
||||
/// common ancestor. Recipe `source` links are committed build state that must
|
||||
/// survive the checkout being moved or cloned to a different prefix; an
|
||||
/// absolute link bakes in one machine's layout and dangles everywhere else —
|
||||
/// exactly the breakage this function had to recover from.
|
||||
pub(crate) fn force_symlink(target: &Path, link: &Path) -> std::io::Result<()> {
|
||||
match fs::symlink_metadata(link) {
|
||||
Ok(meta) => {
|
||||
if meta.file_type().is_dir() {
|
||||
fs::remove_dir_all(link)?;
|
||||
} else {
|
||||
// Regular file, or a symlink of any kind (including dangling
|
||||
// ones and symlinks pointing at directories).
|
||||
fs::remove_file(link)?;
|
||||
}
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
|
||||
let link_dir = link.parent().unwrap_or_else(|| Path::new("."));
|
||||
let link_target = link_dir
|
||||
.canonicalize()
|
||||
.ok()
|
||||
.and_then(|base| relative_path_from(&base, target))
|
||||
.unwrap_or_else(|| target.to_path_buf());
|
||||
|
||||
std::os::unix::fs::symlink(link_target, link)
|
||||
}
|
||||
|
||||
pub(crate) fn cleanup_workspace_pollution(recipe_dir: &Path, logger: &PtyOut) {
|
||||
let recipes_root = recipe_dir.join("../..");
|
||||
for file in &["Cargo.toml", "Cargo.lock"] {
|
||||
@@ -373,10 +458,7 @@ pub fn fetch_offline(recipe: &CookRecipe, logger: &PtyOut) -> Result<FetchResult
|
||||
path.display(),
|
||||
source_dir.display()
|
||||
);
|
||||
if source_dir.exists() {
|
||||
std::fs::remove_dir_all(&source_dir).ok();
|
||||
}
|
||||
std::os::unix::fs::symlink(path.canonicalize().unwrap_or(path.clone()), &source_dir).map_err(|e| {
|
||||
force_symlink(&path.canonicalize().unwrap_or(path.clone()), &source_dir).map_err(|e| {
|
||||
format!("failed to symlink {:?} -> {:?}: {}", path.display(), source_dir.display(), e)
|
||||
})?;
|
||||
} else {
|
||||
@@ -617,10 +699,7 @@ pub fn fetch(recipe: &CookRecipe, check_source: bool, logger: &PtyOut) -> Result
|
||||
path.display(),
|
||||
source_dir.display()
|
||||
);
|
||||
if source_dir.exists() {
|
||||
std::fs::remove_dir_all(&source_dir).ok();
|
||||
}
|
||||
std::os::unix::fs::symlink(path.canonicalize().unwrap_or(path.clone()), &source_dir).map_err(|e| {
|
||||
force_symlink(&path.canonicalize().unwrap_or(path.clone()), &source_dir).map_err(|e| {
|
||||
format!("failed to symlink {:?} -> {:?}: {}", path.display(), source_dir.display(), e)
|
||||
})?;
|
||||
} else {
|
||||
@@ -1014,6 +1093,24 @@ fn fetch_will_build(recipe: &CookRecipe) -> bool {
|
||||
|
||||
pub(crate) fn fetch_make_symlink(source_dir: &PathBuf, same_as: &String) -> Result<()> {
|
||||
let target_dir = Path::new(same_as).join("source");
|
||||
// An existing symlink is only reusable if it still points where the recipe
|
||||
// says it should. A link left over from an earlier layout (e.g. the repo was
|
||||
// moved, so the recorded path no longer resolves) would otherwise be kept
|
||||
// forever, and the build would fail later with a confusing missing-source
|
||||
// error instead of being repaired here.
|
||||
let stale_link = source_dir.is_symlink()
|
||||
&& fs::read_link(source_dir)
|
||||
.map(|current| current != target_dir)
|
||||
.unwrap_or(true);
|
||||
if stale_link {
|
||||
fs::remove_file(source_dir).map_err(|err| {
|
||||
format!(
|
||||
"failed to remove stale symlink '{}': {}",
|
||||
source_dir.display(),
|
||||
err
|
||||
)
|
||||
})?;
|
||||
}
|
||||
if !source_dir.is_symlink() {
|
||||
if source_dir.is_dir() {
|
||||
bail_other_err!(
|
||||
|
||||
Reference in New Issue
Block a user