From 4291b2adeb51ee8abbdc8287b487d16b12fed450 Mon Sep 17 00:00:00 2001 From: sergio Date: Fri, 26 Jun 2026 04:34:56 -0400 Subject: [PATCH] =?UTF-8?q?Etapa=20G=20frente=20Go:=20vendoring=20integrad?= =?UTF-8?q?o=20en=20fetch=20=E2=80=94=20amfora=20construye,=20repro?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- crates/hammer-build/src/fetch.rs | 35 ++++++++++++++++++++++++++++ crates/hammer-build/src/lib.rs | 10 ++++++++ recipes/amfora.toml | 33 ++++++++++++++++++++++++++ scripts/bootstrap-devfs.sh | 33 ++++++++++++++++++++++++++ tandas/staged-2026-06-26/amfora.toml | 19 +++++++-------- 5 files changed, 120 insertions(+), 10 deletions(-) create mode 100644 recipes/amfora.toml 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 `/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"