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>
This commit is contained in:
2026-06-26 04:34:56 -04:00
co-authored by Claude Opus 4.8
parent 1670ebf933
commit 4291b2adeb
5 changed files with 120 additions and 10 deletions
+35
View File
@@ -234,6 +234,41 @@ pub fn vendor_cargo_deps(src: &Path) -> hammer_core::Result<()> {
Ok(()) 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`. /// Aplica los patches de la receta (relativos a `recipe.base_dir`) sobre `tree`.
pub fn apply_patches(recipe: &Recipe, tree: &Path) -> hammer_core::Result<()> { pub fn apply_patches(recipe: &Recipe, tree: &Path) -> hammer_core::Result<()> {
for p in &recipe.source.patches { for p in &recipe.source.patches {
+10
View File
@@ -212,6 +212,16 @@ pub fn build(
fetch::vendor_cargo_deps(&src_tree)?; 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 out_dir = unique_out_dir(&cfg.work_root, &recipe.name, &h)?;
let mut env: Vec<(String, String)> = Vec::new(); let mut env: Vec<(String, String)> = Vec::new();
if matches!(recipe.build.link, LinkMode::Static) { if matches!(recipe.build.link, LinkMode::Static) {
+33
View File
@@ -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"
+33
View File
@@ -28,6 +28,9 @@ ALPINE_VER="${ALPINE_VER:-3.23.4}"
ALPINE_SHA256="${ALPINE_SHA256:-85498865362aa7ebececa0d725a2f2e4db7ac4e4b2850b8df21645afa0d03ee3}" ALPINE_SHA256="${ALPINE_SHA256:-85498865362aa7ebececa0d725a2f2e4db7ac4e4b2850b8df21645afa0d03ee3}"
ZIG_VER="${ZIG_VER:-0.16.0}" ZIG_VER="${ZIG_VER:-0.16.0}"
ZIG_SHA256="${ZIG_SHA256:-70e49664a74374b48b51e6f3fdfbf437f6395d42509050588bd49abe52ba3d00}" 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 SKIP_APK=0
FORCE_ZIG=0 FORCE_ZIG=0
@@ -100,6 +103,36 @@ else
ok "zig $ZIG_VER instalado, symlink $ZIG_LINK" ok "zig $ZIG_VER instalado, symlink $ZIG_LINK"
fi 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) ------------------------------------ # --- 3. Build tools en el rootfs (apk add) ------------------------------------
if [[ $SKIP_APK -eq 1 ]]; then if [[ $SKIP_APK -eq 1 ]]; then
log "apk: --skip-apk ⇒ saltando build tools" log "apk: --skip-apk ⇒ saltando build tools"
+9 -10
View File
@@ -3,16 +3,15 @@
# Estado del frente (2026-06-26): # Estado del frente (2026-06-26):
# ✅ toolchain: recipes/go.toml (1.26.4, binario estático, corre en el sandbox musl, # ✅ 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). # 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 # ✅ vendoring INTEGRADO en fetch.rs (`vendor_go_deps`, análogo a vendor_cargo_deps):
# -mod=vendor GOPROXY=off` produce un binario ESTÁTICO que corre (Amfora v1.11.0). # si el árbol trae go.mod, hammer corre `go mod vendor` en el host (red) y el
# ⬜ FALTA: el source de hammer es repo+commit (checkout pelado, sin vendor/). El # sandbox compila offline con -mod=vendor. El `go` del host lo provee el lab
# sandbox de build es OFFLINE (reproducibilidad) ⇒ `go build` no puede bajar # (.dev-fs/tools/go). NO hace falta tarball-al-mirror: se vendorea in-situ como Rust.
# módulos de proxy.golang.org. Igual que Rust vendorea crates, el frente Go # ✅ VALIDADO end-to-end: esta receta (repo+commit) → binario ESTÁTICO que corre
# necesita que el IMPORTADOR (con red) corra `go mod vendor` y empaquete el # (Amfora v1.11.0), sellado b3:0a3f63bc…
# 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.
# #
# 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" name = "amfora"
version = "1.11.0" version = "1.11.0"
@@ -30,5 +29,5 @@ build = ["go"]
[build.phases] [build.phases]
configure = "true" 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" install = "true"