> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…
Destraba proyectos Go cuyo módulo no está en la raíz (dolt→go, csvtk→subdir): vendor_go_deps corre en
el subdir, detect_build_system/detect_go_main operan ahí, y el compile hace 'cd <subdir>' primero.
Test go_subdir_compiles_in_subdir.
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<Phases> {
let mut out = recipe.build.phases.clone();
- let bs = detect_build_system(src);
+ // Raíz efectiva del módulo Go: `<src>/<subdir>` si la receta lo fija (dolt→go, csvtk→subdir),
+ // si no `src`. detect/detect_go_main operan ahí; el compile hace `cd <subdir>` 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<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, &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<Phases> {
("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 `<src>/<subdir>`.
+ /// 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)]