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:
2026-08-03 09:09:37 +03:00
parent 7c1627d6c0
commit 9ac798d333
+105 -8
View File
@@ -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!(