Etapa G: detect_go_main prefiere cmd/<nombre-receta> entre varios mains

eksctl tiene cmd/eksctl (CLI) + cmd/schema (helper de codegen que panica); detect elegía
arbitrariamente y selló el binario equivocado (schema) — un fallo SILENCIOSO (sella OK pero envía
la herramienta incorrecta). Ahora, a igual clase, el dir cuyo file_name == recipe.name gana. Test
detect_go_main_prefers_cmd_matching_recipe_name. Lección: smoke-testear que el binario instalado es
el ESPERADO, no solo que selló.
This commit is contained in:
2026-06-27 06:28:41 -04:00
parent 7f11f2ed53
commit 7545fc635d
+28 -11
View File
@@ -485,7 +485,7 @@ fn detect_build_system(src: &Path) -> BuildSys {
/// scripts/tools/internal y ocultos/`_`). Elige el principal por heurística: raíz > `cmd/<x>` >
/// menor profundidad. Devuelve un path estilo go (`.`, `./cmd/mlr`). Una receta puede forzar el
/// paquete vía `build.flags` (gana sobre la auto-detección).
fn detect_go_main(src: &Path) -> String {
fn detect_go_main(src: &Path, pkg_name: &str) -> String {
fn is_main_go(path: &Path) -> bool {
let Ok(text) = std::fs::read_to_string(path) else { return false };
// El primer token significativo de un .go es la cláusula `package` (tras build-constraints
@@ -562,17 +562,22 @@ fn detect_go_main(src: &Path) -> String {
}
}
}
// raíz (prioridad 0) > cmd/<x> (1) > resto (2); a igual clase, menor profundidad.
let key = |p: &PathBuf| -> (u8, usize) {
// raíz (prioridad 0) > cmd/<x> (1) > resto (2); a igual clase, el dir cuyo nombre coincide con el
// de la receta (p.ej. cmd/eksctl para la receta `eksctl`) gana sobre otros mains hermanos (cmd/
// schema, un helper de codegen que panica) — sin esto detect elegía arbitrariamente entre varios
// mains y sellaba el binario EQUIVOCADO; luego, a igual nombre, menor profundidad.
let key = |p: &PathBuf| -> (u8, u8, usize) {
let s = p.to_string_lossy();
let depth = p.components().count();
if s.is_empty() {
(0, depth)
let name_match = if p.file_name().map(|f| f == pkg_name).unwrap_or(false) { 0 } else { 1 };
let class = if s.is_empty() {
0
} else if s.starts_with("cmd/") || s == "cmd" {
(1, depth)
1
} else {
(2, depth)
}
2
};
(class, name_match, depth)
};
found.sort_by_key(key);
match found.first() {
@@ -740,7 +745,7 @@ fn resolve_phases(recipe: &Recipe, src: &Path) -> hammer_core::Result<Phases> {
// el nombre. Offline+reproducible: vendor (vendor_go_deps) + GOPROXY=off + -trimpath
// -buildid=. El toolchain `go` llega por deps.build=["go"] (overlay).
let pkg = if recipe.build.flags.is_empty() {
detect_go_main(src)
detect_go_main(src, &recipe.name)
} else {
flags.clone()
};
@@ -839,7 +844,19 @@ commit = "deadbeef"
std::fs::create_dir_all(d.path().join(sub)).unwrap();
std::fs::write(d.path().join(sub).join("main.go"), b"package main\nfunc main(){}\n").unwrap();
}
assert_eq!(detect_go_main(d.path()), "./cmd/mlr");
assert_eq!(detect_go_main(d.path(), "mlr"), "./cmd/mlr");
}
#[test]
fn detect_go_main_prefers_cmd_matching_recipe_name() {
// eksctl-like: cmd/eksctl (CLI real) + cmd/schema (helper que panica). El dir que coincide
// con el name de la receta gana sobre el hermano ⇒ no sella el binario equivocado.
let d = tempfile::tempdir().unwrap();
for sub in ["cmd/schema", "cmd/eksctl"] {
std::fs::create_dir_all(d.path().join(sub)).unwrap();
std::fs::write(d.path().join(sub).join("main.go"), b"package main\nfunc main(){}\n").unwrap();
}
assert_eq!(detect_go_main(d.path(), "eksctl"), "./cmd/eksctl");
}
#[test]
@@ -853,7 +870,7 @@ commit = "deadbeef"
b"/*\nCopyright The Helm Authors.\nLicensed under the Apache License 2.0\n*/\n\npackage main\n\nfunc main() {}\n",
)
.unwrap();
assert_eq!(detect_go_main(d.path()), "./cmd/helm");
assert_eq!(detect_go_main(d.path(), "helm"), "./cmd/helm");
}
#[test]