diff --git a/crates/hammer-build/src/fetch.rs b/crates/hammer-build/src/fetch.rs index a7ab8dd2..873efa77 100644 --- a/crates/hammer-build/src/fetch.rs +++ b/crates/hammer-build/src/fetch.rs @@ -246,26 +246,38 @@ pub fn vendor_cargo_deps(src: &Path) -> hammer_core::Result<()> { /// /// Un proyecto sin deps externas produce un `vendor/` vacío (o ninguno): inocuo. pub fn vendor_go_deps(src: &Path) -> hammer_core::Result<()> { - let output = Command::new("go") - .current_dir(src) - .args(["mod", "vendor"]) + // Proyecto WORKSPACE (multimódulo): si el árbol trae `go.work`, `go mod vendor` con GOWORK=off + // vendorea SÓLO el módulo raíz → el build (que ve todo el workspace) se queja de "inconsistent + // vendoring". `go work vendor` (go 1.22+) produce un `vendor/` consistente para TODO el workspace. + // Destraba la clase go.work (go-mockery, k3d, git-town, csvtk…). Sin go.work, el camino clásico + // `go mod vendor` con GOWORK=off (ignora cualquier go.work espurio y vendorea el raíz). + let has_work = src.join("go.work").is_file(); + let (verb, label): ([&str; 2], &str) = if has_work { + (["work", "vendor"], "go work vendor") + } else { + (["mod", "vendor"], "go mod vendor") + }; + let mut cmd = Command::new("go"); + cmd.current_dir(src) + .args(verb) .env("GOFLAGS", "-mod=mod") - .env("GOTOOLCHAIN", "local") - // `go mod vendor` aborta en modo workspace (si el árbol trae go.work). Lo apagamos para - // vendorear contra el go.mod del módulo raíz (que es lo que el build compila). - .env("GOWORK", "off") + .env("GOTOOLCHAIN", "local"); + if !has_work { + cmd.env("GOWORK", "off"); + } + let output = cmd .stdout(Stdio::inherit()) .stderr(Stdio::inherit()) .output() .map_err(|e| { hammer_core::Error::Other(anyhow::anyhow!( - "spawn go mod vendor: {e} (¿está `go` en el PATH del host? lo provee el lab, \ + "spawn {label}: {e} (¿está `go` en el PATH del host? lo provee el lab, \ cf. scripts/bootstrap-devfs.sh / vps-setup.sh)" )) })?; if !output.status.success() { return Err(hammer_core::Error::Other(anyhow::anyhow!( - "go mod vendor falló (exit {:?})", + "{label} falló (exit {:?})", output.status.code() ))); } diff --git a/crates/hammer-build/src/lib.rs b/crates/hammer-build/src/lib.rs index a34d3f36..f7ea30c7 100644 --- a/crates/hammer-build/src/lib.rs +++ b/crates/hammer-build/src/lib.rs @@ -217,7 +217,7 @@ pub fn build( // aporta `deps.build = ["go"]` (recipes/go.toml) DENTRO del sandbox; el vendoring usa el `go` // del host (lo provee el lab). No hay BuildSys::Go: las recetas Go fijan el compile en // `[build.phases]` (ver tandas/staged-2026-06-26/amfora.toml como plantilla). - if src_tree.join("go.mod").is_file() { + if src_tree.join("go.mod").is_file() || src_tree.join("go.work").is_file() { tracing::info!("go: vendoreando módulos para build offline"); fetch::vendor_go_deps(&src_tree)?; } @@ -443,10 +443,11 @@ 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") { + // `go.mod`/`go.work` PRIMERO: un módulo Go suele traer también un `configure`/`Makefile` AUXILIAR + // (man pages, completions, packaging), pero el binario se hace con `go build`. Ningún paquete + // C/Rust del corpus trae go.mod. `go.work` cubre los WORKSPACE multimódulo cuya RAÍZ no tiene + // go.mod (sólo go.work + submódulos) — sin esto caían a Makefile/Unknown. + if has("go.mod") || has("go.work") { return BuildSys::Go; } // El orden importa: cuando coexisten varios (un tarball release de autotools trae el @@ -836,6 +837,16 @@ commit = "deadbeef" assert_eq!(p.install.as_deref(), Some("true")); } + #[test] + fn detect_go_when_only_go_work() { + // workspace multimódulo sin go.mod en la raíz (sólo go.work) ⇒ sigue siendo Go (antes caía a + // Makefile/Unknown). El vendoring usará `go work vendor`. + let d = tempfile::tempdir().unwrap(); + touch(d.path(), "go.work"); + touch(d.path(), "Makefile"); // un Makefile auxiliar no debe ganarle a Go + assert_eq!(detect_build_system(d.path()), BuildSys::Go); + } + #[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.