diff --git a/src/bin/repo.rs b/src/bin/repo.rs index d7ffb42a93..4b66b6acc4 100644 --- a/src/bin/repo.rs +++ b/src/bin/repo.rs @@ -375,6 +375,41 @@ fn print_cached(command: &CliCommand, recipe: &PackageName) { /// This is safe because each recipe builds in its own target/stage/sysroot and a /// recipe's sysroot is assembled from its dependencies' (lower-level, already /// complete) stage pkgars — read-only for concurrent siblings. +/// Serialise cook units that share a recipe directory. +/// +/// `dep_levels` keys on the package NAME, so a recipe's optional-package +/// variants (`llvm-native`, `llvm-native.dev`, `llvm-native.runtime`) are +/// distinct units with no dependency between them and therefore land in the +/// same level and cook concurrently. They do NOT have independent workspaces: +/// `build/`, `sysroot/` and the `stage*` dirs are all derived from the shared +/// `target//` of their common recipe directory. `create_dir_clean()` +/// on `build/` then wipes a sibling's tree mid-compile: +/// +/// ninja: error: failed recompaction: No such file or directory +/// Canonicalize stage dir failed at ".../stage.tmp": No such file or directory +/// +/// which is why `llvm-native` cooked clean on its own and failed in every +/// parallel build. Unique temp-directory names alone cannot fix this, because +/// `build/` is a real, shared, destructively re-created directory. +/// +/// This restores the invariant `run_parallel_cook` documents -- "each recipe +/// builds in its own target/stage/sysroot" -- by making the recipe DIRECTORY, +/// not the package name, the unit of mutual exclusion. Distinct recipes still +/// cook fully in parallel. Serialising siblings costs almost nothing: one cook +/// already populates every stage dir for the recipe, so the siblings that +/// follow find their work done and return cached instead of rebuilding (which +/// also stops LLVM being built three times over). +fn recipe_dir_lock(dir: &std::path::Path) -> std::sync::Arc> { + use std::collections::HashMap; + use std::path::PathBuf; + use std::sync::{Arc, Mutex, OnceLock}; + static LOCKS: OnceLock>>>> = OnceLock::new(); + let key = dir.canonicalize().unwrap_or_else(|_| dir.to_path_buf()); + let table = LOCKS.get_or_init(|| Mutex::new(HashMap::new())); + let mut guard = table.lock().unwrap_or_else(|e| e.into_inner()); + Arc::clone(guard.entry(key).or_insert_with(|| Arc::new(Mutex::new(())))) +} + fn run_parallel_cook( config: &CliConfig, command: &CliCommand, @@ -435,8 +470,14 @@ fn run_parallel_cook( break; } let recipe = level_recipes[i]; + // Siblings sharing this recipe directory must not cook + // concurrently -- they share build/ and sysroot/. + let dir_lock = recipe_dir_lock(&recipe.dir); + let _dir_guard = + dir_lock.lock().unwrap_or_else(|e| e.into_inner()); let r = repo_inner(level_config, command, recipe) .map_err(|e| format!("{e:#}")); + drop(_dir_guard); results_ref.lock().unwrap().push((recipe.clone(), r)); }); }