cookbook: walk local/recipes/ first so Red Bear forks take precedence

Red Bear is a full fork. The canonical recipe for a Red Bear
package lives in local/recipes/<category>/<pkg>/, NOT in
recipes/<category>/<pkg>/. The recipes/ tree is a frozen snapshot
that will eventually be deleted.

Previously the cookbook only walked recipes/, so local/recipes/
recipes were unreachable unless apply-patches.sh created a
symlink in recipes/. That overlay approach was wrong (per the
NO OVERLAY-STYLE PATCHES policy in AGENTS.md).

This change makes the cookbook walk ['local/recipes', 'recipes']
in that order. When a package exists in both trees, the
local/recipes/ path ALWAYS wins. The conflict resolution in
the walker is updated to enforce this rule explicitly.

Result: local/recipes/<pkg>/recipe.toml is discovered directly,
no symlink needed. 'repo find <pkg>' returns the local path.
'repo cook <pkg>' uses the local recipe.

This makes the full-fork model work end-to-end. local/recipes/
no longer needs an apply-patches.sh symlink; the cookbook
discovers it natively.
This commit is contained in:
2026-06-09 16:52:13 +03:00
parent 13d0543c2b
commit d747b4009a
+47 -30
View File
@@ -13,39 +13,52 @@ use pkg::{Package, PackageError, PackageName};
static RECIPE_PATHS: LazyLock<HashMap<PackageName, PathBuf>> = LazyLock::new(|| { static RECIPE_PATHS: LazyLock<HashMap<PackageName, PathBuf>> = LazyLock::new(|| {
let mut recipe_paths = HashMap::<PackageName, PathBuf>::new(); let mut recipe_paths = HashMap::<PackageName, PathBuf>::new();
let mut walker = ignore::WalkBuilder::new("recipes"); // Walk local/recipes/ first so Red Bear forks take precedence over
walker.follow_links(true); // upstream recipes. This is the canonical discovery order for a
for entry_res in walker.build() { // full fork: the local overlay is the source of truth.
let Ok(entry) = entry_res else { for root in ["local/recipes", "recipes"] {
continue; let mut walker = ignore::WalkBuilder::new(root);
}; walker.follow_links(true);
if entry.file_name() == OsStr::new("recipe.toml") { for entry_res in walker.build() {
let recipe_file = entry.path(); let Ok(entry) = entry_res else {
let Some(recipe_dir) = recipe_file.parent() else {
continue; continue;
}; };
let Some(recipe_name): Option<PackageName> = recipe_dir if entry.file_name() == OsStr::new("recipe.toml") {
.file_name() let recipe_file = entry.path();
.and_then(|x| x.to_str()?.try_into().ok()) let Some(recipe_dir) = recipe_file.parent() else {
else { continue;
continue; };
}; let Some(recipe_name): Option<PackageName> = recipe_dir
let existing = recipe_paths.get(&recipe_name); .file_name()
match existing { .and_then(|x| x.to_str()?.try_into().ok())
Some(other_dir) => { else {
let other_dir = other_dir.clone(); continue;
let current_is_deeper = recipe_dir.starts_with(&other_dir); };
let existing_is_deeper = other_dir.starts_with(&recipe_dir); let existing = recipe_paths.get(&recipe_name);
if current_is_deeper { match existing {
recipe_paths.insert(recipe_name.clone(), other_dir); Some(other_dir) => {
} else if existing_is_deeper { let other_dir = other_dir.clone();
recipe_paths.insert(recipe_name, recipe_dir.to_path_buf()); let current_is_deeper = recipe_dir.starts_with(&other_dir);
} else { let existing_is_deeper = other_dir.starts_with(&recipe_dir);
recipe_paths.insert(recipe_name.clone(), recipe_dir.to_path_buf()); // Red Bear full-fork rule: local/recipes/ ALWAYS wins.
// If either side is a local/recipes/ path, keep it.
let current_is_local = recipe_dir.starts_with("local/recipes");
let existing_is_local = other_dir.starts_with("local/recipes");
if current_is_local && !existing_is_local {
recipe_paths.insert(recipe_name, recipe_dir.to_path_buf());
} else if existing_is_local && !current_is_local {
// keep existing
} else if current_is_deeper {
recipe_paths.insert(recipe_name.clone(), other_dir);
} else if existing_is_deeper {
recipe_paths.insert(recipe_name, recipe_dir.to_path_buf());
} else {
recipe_paths.insert(recipe_name.clone(), recipe_dir.to_path_buf());
}
}
None => {
recipe_paths.insert(recipe_name, recipe_dir.to_path_buf());
} }
}
None => {
recipe_paths.insert(recipe_name, recipe_dir.to_path_buf());
} }
} }
} }
@@ -53,6 +66,10 @@ static RECIPE_PATHS: LazyLock<HashMap<PackageName, PathBuf>> = LazyLock::new(||
recipe_paths recipe_paths
}); });
pub fn find(recipe: &str) -> Option<&'static Path> { pub fn find(recipe: &str) -> Option<&'static Path> {
RECIPE_PATHS.get(recipe).map(PathBuf::as_path) RECIPE_PATHS.get(recipe).map(PathBuf::as_path)
} }