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(|| {
let mut recipe_paths = HashMap::<PackageName, PathBuf>::new();
let mut walker = ignore::WalkBuilder::new("recipes");
walker.follow_links(true);
for entry_res in walker.build() {
let Ok(entry) = entry_res else {
continue;
};
if entry.file_name() == OsStr::new("recipe.toml") {
let recipe_file = entry.path();
let Some(recipe_dir) = recipe_file.parent() else {
// Walk local/recipes/ first so Red Bear forks take precedence over
// upstream recipes. This is the canonical discovery order for a
// full fork: the local overlay is the source of truth.
for root in ["local/recipes", "recipes"] {
let mut walker = ignore::WalkBuilder::new(root);
walker.follow_links(true);
for entry_res in walker.build() {
let Ok(entry) = entry_res else {
continue;
};
let Some(recipe_name): Option<PackageName> = recipe_dir
.file_name()
.and_then(|x| x.to_str()?.try_into().ok())
else {
continue;
};
let existing = recipe_paths.get(&recipe_name);
match existing {
Some(other_dir) => {
let other_dir = other_dir.clone();
let current_is_deeper = recipe_dir.starts_with(&other_dir);
let existing_is_deeper = other_dir.starts_with(&recipe_dir);
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());
if entry.file_name() == OsStr::new("recipe.toml") {
let recipe_file = entry.path();
let Some(recipe_dir) = recipe_file.parent() else {
continue;
};
let Some(recipe_name): Option<PackageName> = recipe_dir
.file_name()
.and_then(|x| x.to_str()?.try_into().ok())
else {
continue;
};
let existing = recipe_paths.get(&recipe_name);
match existing {
Some(other_dir) => {
let other_dir = other_dir.clone();
let current_is_deeper = recipe_dir.starts_with(&other_dir);
let existing_is_deeper = other_dir.starts_with(&recipe_dir);
// 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
});
pub fn find(recipe: &str) -> Option<&'static Path> {
RECIPE_PATHS.get(recipe).map(PathBuf::as_path)
}