Etapa G: frontera go.mod-en-subdir — knob [build] subdir (vendor/detect/compile en src/<subdir>)

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.
This commit is contained in:
2026-06-29 15:06:35 -04:00
parent 49ce5cf051
commit c583c4d679
2 changed files with 46 additions and 5 deletions
+41 -5
View File
@@ -217,9 +217,15 @@ pub fn build(
// aporta `deps.build = ["go"]` (recipes/go.toml) DENTRO del sandbox; el vendoring usa el `go` // 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 // 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). // `[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"); 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)?; 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. /// 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> { fn resolve_phases(recipe: &Recipe, src: &Path) -> hammer_core::Result<Phases> {
let mut out = recipe.build.phases.clone(); 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 = recipe.build.flags.join(" ");
let flags_suffix = if flags.is_empty() { String::new() } else { format!(" {flags}") }; 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 // el nombre. Offline+reproducible: vendor (vendor_go_deps) + GOPROXY=off + -trimpath
// -buildid=. El toolchain `go` llega por deps.build=["go"] (overlay). // -buildid=. El toolchain `go` llega por deps.build=["go"] (overlay).
let pkg = if recipe.build.flags.is_empty() { let pkg = if recipe.build.flags.is_empty() {
detect_go_main(src, &recipe.name) detect_go_main(&go_src, &recipe.name)
} else { } else {
flags.clone() 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 // 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, // /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 // 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=") ("CGO_ENABLED=0", "-ldflags=-buildid=")
}; };
Some(format!( 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 \ GOTOOLCHAIN=local {cgo_env} GOFLAGS=-mod=vendor GOPROXY=off \
GOBIN=/out/usr/bin; mkdir -p /src/.gotmp; \ GOBIN=/out/usr/bin; mkdir -p /src/.gotmp; \
go install -trimpath {ldflags} {pkg}" go install -trimpath {ldflags} {pkg}"
@@ -833,6 +852,23 @@ commit = "deadbeef"
r 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] #[test]
fn go_mod_wins_over_configure_and_derives_go_install() { fn go_mod_wins_over_configure_and_derives_go_install() {
// miller-like: go.mod + configure auxiliar ⇒ Go gana, y el compile es `go install`. // miller-like: go.mod + configure auxiliar ⇒ Go gana, y el compile es `go install`.
+5
View File
@@ -111,6 +111,11 @@ pub struct Build {
/// esas recetas en el sandbox. No-op fuera de BuildSys::Go. /// esas recetas en el sandbox. No-op fuera de BuildSys::Go.
#[serde(default)] #[serde(default)]
pub cgo: bool, 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 /// 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). /// heurística en el lab (ver `docs/02-build-lab.md` §4).
#[serde(default)] #[serde(default)]