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,
|
Meson,
|
||||||
/// `Cargo.toml`: crate o workspace Rust. Build con `cargo`, link con `zig cc`.
|
/// `Cargo.toml`: crate o workspace Rust. Build con `cargo`, link con `zig cc`.
|
||||||
Cargo,
|
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.
|
/// `Makefile` plano (o `makefile`) sin sistema generador.
|
||||||
Make,
|
Make,
|
||||||
/// No detectamos nada: la receta debe traer overrides en `[build.phases]`.
|
/// 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 {
|
fn detect_build_system(src: &Path) -> BuildSys {
|
||||||
let has = |p: &str| src.join(p).exists();
|
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
|
// 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
|
// `configure` regenerado y un `Makefile` huérfano de un build previo), preferimos el
|
||||||
// sistema "más alto" que sí sabe regenerar todo.
|
// sistema "más alto" que sí sabe regenerar todo.
|
||||||
@@ -469,6 +478,75 @@ fn detect_build_system(src: &Path) -> BuildSys {
|
|||||||
BuildSys::Unknown
|
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`
|
/// 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
|
/// 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
|
/// (`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}"
|
"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}")),
|
BuildSys::Meson => Some(format!("meson setup _build --prefix=/usr{flags_suffix}")),
|
||||||
// Cargo no tiene fase configure separada.
|
// Cargo/Go no tienen fase configure separada.
|
||||||
BuildSys::Cargo | BuildSys::Make | BuildSys::Unknown => None,
|
BuildSys::Cargo | BuildSys::Go | BuildSys::Make | BuildSys::Unknown => None,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if out.compile.is_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,
|
BuildSys::Unknown => None,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -646,6 +741,8 @@ fn resolve_phases(recipe: &Recipe, src: &Path) -> hammer_core::Result<Phases> {
|
|||||||
-exec cp {{}} /out/usr/bin/ \\;"
|
-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,
|
BuildSys::Unknown => None,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -676,6 +773,33 @@ commit = "deadbeef"
|
|||||||
r
|
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]
|
#[test]
|
||||||
fn detect_autoconf_raw_when_only_configure_ac() {
|
fn detect_autoconf_raw_when_only_configure_ac() {
|
||||||
let d = tempfile::tempdir().unwrap();
|
let d = tempfile::tempdir().unwrap();
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ pub struct NixPkg {
|
|||||||
/// por `cargoDeps`). Activa la plantilla Cargo de hammer (`--bin` + cp target/release).
|
/// por `cargoDeps`). Activa la plantilla Cargo de hammer (`--bin` + cp target/release).
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub is_rust: bool,
|
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`.
|
/// El binario principal (`meta.mainProgram` de nix; p.ej. ripgrep→"rg"). Para el `--bin`.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub main_program: String,
|
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
|
// 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
|
// 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).
|
// 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()
|
String::new()
|
||||||
} else if pkg.is_rust {
|
} else if pkg.is_rust {
|
||||||
let list = deps.join(", ");
|
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
|
// 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.
|
// 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
|
rust_install
|
||||||
} else if pkg.configure_flags.is_empty() {
|
} else if pkg.configure_flags.is_empty() {
|
||||||
String::new()
|
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 []));
|
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.
|
# Rust: buildRustPackage setea cargoDeps ⇒ activa la plantilla Cargo del importador.
|
||||||
is_rust = builtins.hasAttr "cargoDeps" p;
|
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 "";
|
main_program = p.meta.mainProgram or p.pname or "";
|
||||||
}')
|
}')
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user