Etapa G frente Go: AUTOMATIZADO end-to-end (BuildSys::Go + importador)
Cierra la fragilidad manual del patron Go (path del main por receta). Ahora importar Go es tan automatico como Rust: import -> pin -> build, sin tocar nada. - lib.rs: BuildSys::Go (go.mod, prioritario sobre configure/make auxiliares que traen muchos proyectos Go). resolve_phases deriva el compile generico: 'go install -trimpath -ldflags=-buildid=' del paquete main. install='true' (go install ya deja en GOBIN=/out/usr/bin). - detect_go_main(): detecta el dir del main por FILESYSTEM (no compila, evita contaminarse con mains de ejemplo rotos en docs/scripts que rompen go list). Heuristica raiz > cmd/<x> > menor profundidad; excluye vendor/docs/test/etc. go install nombra el binario solo (cmd/mlr->mlr, raiz->modulo). - nix_import.rs + nix-import.sh: detecta is_go (vendorHash de buildGoModule), emite deps.build=['go'] sin phases (BuildSys::Go las deriva). - tests: go_mod_wins_over_configure, detect_go_main_picks_cmd_over_docs. Validado end-to-end: miller (cmd/mlr->mlr, esquiva docs rotos), amfora (raiz), duf (import->pin->build 100% automatico, binario estatico que corre). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -330,6 +330,9 @@ enum BuildSys {
|
||||
Meson,
|
||||
/// `Cargo.toml`: crate o workspace Rust. Build con `cargo`, link con `zig cc`.
|
||||
Cargo,
|
||||
/// `go.mod`: módulo Go. Build con `go install` del paquete main (auto-detectado),
|
||||
/// deps vendoreadas en el fetch (`vendor_go_deps`). El toolchain `go` llega por deps.build.
|
||||
Go,
|
||||
/// `Makefile` plano (o `makefile`) sin sistema generador.
|
||||
Make,
|
||||
/// No detectamos nada: la receta debe traer overrides en `[build.phases]`.
|
||||
@@ -440,6 +443,12 @@ fn ensure_cargo_workspace_isolation(src: &Path) -> hammer_core::Result<()> {
|
||||
|
||||
fn detect_build_system(src: &Path) -> BuildSys {
|
||||
let has = |p: &str| src.join(p).exists();
|
||||
// `go.mod` PRIMERO: un módulo Go suele traer también un `configure`/`Makefile` AUXILIAR (para
|
||||
// man pages, completions, packaging), pero el binario se hace con `go build`. Ningún paquete
|
||||
// C/Rust del corpus trae go.mod, así que esto sólo captura proyectos Go (miller, etc.).
|
||||
if has("go.mod") {
|
||||
return BuildSys::Go;
|
||||
}
|
||||
// El orden importa: cuando coexisten varios (un tarball release de autotools trae el
|
||||
// `configure` regenerado y un `Makefile` huérfano de un build previo), preferimos el
|
||||
// sistema "más alto" que sí sabe regenerar todo.
|
||||
@@ -469,6 +478,75 @@ fn detect_build_system(src: &Path) -> BuildSys {
|
||||
BuildSys::Unknown
|
||||
}
|
||||
|
||||
/// Detecta el directorio del paquete `main` PRINCIPAL de un módulo Go, relativo a `src`, para
|
||||
/// `go install`. Busca por FILESYSTEM (no compila ⇒ no se contamina con mains de ejemplo rotos,
|
||||
/// como los de `docs/`/`scripts/` que rompen `go list ./...`): archivos `.go` cuya primera línea
|
||||
/// significativa es `package main`, descartando dirs auxiliares (vendor/docs/examples/tests/
|
||||
/// 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 is_main_go(path: &Path) -> bool {
|
||||
let Ok(text) = std::fs::read_to_string(path) else { return false };
|
||||
for line in text.lines() {
|
||||
let l = line.trim();
|
||||
if l.is_empty() || l.starts_with("//") || l.starts_with("/*") || l.starts_with('*') {
|
||||
continue;
|
||||
}
|
||||
return l.starts_with("package main");
|
||||
}
|
||||
false
|
||||
}
|
||||
const SKIP: &[&str] = &[
|
||||
"vendor", "docs", "doc", "example", "examples", "test", "tests", "testdata", "script",
|
||||
"scripts", "tools", "internal", "third_party",
|
||||
];
|
||||
let mut found: Vec<PathBuf> = Vec::new();
|
||||
let mut stack = vec![src.to_path_buf()];
|
||||
while let Some(dir) = stack.pop() {
|
||||
let Ok(entries) = std::fs::read_dir(&dir) else { continue };
|
||||
let mut has_main = false;
|
||||
for e in entries.flatten() {
|
||||
let p = e.path();
|
||||
if p.is_dir() {
|
||||
let name = e.file_name().to_string_lossy().to_string();
|
||||
if name.starts_with('.') || name.starts_with('_') || SKIP.contains(&name.as_str()) {
|
||||
continue;
|
||||
}
|
||||
stack.push(p);
|
||||
} else if !has_main
|
||||
&& p.extension().map(|x| x == "go").unwrap_or(false)
|
||||
&& !p.to_string_lossy().ends_with("_test.go")
|
||||
&& is_main_go(&p)
|
||||
{
|
||||
has_main = true;
|
||||
}
|
||||
}
|
||||
if has_main {
|
||||
if let Ok(rel) = dir.strip_prefix(src) {
|
||||
found.push(rel.to_path_buf());
|
||||
}
|
||||
}
|
||||
}
|
||||
// raíz (prioridad 0) > cmd/<x> (1) > resto (2); a igual clase, menor profundidad.
|
||||
let key = |p: &PathBuf| -> (u8, usize) {
|
||||
let s = p.to_string_lossy();
|
||||
let depth = p.components().count();
|
||||
if s.is_empty() {
|
||||
(0, depth)
|
||||
} else if s.starts_with("cmd/") || s == "cmd" {
|
||||
(1, depth)
|
||||
} else {
|
||||
(2, depth)
|
||||
}
|
||||
};
|
||||
found.sort_by_key(key);
|
||||
match found.first() {
|
||||
Some(p) if !p.as_os_str().is_empty() => format!("./{}", p.to_string_lossy()),
|
||||
_ => ".".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Target nativo del sandbox del lab (Alpine x86_64 musl). Una receta Cargo cuyo `build.target`
|
||||
/// sea éste se construye **nativa** (sin `--target`): el rust del sandbox sólo trae su std nativo
|
||||
/// (`x86_64-alpine-linux-musl`), no el `x86_64-unknown-linux-musl` que pediría un cross, y no hay
|
||||
@@ -522,8 +600,8 @@ fn resolve_phases(recipe: &Recipe, src: &Path) -> hammer_core::Result<Phases> {
|
||||
"cmake -S . -B _build -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release{flags_suffix}"
|
||||
)),
|
||||
BuildSys::Meson => Some(format!("meson setup _build --prefix=/usr{flags_suffix}")),
|
||||
// Cargo no tiene fase configure separada.
|
||||
BuildSys::Cargo | BuildSys::Make | BuildSys::Unknown => None,
|
||||
// Cargo/Go no tienen fase configure separada.
|
||||
BuildSys::Cargo | BuildSys::Go | BuildSys::Make | BuildSys::Unknown => None,
|
||||
};
|
||||
}
|
||||
if out.compile.is_none() {
|
||||
@@ -621,6 +699,23 @@ fn resolve_phases(recipe: &Recipe, src: &Path) -> hammer_core::Result<Phases> {
|
||||
))
|
||||
}
|
||||
}
|
||||
BuildSys::Go => {
|
||||
// Un solo patrón GENÉRICO para todo proyecto Go: `go install` del paquete main
|
||||
// auto-detectado (o el que fije `build.flags`). go install nombra el binario solo
|
||||
// (cmd/mlr→mlr, raíz→<módulo>) y lo deja en GOBIN=/out/usr/bin ⇒ no hace falta saber
|
||||
// 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)
|
||||
} else {
|
||||
flags.clone()
|
||||
};
|
||||
Some(format!(
|
||||
"export GOCACHE=/tmp/gocache GOPATH=/tmp/gopath GOTOOLCHAIN=local \
|
||||
CGO_ENABLED=0 GOFLAGS=-mod=vendor GOPROXY=off GOBIN=/out/usr/bin; \
|
||||
go install -trimpath -ldflags=-buildid= {pkg}"
|
||||
))
|
||||
}
|
||||
BuildSys::Unknown => None,
|
||||
};
|
||||
}
|
||||
@@ -646,6 +741,8 @@ fn resolve_phases(recipe: &Recipe, src: &Path) -> hammer_core::Result<Phases> {
|
||||
-exec cp {{}} /out/usr/bin/ \\;"
|
||||
))
|
||||
}
|
||||
// `go install` ya dejó los binarios en GOBIN=/out/usr/bin: nada que hacer.
|
||||
BuildSys::Go => Some("true".to_string()),
|
||||
BuildSys::Unknown => None,
|
||||
};
|
||||
}
|
||||
@@ -676,6 +773,33 @@ commit = "deadbeef"
|
||||
r
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn go_mod_wins_over_configure_and_derives_go_install() {
|
||||
// miller-like: go.mod + configure auxiliar ⇒ Go gana, y el compile es `go install`.
|
||||
let d = tempfile::tempdir().unwrap();
|
||||
touch(d.path(), "go.mod");
|
||||
touch(d.path(), "configure");
|
||||
std::fs::write(d.path().join("main.go"), b"package main\nfunc main(){}\n").unwrap();
|
||||
assert_eq!(detect_build_system(d.path()), BuildSys::Go);
|
||||
let p = resolve_phases(&recipe(&[]), d.path()).unwrap();
|
||||
assert!(p.configure.is_none());
|
||||
let c = p.compile.unwrap();
|
||||
assert!(c.contains("go install -trimpath"), "{c}");
|
||||
assert!(c.ends_with(" ."), "raíz ⇒ paquete `.`: {c}");
|
||||
assert_eq!(p.install.as_deref(), Some("true"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_go_main_picks_cmd_over_docs() {
|
||||
// main real en cmd/mlr + main de ejemplo roto en docs/ ⇒ elige cmd/mlr, ignora docs.
|
||||
let d = tempfile::tempdir().unwrap();
|
||||
for sub in ["cmd/mlr", "docs/example"] {
|
||||
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");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_autoconf_raw_when_only_configure_ac() {
|
||||
let d = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -32,6 +32,10 @@ pub struct NixPkg {
|
||||
/// por `cargoDeps`). Activa la plantilla Cargo de hammer (`--bin` + cp target/release).
|
||||
#[serde(default)]
|
||||
pub is_rust: bool,
|
||||
/// `true` si nixpkgs lo construye con `buildGoModule` (lo detecta por `vendorHash`/`goModules`).
|
||||
/// Emite `deps.build = ["go"]` y deja las phases a `BuildSys::Go` (go install del main auto).
|
||||
#[serde(default)]
|
||||
pub is_go: bool,
|
||||
/// El binario principal (`meta.mainProgram` de nix; p.ej. ripgrep→"rg"). Para el `--bin`.
|
||||
#[serde(default)]
|
||||
pub main_program: String,
|
||||
@@ -168,7 +172,16 @@ pub fn to_recipe_toml(pkg: &NixPkg) -> Result<String, String> {
|
||||
// ripgrep-no-jemalloc), no una dep de corpus. Las recetas Rust validadas (ripgrep/uutils) NO
|
||||
// declaran `[deps]`; emitirlas rompe `hammer build` (busca recipes/<dep>.toml). Quedan como
|
||||
// COMENTARIO de provenance (no se pierden: señalan qué C podría necesitarse).
|
||||
let deps_block = if deps.is_empty() {
|
||||
let deps_block = if pkg.is_go {
|
||||
// Go: la única build-dep es el toolchain `go` (recipes/go.toml). Los buildInputs C de nix
|
||||
// NO aplican (binario Go estático puro, CGO_ENABLED=0). Quedan como comentario de provenance.
|
||||
let prov = if deps.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("# buildInputs de nix (no usados con CGO off): {}\n", deps.join(", "))
|
||||
};
|
||||
format!("\n{prov}[deps]\nbuild = [\"go\"]\n")
|
||||
} else if deps.is_empty() {
|
||||
String::new()
|
||||
} else if pkg.is_rust {
|
||||
let list = deps.join(", ");
|
||||
@@ -198,7 +211,10 @@ pub fn to_recipe_toml(pkg: &NixPkg) -> Result<String, String> {
|
||||
|
||||
// configureFlags → una fase configure explícita (si las hay). Si no, se deja a la heurística
|
||||
// del lab. NOTA: muchos paquetes nix no son autotools (cmake/meson); esto es best-effort.
|
||||
let phases_block = if !rust_install.is_empty() {
|
||||
let phases_block = if pkg.is_go {
|
||||
// Go: sin phases. `BuildSys::Go` deriva el compile (go install del main auto-detectado).
|
||||
String::new()
|
||||
} else if !rust_install.is_empty() {
|
||||
rust_install
|
||||
} else if pkg.configure_flags.is_empty() {
|
||||
String::new()
|
||||
|
||||
@@ -33,6 +33,8 @@ json=$(nix eval $NIXFLAGS --json "${NIXPKGS}#${ATTR}" --apply '
|
||||
native_build_inputs = builtins.filter (s: s != "") (map (x: x.pname or x.name or "") (p.nativeBuildInputs or []));
|
||||
# Rust: buildRustPackage setea cargoDeps ⇒ activa la plantilla Cargo del importador.
|
||||
is_rust = builtins.hasAttr "cargoDeps" p;
|
||||
# Go: buildGoModule setea vendorHash (goModules en versiones viejas) ⇒ plantilla Go.
|
||||
is_go = builtins.hasAttr "vendorHash" p || builtins.hasAttr "goModules" p;
|
||||
main_program = p.meta.mainProgram or p.pname or "";
|
||||
}')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user