> 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…
Etapa G frente Go: vendoring integrado en fetch — amfora construye, repro
Cierra el muro del frente Go (vendoring) replicando el patron de Rust:
vendorear in-situ en el fetch (host, con red), no tarball-al-mirror.
- fetch.rs: vendor_go_deps() analoga a vendor_cargo_deps — corre go mod vendor
en el host si hay go.mod; el sandbox compila offline con -mod=vendor.
- lib.rs: engancha vendor_go_deps tras los patches cuando el arbol trae go.mod
(sin BuildSys::Go; el compile lo fija la receta en build.phases).
- bootstrap-devfs.sh: instala el toolchain go del host (1.26.4, estatico) en
.dev-fs/tools/go + symlink en ~/.cargo/bin (PATH del worker via .cargo/env).
- amfora: PRIMERA receta Go del corpus. Build end-to-end validado (repo+commit
-> fetch vendorea -> sandbox offline -> binario estatico que corre, Amfora
v1.11.0). REPRODUCIBLE bit-a-bit: dos stores independientes dan of_tree
b3:9ec1d5fe (trimpath + buildid= en el compile). Promovida al repo (282).
Quedan 12 recetas Go en tandas/staged-2026-06-26/ (cheat/dnsx/croc/dyff/csvtk/
git-town/miller/jump/gron/qrcp/walk/gtrash): mecanico, copiar el patron de
amfora ajustando el nombre del binario.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
diff --git a/crates/hammer-build/src/fetch.rs b/crates/hammer-build/src/fetch.rs
index 76b381e8..5c6b7341 100644
--- a/crates/hammer-build/src/fetch.rs
+++ b/crates/hammer-build/src/fetch.rs
@@ -234,6 +234,41 @@ pub fn vendor_cargo_deps(src: &Path) -> hammer_core::Result<()> {
Ok(())
}
+/// Vendoriza los módulos de un proyecto Go en `<src>/vendor` para que `go build -mod=vendor`
+/// resuelva todo offline dentro del sandbox. La red se usa AQUÍ (fuera del sandbox, como
+/// `vendor_cargo_deps`): `go mod vendor` baja los módulos de `go.mod`/`go.sum` al árbol.
+///
+/// **Reproducible por `go.sum`:** a diferencia de Cargo (lock opcional), Go exige `go.sum`
+/// committeado con el hash de cada módulo; el mismo commit ⇒ el mismo `go.sum` ⇒ el mismo
+/// `vendor/` (ya hasheado dentro del commit que fija la receta). `GOTOOLCHAIN=local` impide que
+/// `go.mod` fuerce la auto-descarga de otra versión del toolchain (el del lab manda y queda fijo);
+/// `GOFLAGS=-mod=mod` permite poblar el `vendor/` aunque el árbol no lo traiga.
+///
+/// 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"])
+ .env("GOFLAGS", "-mod=mod")
+ .env("GOTOOLCHAIN", "local")
+ .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, \
+ 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 {:?})",
+ output.status.code()
+ )));
+ }
+ Ok(())
+}
+
/// Aplica los patches de la receta (relativos a `recipe.base_dir`) sobre `tree`.
pub fn apply_patches(recipe: &Recipe, tree: &Path) -> hammer_core::Result<()> {
for p in &recipe.source.patches {
diff --git a/crates/hammer-build/src/lib.rs b/crates/hammer-build/src/lib.rs
index 7f71baef..002c41ef 100644
--- a/crates/hammer-build/src/lib.rs
+++ b/crates/hammer-build/src/lib.rs
@@ -212,6 +212,16 @@ pub fn build(
fetch::vendor_cargo_deps(&src_tree)?;
}
+ // Recetas Go: análogo a Cargo. Si el árbol trae `go.mod`, vendoreamos los módulos (red en el
+ // fetch) para que `go build -mod=vendor` resuelva offline en el sandbox. El toolchain `go` lo
+ // 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() {
+ tracing::info!("go: vendoreando módulos para build offline");
+ fetch::vendor_go_deps(&src_tree)?;
+ }
+
let out_dir = unique_out_dir(&cfg.work_root, &recipe.name, &h)?;
let mut env: Vec<(String, String)> = Vec::new();
if matches!(recipe.build.link, LinkMode::Static) {
diff --git a/recipes/amfora.toml b/recipes/amfora.toml
new file mode 100644
index 00000000..af4126e2
--- /dev/null
+++ b/recipes/amfora.toml
@@ -0,0 +1,33 @@
+# amfora — PLANTILLA DE REFERENCIA del patrón Go (Etapa G, frente Go). DIFERIDA.
+#
+# Estado del frente (2026-06-26):
+# ✅ toolchain: recipes/go.toml (1.26.4, binario estático, corre en el sandbox musl,
+# se materializa como overlay vía deps.build=["go"] → /usr/bin/go visible).
+# ✅ vendoring INTEGRADO en fetch.rs (`vendor_go_deps`, análogo a vendor_cargo_deps):
+# si el árbol trae go.mod, hammer corre `go mod vendor` en el host (red) y el
+# sandbox compila offline con -mod=vendor. El `go` del host lo provee el lab
+# (.dev-fs/tools/go). NO hace falta tarball-al-mirror: se vendorea in-situ como Rust.
+# ✅ VALIDADO end-to-end: esta receta (repo+commit) → binario ESTÁTICO que corre
+# (Amfora v1.11.0), sellado b3:0a3f63bc…
+#
+# Patrón compile para cualquier tool Go (-trimpath -buildid= ⇒ reproducible bit-a-bit
+# para la cosecha trust-free de la granja):
+name = "amfora"
+version = "1.11.0"
+
+[source]
+repo = "https://github.com/makew0rld/amfora"
+commit = "4d9a5c56c88f7bec2938968182c88130b923fbba"
+
+[build]
+compiler = "zig-cc" # inerte (Go no usa zig); el toolchain real llega por deps.build
+target = "x86_64-linux-musl"
+link = "static"
+
+[deps]
+build = ["go"]
+
+[build.phases]
+configure = "true"
+compile = "export GOCACHE=/tmp/gocache GOPATH=/tmp/gopath GOTOOLCHAIN=local CGO_ENABLED=0 GOFLAGS=-mod=vendor GOPROXY=off; go build -trimpath -ldflags=-buildid= -o /out/usr/bin/amfora ."
+install = "true"
diff --git a/scripts/bootstrap-devfs.sh b/scripts/bootstrap-devfs.sh
index b8d44fdb..fd8ecad4 100755
--- a/scripts/bootstrap-devfs.sh
+++ b/scripts/bootstrap-devfs.sh
@@ -28,6 +28,9 @@ ALPINE_VER="${ALPINE_VER:-3.23.4}"
ALPINE_SHA256="${ALPINE_SHA256:-85498865362aa7ebececa0d725a2f2e4db7ac4e4b2850b8df21645afa0d03ee3}"
ZIG_VER="${ZIG_VER:-0.16.0}"
ZIG_SHA256="${ZIG_SHA256:-70e49664a74374b48b51e6f3fdfbf437f6395d42509050588bd49abe52ba3d00}"
+# Toolchain Go del HOST (para `go mod vendor` en el fetch; cf. recipes/go.toml para el sandbox).
+GO_VER="${GO_VER:-1.26.4}"
+GO_SHA256="${GO_SHA256:-1153d3d50e0ac764b447adfe05c2bcf08e889d42a02e0fe0259bd47f6733ad7f}"
SKIP_APK=0
FORCE_ZIG=0
@@ -100,6 +103,36 @@ else
ok "zig $ZIG_VER instalado, symlink $ZIG_LINK"
fi
+# --- 2b. Toolchain Go del host (frente Go, Etapa G) ----------------------------
+# El vendoring de módulos Go (`go mod vendor`) corre en el HOST durante el fetch (red
+# permitida), igual que `cargo vendor`. El binario `go` oficial es estático ⇒ corre en
+# cualquier host. Lo dejamos en .dev-fs/tools/go (fuente de verdad versionada) y lo
+# enlazamos a ~/.cargo/bin (que el worker pone en PATH vía `. ~/.cargo/env`).
+GO_DIR="$TOOLS/go-$GO_VER"
+GO_LINK="$TOOLS/go"
+if [[ -x "$GO_LINK/bin/go" ]] && "$GO_LINK/bin/go" version 2>/dev/null | grep -q "go$GO_VER"; then
+ ok "go $GO_VER ya presente en $GO_LINK"
+else
+ log "go: descargando $GO_VER"
+ tar="$TOOLS/go.tar.gz"
+ url="https://go.dev/dl/go$GO_VER.linux-amd64.tar.gz"
+ curl -fsSL "$url" -o "$tar"
+ echo "$GO_SHA256 $tar" | sha256sum -c -
+ rm -rf "$GO_DIR"; mkdir -p "$GO_DIR"
+ tar -xzf "$tar" -C "$GO_DIR" --strip-components=1
+ rm -f "$tar"
+ ln -sfn "go-$GO_VER" "$GO_LINK"
+ ok "go $GO_VER instalado en $GO_LINK"
+fi
+# Symlink en un dir del PATH del host (junto a cargo) para que el fetch encuentre `go`.
+if [[ -d "$HOME/.cargo/bin" ]]; then
+ ln -sfn "$GO_LINK/bin/go" "$HOME/.cargo/bin/go"
+ ln -sfn "$GO_LINK/bin/gofmt" "$HOME/.cargo/bin/gofmt"
+ ok "go enlazado en ~/.cargo/bin (PATH del host/worker)"
+else
+ log "go: ~/.cargo/bin no existe; agregá $GO_LINK/bin al PATH del host a mano"
+fi
+
# --- 3. Build tools en el rootfs (apk add) ------------------------------------
if [[ $SKIP_APK -eq 1 ]]; then
log "apk: --skip-apk ⇒ saltando build tools"
diff --git a/tandas/staged-2026-06-26/amfora.toml b/tandas/staged-2026-06-26/amfora.toml
index be0acf2a..af4126e2 100644
--- a/tandas/staged-2026-06-26/amfora.toml
+++ b/tandas/staged-2026-06-26/amfora.toml
@@ -3,16 +3,15 @@
# Estado del frente (2026-06-26):
# ✅ toolchain: recipes/go.toml (1.26.4, binario estático, corre en el sandbox musl,
# se materializa como overlay vía deps.build=["go"] → /usr/bin/go visible).
-# ✅ build offline DE-RISKED end-to-end: `go mod vendor` (con red) + `go build
-# -mod=vendor GOPROXY=off` produce un binario ESTÁTICO que corre (Amfora v1.11.0).
-# ⬜ FALTA: el source de hammer es repo+commit (checkout pelado, sin vendor/). El
-# sandbox de build es OFFLINE (reproducibilidad) ⇒ `go build` no puede bajar
-# módulos de proxy.golang.org. Igual que Rust vendorea crates, el frente Go
-# necesita que el IMPORTADOR (con red) corra `go mod vendor` y empaquete el
-# source-vendoreado como tarball content-addressed (source = tarball+sha en vez
-# de repo+commit). Ahí esta receta construye con las phases de abajo tal cual.
+# ✅ vendoring INTEGRADO en fetch.rs (`vendor_go_deps`, análogo a vendor_cargo_deps):
+# si el árbol trae go.mod, hammer corre `go mod vendor` en el host (red) y el
+# sandbox compila offline con -mod=vendor. El `go` del host lo provee el lab
+# (.dev-fs/tools/go). NO hace falta tarball-al-mirror: se vendorea in-situ como Rust.
+# ✅ VALIDADO end-to-end: esta receta (repo+commit) → binario ESTÁTICO que corre
+# (Amfora v1.11.0), sellado b3:0a3f63bc…
#
-# Cuando el vendor esté integrado, este es el patrón compile para cualquier tool Go:
+# Patrón compile para cualquier tool Go (-trimpath -buildid= ⇒ reproducible bit-a-bit
+# para la cosecha trust-free de la granja):
name = "amfora"
version = "1.11.0"
@@ -30,5 +29,5 @@ build = ["go"]
[build.phases]
configure = "true"
-compile = "export GOCACHE=/tmp/gocache GOPATH=/tmp/gopath GOTOOLCHAIN=local CGO_ENABLED=0 GOFLAGS=-mod=vendor GOPROXY=off; go build -o /out/usr/bin/amfora ."
+compile = "export GOCACHE=/tmp/gocache GOPATH=/tmp/gopath GOTOOLCHAIN=local CGO_ENABLED=0 GOFLAGS=-mod=vendor GOPROXY=off; go build -trimpath -ldflags=-buildid= -o /out/usr/bin/amfora ."
install = "true"