diff --git a/crates/hammer-build/src/lib.rs b/crates/hammer-build/src/lib.rs index 2b3524fb..906bb1f9 100644 --- a/crates/hammer-build/src/lib.rs +++ b/crates/hammer-build/src/lib.rs @@ -217,9 +217,15 @@ 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() || src_tree.join("go.work").is_file() { + // El módulo Go puede estar en un subdir (recipe `build.subdir`, p.ej. dolt→go); vendoreamos ahí. + let go_root = if eff_recipe.build.subdir.is_empty() { + src_tree.clone() + } else { + src_tree.join(&eff_recipe.build.subdir) + }; + if go_root.join("go.mod").is_file() || go_root.join("go.work").is_file() { tracing::info!("go: vendoreando módulos para build offline"); - fetch::vendor_go_deps(&src_tree)?; + fetch::vendor_go_deps(&go_root)?; } let out_dir = unique_out_dir(&cfg.work_root, &recipe.name, &h)?; @@ -612,7 +618,14 @@ fn cargo_linker_env_var(triple: &str) -> String { /// ganan; el resto lo deriva la heurística según el sistema de build detectado. fn resolve_phases(recipe: &Recipe, src: &Path) -> hammer_core::Result { let mut out = recipe.build.phases.clone(); - let bs = detect_build_system(src); + // Raíz efectiva del módulo Go: `/` si la receta lo fija (dolt→go, csvtk→subdir), + // si no `src`. detect/detect_go_main operan ahí; el compile hace `cd ` primero. + let go_src = if recipe.build.subdir.is_empty() { + src.to_path_buf() + } else { + src.join(&recipe.build.subdir) + }; + let bs = detect_build_system(&go_src); let flags = recipe.build.flags.join(" "); let flags_suffix = if flags.is_empty() { String::new() } else { format!(" {flags}") }; @@ -746,10 +759,16 @@ fn resolve_phases(recipe: &Recipe, src: &Path) -> hammer_core::Result { // 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, &recipe.name) + detect_go_main(&go_src, &recipe.name) } else { flags.clone() }; + // Si el módulo está en subdir, el compile entra ahí primero (el sandbox arranca en /src). + let cd_prefix = if recipe.build.subdir.is_empty() { + String::new() + } else { + format!("cd {} && ", recipe.build.subdir) + }; // GOCACHE/GOTMPDIR en /src (disk-backed, per-build, lo limpia el watchdog), NO en // /tmp: el sandbox monta `--tmpfs /tmp` (~½ RAM); un proyecto Go ENORME (telegraf, // vault) desborda ese tmpfs con sus objetos compilados + el temp del linker al mapear @@ -770,7 +789,7 @@ fn resolve_phases(recipe: &Recipe, src: &Path) -> hammer_core::Result { ("CGO_ENABLED=0", "-ldflags=-buildid=") }; Some(format!( - "export GOCACHE=/src/.gocache GOTMPDIR=/src/.gotmp GOPATH=/tmp/gopath \ + "{cd_prefix}export GOCACHE=/src/.gocache GOTMPDIR=/src/.gotmp GOPATH=/tmp/gopath \ GOTOOLCHAIN=local {cgo_env} GOFLAGS=-mod=vendor GOPROXY=off \ GOBIN=/out/usr/bin; mkdir -p /src/.gotmp; \ go install -trimpath {ldflags} {pkg}" @@ -833,6 +852,23 @@ commit = "deadbeef" r } + #[test] + fn go_subdir_compiles_in_subdir() { + // dolt-like: go.mod en subdir `go/`. Con build.subdir="go" el lab detecta Go ahí y el compile + // entra al subdir primero. + let d = tempfile::tempdir().unwrap(); + std::fs::create_dir_all(d.path().join("go/cmd/dolt")).unwrap(); + touch(d.path(), "go/go.mod"); + std::fs::write(d.path().join("go/cmd/dolt/main.go"), b"package main\nfunc main(){}\n").unwrap(); + let mut r = recipe(&[]); + r.name = "dolt".into(); + r.build.subdir = "go".into(); + let p = resolve_phases(&r, d.path()).unwrap(); + let c = p.compile.unwrap(); + assert!(c.starts_with("cd go && "), "entra al subdir: {c}"); + assert!(c.contains("go install -trimpath") && c.trim_end().ends_with("./cmd/dolt"), "{c}"); + } + #[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`. diff --git a/crates/hammer-core/src/recipe.rs b/crates/hammer-core/src/recipe.rs index 9c54c18a..75ee9bea 100644 --- a/crates/hammer-core/src/recipe.rs +++ b/crates/hammer-core/src/recipe.rs @@ -111,6 +111,11 @@ pub struct Build { /// esas recetas en el sandbox. No-op fuera de BuildSys::Go. #[serde(default)] pub cgo: bool, + /// Subdirectorio donde vive el módulo Go cuando NO está en la raíz del repo (p.ej. dolt → `go`, + /// csvtk → su subdir). El lab vendorea, detecta el main y compila DENTRO de `/`. + /// Vacío = raíz (lo normal). Sólo aplica a BuildSys::Go. + #[serde(default)] + pub subdir: String, /// Overrides explícitos de las fases del build. Lo no especificado se decide por /// heurística en el lab (ver `docs/02-build-lab.md` §4). #[serde(default)]