RESULTADO, con los tres criterios medidos a la vez sobre las dos recetas que divergían:
bison 6M → 3M · 0 ficheros vacíos · 0 secciones .debug_ · «bison (GNU Bison) 3.8.2»
appstream 56M → 18M · 0 ficheros vacíos · 0 secciones .debug_ · «AppStream version: 1.0.5»
y las DOS pasan de DIVERGIR a REPRODUCIR.
O sea que un solo cambio recupera espacio Y cierra la fuga de reproducibilidad, como predijo el
§1.bis. Pero se llegó ahí después de tres errores que conviene dejar escritos.
🧨 1. `zig objcopy --strip-debug X X` (mismo fichero de entrada y salida) TRUNCA EL FICHERO A 0
BYTES. Destruyó los artefactos del piloto — y lo grave es que LOS TRES INDICADORES DECÍAN QUE
IBA BIEN: el tamaño cayó 84% (porque los ficheros quedaron vacíos), `why-differs` dijo REPRODUCE
(porque dos árboles vacíos son idénticos) y no quedaban secciones .debug_ (porque no quedaba
ninguna sección). Se cazó al EJECUTAR el binario: 0 bytes.
⇒ La verificación de un artefacto tiene que incluir que SIGA FUNCIONANDO, no sólo que pese menos
y reproduzca. Un artefacto vacío cumple las dos y no sirve para nada. Es la lección de esta
campaña aplicada a la campaña misma: una métrica que parece éxito.
2. Al arreglarlo con fichero temporal, el strip pasó a ser un NO-OP SILENCIOSO: los binarios
quedaban intactos y el tamaño no bajaba, porque no se pudo confirmar que `zig objcopy` acepte
`--strip-debug`. Cambiado al `strip` de binutils, que sí funciona, a costa de declarar la dep.
⇒ Preferible una dep explícita que funciona a una comodidad que no se sabe si hace algo.
3. Con el strip real, apareció una fuga NUEVA: los artefactos seguían divergiendo, ahora por la
CABECERA `ar` de los `.a` — `strip` los reescribe con los timestamps de cada corrida. Lo nombró
`why-differs` exacto («archivar en modo determinista»). Arreglado con `strip -D`
(= --enable-deterministic-archives). ⇒ Arreglar media causa deja el invariante igual de roto: el
debug ya no divergía y el archivo sí.
DISEÑO: `strip_debug` es un campo de la receta que ENTRA en `hash_inputs` y sólo si está fijado
(mismo patrón que `zig_version`). Las dos mitades importan y están clavadas en un test: si no
entrara, el lab cambiaría el contenido del artefacto sin mover el hash y el store MENTIRÍA; y al
entrar sólo si está fijado, se despliega receta a receta sin re-hashear las 1161 de golpe —
verificado: con el campo añadido al código, los 1161 hashes existentes NO se movieron.
Va como paso del lab y no en la fase install de cada receta porque 383 de las 1161 no tienen
install explícita: meterlo receta a receta obligaría a escribir a mano ese install por defecto en
las 383, con riesgo de no clavarlo exacto.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rsvg-2.0 librsvg b3:351f4658
IBus-1.0 ibus b3:d4d3750a
**1. PERILLA NUEVA DEL LAB: `[source] cargo_vendor`.** `detect_build_system` asume UN
sistema de build por árbol, y librsvg genuinamente tiene dos: `configure.ac` gana por
precedencia pero su Makefile llama a `cargo build`, que dentro del sandbox hermético
no tiene red. La perilla fuerza el vendoreo (que ocurre en el fetch, donde sí hay red)
sin tocar la detección. **No entra al ArtifactHash** —decide de dónde salen las deps,
no cuáles: eso lo fija el Cargo.lock, ya bajo el sha256 de la fuente— así que se puede
prender en una receta ya sellada sin re-hashear nada. Con test, y verificado en vivo:
libelogind no movió su hash tras el cambio.
**2. LA DEUDA DEL UNWINDER, PAGADA.** librsvg moría en
`undefined reference: _Unwind_DeleteException`. No es de librsvg: es de la `std` de
rustc, que trae landing pads y espera el runtime que en glibc vive en libgcc_s. Es la
deuda que el corpus arrastra desde matar-gcc —«las 12 recetas Rust son UN problema, no
12»— y el remedio estaba a mano: **zig empaqueta la libunwind de LLVM** y exporta los
`_Unwind_*` (verificado con nm). Alcanza `LIBS=-lunwind`. Va por LIBS y no por LDFLAGS
porque autotools pone LIBS al FINAL de la línea de enlace, que es donde tiene que ir
una librería que resuelve símbolos indefinidos.
librsvg va en 2.58.5 y no 2.59+: en 2.59 cambió a meson + cargo-c, y cargo-cbuild
enlaza el crate `cargo` entero (libgit2, libssh2, libcurl, openssl) — una campaña
propia por un binario que sólo corre en el constructor. 2.58.5 produce el mismo
Rsvg-2.0. Mismo criterio que gnome-desktop 44.5.
**3. `x11-compose-data`, y es una tensión que vale la pena tener escrita.** ibus COMPILA
la tabla de teclas muertas de X11 dentro de libibus (Makefile.am:310, incondicional, sin
`--disable-`). Sin datos no construye. Los datos viven en el tarball de libX11 por
historia, no por necesidad técnica: son 5192 líneas de reglas. La receta extrae SÓLO los
ficheros de locale reproduciendo la regla de upstream (`cpprules.in`: cpp crudo +
CPP_SED_MAGIC literal) y no compila una línea de X11. Un escritorio Wayland-only sigue
necesitando la tabla de composición del mundo Unix.
Gotchas de ibus, los tres medidos: su ayuda MIENTE (`--enable-gtk4`/`--enable-wayland`
sugieren default apagado; el default es `yes`); `--disable-emoji-dict` NO apaga
`--disable-unicode-dict`; y con `--disable-wayland` el build muere igual porque
`tools/main.c` llama wl_display_* sin `#ifdef` mientras WAYLAND_LIBS sólo se agrega si
la opción está prendida — bug de upstream en su propia configuración sin Wayland.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
El wrapper anterior (quita -static con -shared) destrabó la extensión Python de
g-i, pero g-i es dinámico en 3 puntos y el fallo saltó a los otros dos:
"error: using shared libraries requires dynamic linking" en (a) las tools que
linkean libgirepository-1.0.so y (b) el dumper del scanner con -Wl,--export-dynamic.
Generalizo la regla del wrapper: -static se quita ante cualquier marcador de link
dinámico — -shared, -rdynamic, --export-dynamic, o un shared object (.so/.so.N) en
la línea. Todos son casos donde -static es contradictorio de por sí. El link
estático normal (exe/.a sin marcadores) y el compile puro quedan byte-idénticos.
Verificado contra las 2 líneas reales que fallaban + 2 casos de no-regresión.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Las extensiones Python (python.extension_module → shared_module) son .so que el
intérprete del sandbox debe dlopen; intrínsecamente dinámicas. El harness inyecta
LDFLAGS=-static para link=static, y meson/autotools lo aplican a TODOS los targets
por igual ⇒ el .so sale ET_EXEC y dlopen da "Exec format error" (el muro de
gobject-introspection: _giscanner.cpython-312.so no cargaba).
Fix estructural (elegido sobre flip-a-dynamic): ensure_layout materializa en el
rootfs dos wrappers (hammer-zig-cc/-cxx sobre zig cc/c++) que QUITAN -static SÓLO
cuando el link lleva -shared. -static+-shared es contradictorio ⇒ correcto por
construcción: transparente (byte-idéntico) para todo link normal, arregla TODA
extensión Python futura, no sólo g-i.
Hash-safe: el ArtifactHash se deriva de inputs (receta+deps), no de los bytes ⇒
no re-hashea ninguno de los 700+ sellados (cache-hit intacto); sólo cambia builds
nuevos con .so dinámicos. Verificado: rota positional params, preserva orden y
espacios; transparente sin -shared.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
merge_deps_layer funde las deps en UNA capa overlay por hardlinks (cp -al) para
no desbordar el lowerdir de overlayfs (tope ~4096B por página). Pero calculaba
la ruta del merge en deps.parent().parent() = <store>/../.dmerge, asumiendo que
el store está en <base>/store del MISMO fs. En el worker el store es un VOLUMEN
bind-montado (/opt/hammer/store en /dev/sdb, /opt/hammer en /dev/sda1) ⇒ el merge
caía en la raíz, cp -al fallaba cross-device, y el fallback apilaba las ~50 deps
de kio directo → lowerdir 5315B > 4096B → "bwrap: Can't make overlay mount" →
kio (EL keystone, gatea 36) imposible de construir. Por eso estaba clavado.
Fix de una línea: el merge va en el padre INMEDIATO de las deps (= el store
mismo), garantizado mismo fs. Confirmado en el worker: cp -al a /opt/hammer/.dmerge
falla "Invalid cross-device link"; a /opt/hammer/store/.dmerge funciona.
En el laptop andaba por casualidad (store y su padre en el mismo fs).
`.dmerge` (dot-prefix) es invisible para el store lookup/build-state como .times.
cosecha excluye /.dmerge del rsync (es scratch de hardlinks; sin -H rsync lo
expandiría a copias reales).
Descubierto levantando el worker para poblar tiempos: yupana keystones apuntó a
kio, la campaña falló ahí, y el radio de lowerdirs (5315B) destapó la causa. Una
"sorpresa de entorno" (infra del sandbox, fuera del grafo) que el frente de
timing sacó a la luz.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Queda una carrera abierta que el lock de script NO cierra: `build-farm.sh` corre `xargs -P2` y dos
recetas de la misma cola que compartan dep se pisan igual, porque `fetch` nombra el árbol de forma
determinista y sin nada del constructor.
El fondo no es "falta un lock", es que el árbol cumple DOS papeles que se contradicen bajo
concurrencia: caché direccionada por contenido (clave = nombre+sha, re-extraer es caro) y workspace
mutable de build (lib.rs lo parchea, aísla el workspace Cargo y vendorea EN EL SITIO, y después el
sandbox lo bind-montea). Por eso un lock alrededor de la extracción parece correcto y NO lo es: un
segundo proceso puede borrar un árbol que un bwrap ya está compilando.
El ADR deja las tres salidas (lock por árbol durante todo el build / árbol privado por build /
separar caché inmutable + copia privada) con su contra concreta cada una —incluida que el worker
corre ext4, sin reflink, así que la copia de la opción C es real— y los tres números que habría que
medir para elegir en vez de opinar.
Se ancla en los dos sitios donde alguien va a chocar: el ADR indexado en docs/README.md (de paso se
agregan 0009-0011, que faltaban) y un comentario en el propio `fetch_tarball` que dice explícitamente
que no se arregle a medias.
Va como PENDIENTE y no decidido a propósito: hay otro agente sobre este repo y esto es lo que evita
que la carrera se "arregle" de una forma que parece bien y deja el bug.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Atar el lab al PADRE DEL STORE era una suposición oculta y cara. Al anclar el store de un worker a
un volumen persistente, el lab pasó a buscarse en /mnt/cosecha/.dev-fs —que no existe— y toda
receta con `zig_version` murió con "no encuentro el ejecutable zig en …", un mensaje que apunta al
lugar equivocado: el zig estaba, y estaba bien, en /opt/hammer/.dev-fs/tools/. Dos campañas
leyeron eso como deuda de la cascada GUI. El commit anterior lo tapó con un bind-mount, que respeta
la suposición en vez de eliminarla; esto la elimina.
Son dos decisiones independientes: dónde se GUARDA lo sellado (almacenamiento) y dónde vive el
toolchain de desarrollo (entorno).
`work_root` NO se desacopla, y es deliberado: el seal final es un rename, que sólo es atómico
dentro del mismo filesystem, así que work DEBE seguir al store. Ése es un acoplamiento real, no una
suposición — el test lo fija para que nadie lo "arregle" de más.
Resolución del lab, de más explícito a más adivinado: `HAMMER_LAB` → hermano del store si existe
(preserva EXACTAMENTE el comportamiento histórico en la topología normal) → hacia arriba desde el
CWD, como git con .git (ésta es la que desacopla) → None, que cae al default de siempre para que
los errores sigan apuntando a un lugar previsible.
La parte que huele el entorno (env + CWD) queda sólo en `from_env_or_defaults`;
`defaults_for_store` se mantiene PURA para que los tests sigan siendo herméticos.
Los hashes NO se mueven: `artifact_hash` no recibe BuildConfig y `hash_inputs` sólo mezcla
contenido de la receta (source id, compiler, target, link, el string zig_version, patches, flags,
phases, hashes de deps) — ninguna ruta del lab. Verificado empíricamente además de por lectura:
`hammer hash --check` sobre el corpus da 754 selladas / 14 sin sellar de 768, calcado al grafo de
estado (12 deuda + 2 nunca). Si algún hash se hubiera movido, una sellada diría NO-SELLADO.
Verificado también end-to-end: con un store cuyo padre no tiene .dev-fs, hammer sube desde el CWD,
encuentra el lab y construye (antes moría en el acto); y la topología normal sigue dando cache-hit
instantáneo con el mismo hash.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
harkaq-farm-setup.sh: provisiona un worker para barrer (idempotente; gatea por
ABI>=7 y aborta si no llega — con la golden vieja el barrido daría SinEvidencia
en TODO). Deriva el runtime base DEL ROOTFS DEL WORKER, no copia el del laptop:
es por-rootfs (§4.3) y los sonames dependen de la versión de Alpine.
Barrido: 24 recetas, 14 con veredicto. Primer número crudo: 4 irreducibles (29%).
ERA FALSO, y por dos motivos propios:
1. GAP DE POLÍTICA. Los `list` salían sólo de los ancestros de la clausura ⇒ una
receta SIN deps (bash) no generaba ni uno y todo escaneo del árbol caía como
denegación (/usr/lib). Listar NO es leer: Landlock separa READ_DIR de
READ_FILE ⇒ la estructura del árbol es CONTRATO. Se puede `ls /usr/lib` sin
leer un solo fichero no declarado; la deuda es leer lo ajeno, no saber que
existe. Idem /var/tmp: con --tmp-overlay / es scratch descartable, como /tmp.
2. La heurística del catálogo es por NOMBRE y un fichero no se llama como su
paquete: /usr/bin/ranlib lo trae `binutils`, /usr/bin/diff lo trae
`diffutils`. El único que sabe la verdad es el STORE (conoce la lista de
ficheros de cada artefacto) — y el worker tiene 103 artefactos contra los
cientos del hub.
⇒ EL WORKER MIDE, EL HUB CLASIFICA. Misma separación que lector/clasificador: el
que tiene el privilegio —o los datos— hace lo mínimo. Reclasificado en el hub:
bash ranlib,/usr/lib,/var/tmp → nada (gap de política)
doas /usr/bin/diff → nada (diffutils lo provee)
ca-certificates /usr/bin/perl → /usr/bin/perl
curl /usr/bin/perl → /usr/bin/perl
RESULTADO: la deuda irreducible de toda la muestra es UN path — /usr/bin/perl
(2 de 14 = 14%). No hay recipes/perl.toml ni artefacto: deuda genuina y nombrada.
Todo lo demás era declarable (make ×10, ranlib→binutils, diff→diffutils).
Sigue por encima del <5% del §7, pero el perfil es el que importa: NO hay cola
larga de sorpresas. La deuda de un catálogo entero se resume en "declarar make" y
"no tenemos perl".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
harkaq (§4.6) dejó UN solo caso irreducible en el primer barrido: brotli pidiendo
libstdc++.so.6.0.34 + libgcc_s.so.1. Pero brotli es C, no C++ — la libstdc++ no
era suya: era de `cmake`, su dep de build.
store/99864dcc…-cmake/usr/bin/cmake
NEEDED: libstdc++.so.6, libgcc_s.so.1, libc.so
"gcc retenido para {kernel, cmake}" no era una concesión de BUILD-TIME como
sonaba: el artefacto de cmake arrastraba el runtime C++ de Alpine hacia dentro de
CADA build que lo declarara como dep. Un agujero de soberanía viajando por el
grafo de deps, invisible en la receta del consumidor. harkaq lo señaló desde el
consumidor, que es donde se ve.
EL ARREGLO NO TOCA EL COMPILADOR — gcc sigue compilando cmake (camino
conocido-bueno; zig c++ segfaultea el cmake mínimo). Sólo deja de enlazar su
runtime en dinámico:
CXX='g++ -static-libstdc++ -static-libgcc' LDFLAGS='-static-libstdc++ -static-libgcc'
Medido:
antes NEEDED: libstdc++.so.6, libgcc_s.so.1, libc.so
después NEEDED: libc.musl-x86_64.so.1 (y corre: cmake version 3.31.6)
EL PAGO: brotli —el único irreducible del barrido— pasa a Hermetico ×3 fases,
artefacto sellado b3:bc900676…. El barrido queda 0 irreducibles de 6. El caso que
iba a contar contra el <5% no era una receta mal escrita: era una herramienta de
hammer filtrando Alpine.
+ /proc como superficie de CONTRATO (lo destapó el configure de brotli, que lee
/proc/cpuinfo y /proc/meminfo): bwrap monta un /proc FRESCO dentro del pidns, no
sale del rootfs Alpine y no ve al host ⇒ contrato, no deuda. Mismo caso que
/cache. El hash del artefacto es idéntico antes y después de añadirlo: cambia el
veredicto, no el build.
COSTO DEL ROLLOUT: cambiar recipes/cmake.toml re-hashea cmake y sus 3
consumidores (brotli, libjpeg-turbo, libtiff = 6 sellados). Radio chico, PERO
libjpeg-turbo y libtiff son la cadena GUI y la regla es no rebuildearla en el
laptop (zig-skew rompe cairo) ⇒ el rebuild va al worker.
LO QUE NO CIERRA: /usr/bin/gcc, c89, c99, ldd y el plugin LTO (§4.7) siguen
siendo SONDAS — la jaula las deniega, los builds completan igual, y denegarlas es
lo correcto. El gcc de Alpine sigue en el rootfs y sigue haciendo falta para
{kernel, cmake} en BUILD-TIME. Lo cerrado es la filtración a RUNTIME, que es la
que contaminaba artefactos.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
crates/hammer-build/src/harkaq.rs + Sandbox::bwrap_args_harkaq: harkaq sale de
`hammer build`, no de un script. Deriva la política de la clausura declarada
(D1), lanza el lector, mete harkaq-exec como último eslabón antes del `sh -c`, y
recoge el Verdict por FASE (configure/compile/install son bwraps distintos ⇒
dominios distintos ⇒ un veredicto cada uno).
EL HITO, sobre recipes/zlib.toml (receta real del catálogo):
configure → Hermetico esperadas (14): /usr/bin/gcc deuda: ninguna ✓
compile → Impuro DEUDA (1): /usr/bin/make → rc=126, la jaula lo frenó
El diagnóstico completo de zlib cabe en una frase: lo único que toma de Alpine
sin declararlo es `make`.
INERTE sin HARKAQ=1: mismos args de bwrap, mismo entorno, ningún proceso extra.
Requisito duro, no cortesía — 700+ artefactos sellados no pueden cambiar de hash
por encender un diagnóstico. Los 57 tests previos del crate siguen verdes sin
tocar, que es la prueba.
harkaq-policy es PURO y se testea sin kernel (4 tests nuevos, como pide §4):
traduce store→sandbox, excluye la metadata `.hammer/` del artefacto, y deja
listables los ancestros de cada fichero de la clausura.
La integración se delató sola en su PRIMERA corrida real: faltaba /cache como
superficie de contrato (ZIG_GLOBAL_CACHE_DIR=/cache/zig — zig CREA dirs ahí) y
la fase moría con `fs.make_dir /cache/zig/tmp`. De ahí que las superficies de
contrato las decida el Sandbox (que sabe qué montó) y no harkaq: /cache sólo
existe si hay cache_dir, y una política que nombre un path inexistente aborta el
build a propósito.
libc en hammer-build: Child::kill() manda SIGKILL y el lector moriría MUDO, sin
emitir el Verdict — justo lo que no queremos de un componente cuyo producto ES
el veredicto. Hace falta SIGTERM.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El grafo de build de KF6/Qt converge en qtbase/kcoreaddons por decenas de rutas; artifact_hash_rec
recorría cada camino sin caché ⇒ O(nº caminos) exponencial. Memo por path canónico de receta.
El hash resultante es idéntico (sólo cachea) ⇒ no invalida artefactos sellados.
overlayfs concatena todas las lowerdir en un string de opciones acotado a ~1 página (4096B). KIO
(~43 deps) desbordaba → bwrap 'Can't make overlay mount ... No such file or directory' (truncado).
bwrap CANONICALIZA cada --overlay-src a su ruta real, así que symlinks cortos no ayudan (probado).
Fix: cuando la suma de rutas supera ~3000B, se funden todas las deps en un dir real por hardlinks
(cp -alf, last-wins = misma semántica que el apilado overlay) y se pasa esa ÚNICA capa ⇒ lowerdir
constante, escala a cientos de deps (todo Plasma/apps). Cacheado por build con marker .done (una vez
aunque bwrap_args corra por fase). Pocas deps = ruta directa sin I/O (sandbox idéntico, tests intactos).
Validado: KIO configura con 43 deps visibles y compila. 57 tests verdes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`cargo vendor vendor` borra lo que no reconoce del dir destino; proyectos que
commitean su PROPIO vendor/ (mise → vendor/aqua-registry, 3.1MB que build.rs lee)
lo perdían → build.rs falla 'No such file'. Nuevo campo opcional [source]
cargo_vendor_dir (default 'vendor', sin cambios para las ~200 recetas selladas)
manda el vendoreo cargo a otro dir; el .cargo/config.toml que emite cargo vendor
ya apunta ahí. mise.toml usa '.hammer-cargo-vendor'. Validado en vivo: aqua-registry
sobrevive + compila 591 crates pasando los muros previos (openssl vía rustls + AR).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sube el modelo de slots de wawa-memo (prototipo host) a hammer:
- Recipe + source_patch del .swm llevan bloque `slots` {claims, requires} (slot->b3:…),
FUERA de hash_inputs (topología ≠ identidad, no mueve el artifact_hash). Viaja intacto
por los dos sentidos del puente (Recipe->.swm->Recipe): test de round-trip.
- InstalledDb registra claims por paquete + system_state() -> slot->hash (el Estado del gate).
- hammer-core::compat::evaluar(estado, slots) -> Veredicto {Compatible, Colision, Incompatible}
(el álgebra probada en wawa-memo, sobre tipos de hammer).
- Gate en `hammer install`: antes de tocar nada evalúa el paquete contra el estado instalado.
Incompatible (requisito sin resolver, caso wayland) -> aborta; Colisión (caso logo) ->
aborta pidiendo elección salvo --force-slots; Compatible -> procede y registra los claims.
Plumbing propagado por los 5 sitios de Mutation::SourcePatch (from_recipe, swm_bridge,
export, bus, orchestrator). Tests: compat (4) + slots-no-en-hash/round-trip (2) +
system_state (1) + puente receta<->swm (1). Workspace compila y verde.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El verificador que corre cada check en el sandbox reproducible y falla ⇒ no proponer
(SDD 15 §H1). Piezas: (1) hammer-core: EvidenceCheck::evaluate(exit, stdout) -> CheckOutcome
(pura, testeada: exige expected_exit y, si hay, blake3(stdout)==expected_output) +
ArtifactHash::of_bytes. (2) hammer-build: Sandbox::run_capture -> CmdOutcome (captura stdout
completo + exit, tee de stderr) + run_evidence(recipe,cfg,store) -> EvidenceReport que
reproduce el artefacto (cache-hit), levanta un sandbox con fuente+build-deps+el artefacto
instalado como capa overlay (binarios en PATH) y corre cada check. (3) CLI: swm-verify
--evidence reconstruye cada source_patch y corre run_evidence; imprime veredicto por check +
estrato máximo alcanzado; exit != 0 si algún check falla. Es un runner de comandos con hash
del output, no un framework — la confianza vive en el checker.
Verificado e2e: tree con [[evidence.checks]] cmd-exit 'tree --version' → pack cache-hitea
(evidencia no cambia el hash) → swm-verify --evidence corre el check en el sandbox: pasa (exit
0) y falla con expected_exit=7 (exit 1, 'NO proponer'). Núcleo puro con tests unitarios.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Frontera AI-nativa SDD 15 §H1. Agrega `Evidence { checks: Vec<EvidenceCheck> }` a Recipe
(TOML) y a Mutation::SourcePatch (.swm YAML): cada check es {kind, cmd, expected_exit,
expected_output?} con kind ∈ {cmd-exit, proptest, contract, kani} (estratos de confianza
crecientes, EvidenceKind: Ord). DECISIÓN CLAVE: la evidencia NO entra en hash_inputs —
certifica comportamiento, no identidad ⇒ no mueve el artifact_hash (baseline de
reproducibilidad intacto). Round-trip completo: from_recipe (forward) + swm_bridge
synthesize_recipe (reverse) + camino de pack (cli). verify_schema valida forma (cmd no
vacío, expected_output con prefijo b3:). El checker que EJECUTA la evidencia es H1b; el
cableado al Orchestrator VERIFY es H1c (marcado con evidence: _). Tests: recipe + swm,
incl. que la evidencia no cambia el hash. Sin warnings clippy nuevos.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Como root (granja), tar preservaba el uid del tarball → el sandbox (userns) no podía chmod/escribir
esos ficheros → configure autotools fallaba (oniguruma: Permission denied creando Makefile). Extraer
como root sin preservar owner lo arregla para TODO tarball. oniguruma vuelve a configure plano.
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.
Destraba la clase workspace (go-mockery/k3d/git-town/csvtk): con go.work, GOWORK=off + 'go mod
vendor' sólo vendoreaba el módulo raíz → 'inconsistent vendoring' en el build. Ahora si hay go.work
se usa 'go work vendor' (go 1.22+, vendor consistente de todo el workspace); el trigger y
detect_build_system también reconocen go.work (workspace sin go.mod raíz). Test detect_go_when_only_go_work.
eksctl tiene cmd/eksctl (CLI) + cmd/schema (helper de codegen que panica); detect elegía
arbitrariamente y selló el binario equivocado (schema) — un fallo SILENCIOSO (sella OK pero envía
la herramienta incorrecta). Ahora, a igual clase, el dir cuyo file_name == recipe.name gana. Test
detect_go_main_prefers_cmd_matching_recipe_name. Lección: smoke-testear que el binario instalado es
el ESPERADO, no solo que selló.
telegraf/vault (binarios Go gigantes) fallaban en el LINK con 'mapping output file failed: no space
left on device': el sandbox monta --tmpfs /tmp (~½ RAM) y el GOCACHE + temp del linker desbordan ese
tmpfs aunque el disco tenga 18G libres. Ahora GOCACHE/GOTMPDIR van a /src (árbol de fuentes bind-
montado, en disco, per-build, lo limpia el watchdog; no se sella). No entra al hash de input (fase
generada) ⇒ no re-sella el corpus Go.
cmd/helm/helm.go arranca con un header Apache /* ... */ cuyas líneas internas no llevan '*';
el filtro por-línea ingenuo de is_main_go cortaba en 'Copyright ...' y daba 'no es main' ⇒
detect_go_main no hallaba cmd/helm y caía a '.' (raíz sin .go → 'no Go files in /src'). Ahora
rastrea el estado dentro/fuera de bloque /* */. Afecta a cualquier .go con header de bloque.
Test: detect_go_main_sees_main_under_block_comment_header.
Cierra la fragilidad manual del patron Go (path del main por receta). Ahora
importar Go es tan automatico como Rust: import -> pin -> build, sin tocar nada.
- lib.rs: BuildSys::Go (go.mod, prioritario sobre configure/make auxiliares que
traen muchos proyectos Go). resolve_phases deriva el compile generico:
'go install -trimpath -ldflags=-buildid=' del paquete main. install='true'
(go install ya deja en GOBIN=/out/usr/bin).
- detect_go_main(): detecta el dir del main por FILESYSTEM (no compila, evita
contaminarse con mains de ejemplo rotos en docs/scripts que rompen go list).
Heuristica raiz > cmd/<x> > menor profundidad; excluye vendor/docs/test/etc.
go install nombra el binario solo (cmd/mlr->mlr, raiz->modulo).
- nix_import.rs + nix-import.sh: detecta is_go (vendorHash de buildGoModule),
emite deps.build=['go'] sin phases (BuildSys::Go las deriva).
- tests: go_mod_wins_over_configure, detect_go_main_picks_cmd_over_docs.
Validado end-to-end: miller (cmd/mlr->mlr, esquiva docs rotos), amfora (raiz),
duf (import->pin->build 100% automatico, binario estatico que corre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Aplica el patron de amfora a la escoria Go. Build-yield 11/13:
- raiz: croc/gron/jump/gtrash/walk/qrcp (go build .)
- main en ./cmd/<x>: cheat/dnsx/dyff + miller (binario mlr)
Todos estaticos y corren (mlr 6.18.1, croc v10.4.4, cheat 4.5.0, ...).
fetch.rs: vendor_go_deps ahora setea GOWORK=off (go mod vendor aborta en
modo workspace si el arbol trae go.work).
Diferidas 2 (casos especiales): csvtk (go.mod en subdir, el vendor no corre
en /src) y git-town (go.work multimodulo -> inconsistent vendoring; necesita
go work vendor, no go mod vendor).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Cierra el lazo Etapa F+G: el userland del producto se arma vía `hammer install
--repo --prefix --require-signed` desde el repo firmado, no del bootstrap
hardcodeado. scripts/product-userland-from-repo.sh hidrata un set curado (13+
tools validados estáticos del repo).
Dos bugs reales del path install/pack encontrados+arreglados:
1. pack derivaba target_bin=/usr/bin/{name}; para paquetes con binario≠nombre
(ripgrep→rg, repgrep→rgr) quedaba mal y el sanity-check de install fallaba.
Ahora pack lo deriva del flag `--bin <X>` de la receta Cargo.
2. La reproducción del source_patch derivaba el NOMBRE de la receta del
target_bin ⇒ "rg" ≠ "ripgrep" del corpus ⇒ no cache-hit ⇒ rebuild + dup en
el store (find_by_hash ambiguo). build_source_patch/run_apply ahora reciben
el nombre del paquete (install/bootstrap lo pasan; apply=None) ⇒ cache-hit
del artefacto del corpus, sin duplicar.
(El "EACCES" inicial era sólo --store ausente: DEFAULT_STORE=/store root-only.)
hammer-build 49/49 tests ok; ripgrep install validado e2e (rg hidratado).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dos fixes en hammer-build que juntos destraban las recetas con C-FFI:
- Inyecta OPENSSL_DIR=/usr + OPENSSL_STATIC + OPENSSL_NO_VENDOR cuando la receta declara
'openssl' como dep ⇒ openssl-sys usa la openssl hammer-built (estática/musl) en vez de
buscar en el host y abortar.
- resolve_dep_path(): fallback al catálogo PADRE — una receta en recipes/incoming/ ahora ve
las deps del corpus recipes/ sin duplicarlas (la granja construye desde incoming/).
Recetas: +dep openssl a monolith/mdcat/mise; tuc saca 'cargo auditable'→'cargo build'.
VALIDADO: monolith v2.10.1 sella (openssl-sys linkea contra la openssl del corpus).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
La granja revelo una clase entera de fallos Rust: crates con dep-C cuyo build-script
(cc-rs) compila C con zig-cc, que lo RECHAZA/miscompila (delta=libgit2-sys,
gitui=openssl-sys, jless=libc-stdhandle). Mismo frente que file/jq en C puro -> misma
solucion: el GUETO gcc.
FIX (hammer-build/lib.rs, camino Cargo nativo): cuando la receta declara compiler="gcc",
cc-rs usa CC=gcc (el Alpine musl gcc del sandbox, probado en C) para compilar el C de
las deps; el LINKER sigue siendo el wrapper zig-cc. Los .o/.a de gcc-musl y el Rust
zig-musl son ABI-compatibles (ambos musl, mismo ELF x86-64). ADITIVO: una receta Rust
sin compiler= queda en ZigCc -> baseline (ripgrep/fd/...) intacto. 2 tests nuevos.
PROBADO END-TO-END: delta 0.19.2 construye+corre ELF estatico (libgit2-sys vendorea y
compila libgit2 desde decenas de .c con gcc; + dep zlib para -lz que libgit2/libz-sys
enlazan). Publicado al repo firmado (83 paquetes). Corpus 82->83.
LEVERAGE: libgit2-sys/openssl-sys/onig_sys/libc-stdhandle aparecen en CIENTOS de crates
-> el gueto gcc (+ resolver libs externas via deps.build) destraba esa clase entera, no
solo delta. jless queda staged (su feature de clipboard pide libxcb/X11, fuera de
alcance, NO toolchain). gitui (openssl-src vendoreado) deberia construir con el mismo
patron (compiler=gcc), pendiente de correr.
Dogfoodear el corpus entero (build-repo.sh: 55/56 con expected_hash anclado, repo
firmado Ed25519) destapó un bug latente en la reconstruccion desde .swm: el path
del patch se prefijaba con el catalogo (swm-recipes/) TRES veces, y el install
abortaba con 'No such file'. jq es el PRIMER paquete con patch + build-dep que se
instala (fd/bwrap no tienen patch) -> nunca se habia ejercido ese camino.
Causa: recipe_from_source_patch metia catalog_dir en el path Y synthesize_recipe lo
re-unia a base_dir, redundante con el base_dir.join(p) que el lab hace al construir
(fetch::apply_patches / Recipe::hash). Fix: source.patches guarda solo el NOMBRE
relativo a base_dir; un unico join lo resuelve. El hash usa el CONTENIDO del patch,
no el string del path -> el expected_hash anclado sigue casando.
Verificado e2e: install jq desde el repo firmado -> trusted -> reproduce desde
fuente (hash e770e04d = el anclado) -> patch aplicado -> 18 ficheros hidratados ->
registrado en DB -> jq corre. + test de regresion inline_patch_resuelve_con_un_solo_join.
El residuo autotools de los imports de Alpine (configure --build=$CBUILD --host=$CHOST
del abuild) ya no es trabajo a mano:
- El lab exporta CBUILD/CHOST con el triple nativo SANEADO (x86_64-linux-musl, el
mismo que el wrapper zig-cc emite) ⇒ las fases traducidas de Alpine que referencian
$CBUILD/$CHOST literal resuelven en runtime en vez de quedar vacías (config.guess
detectaría x86_64-alpine-linux-musl, vendor que zig rechaza).
- La heurística autotools inyecta --build/--host al triple saneado cuando la receta no
los puso ya (juicio per-paquete gana). build==host ⇒ NATIVO: autotools sigue corriendo
sus AC_RUN tests; sólo normaliza el triple.
- Inerte para Cargo/CMake/Meson (no leen esas envs ni el triple).
VALIDADO REAL: e2e autotools BUILDEA (configure 'cross compiling... no', sella+corre).
3 tests nuevos de heurística + import comment actualizado.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sube el yield Rust a casi-todo el ecosistema CLI moderno (sd/fd/… usan workspace virtual: root
sólo agrupa, el bin vive en un sub-paquete ≠ pname). Antes exigía `-p` manual; ahora el lab lo
resuelve solo.
- hammer-build/lib.rs: cargo_root_is_virtual (root con [workspace] sin [package]) +
resolve_cargo_bin_package (lee los members vía `cargo metadata` —fuente autoritativa: globs,
[[bin]], src/bin/*, nested— y devuelve el paquete que expone el bin) + inject_cargo_package_selector
(si virtual y flags piden --bin X sin -p, antepone `-p <pkg>`). Determinista ⇒ reproducible; el
hash usa los flags ORIGINALES, el -p es resolución interna. Crate suelto / ripgrep: intactos
(no virtual). serde_json a deps de hammer-build.
- VALIDADO REAL: sd (workspace virtual, bin en `sd-cli`≠pname) ahora BUILDEA SOLO (sd 1.0.0),
sin tocar la receta (flags quedan --bin sd, el lab resuelve -p sd-cli). 31 suites verde.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Midiendo build-yield real con la capa puesta: hyperfine (nix) construye end-to-end → ELF estático
musl que corre. Dos fixes que lo desbloquean genéricamente (sin patch por receta):
- nix_import.rs: rustc/cargo/rust se filtran de deps (son el LAB, no paquetes) — sin esto el build
abortaba buscando rustc.toml. Default de flags Rust vuelve a `--bin <bin>` (el `-p <pname>` no
generaliza: el paquete cargo del bin puede ≠ pname, p.ej. sd→sd-cli).
- hammer-build/lib.rs: `ensure_cargo_workspace_isolation` inyecta `[workspace]` vacío al Cargo.toml
de la fuente si no lo tiene, ANTES de vendor. Idempotente ⇒ no choca con las recetas del corpus
que lo parchean a mano. Resuelve el gotcha "fuente dentro del workspace hammer ⇒ cargo vendor
aborta" para CUALQUIER import Rust.
BUILD-YIELD medido (real, con la capa): lz4 (C/Alpine, escape gcc) ✓ · hyperfine (Rust/nix) ✓ ·
sd (Rust) ✗ workspace-virtual con bin en paquete ≠pname (necesita `-p` manual). Texture honesta:
los bien-estructurados buildean solos; los con quirks de workspace necesitan toque per-paquete.
31 suites verde.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El muro del piloto: zig-cc filtra los flags de lld (rechaza -Wl,--allow-multiple-definition y
-z muldefs) ⇒ paquetes C con símbolos duplicados (lz4) no linkean. El escape es usar un toolchain
real (gcc de Alpine = musl + GNU ld, que SÍ acepta -z muldefs), pero el sandbox FIJABA CC="zig cc"
ignorando el campo `compiler` de la receta.
- hammer-build/lib.rs: `build.compiler` ahora setea CC/CXX/AR reales en el env (gcc→gcc/g++/ar,
clang→clang/clang++/llvm-ar; zig-cc = default sin cambio). `self.env` pisa los defaults del
sandbox. gcc/clang default a x86-64 genérico ⇒ reproducible, sin -mcpu=baseline.
- PURAMENTE ADITIVO: ningún recipe del corpus declara compiler=gcc (el gueto usa CC=gcc en fases),
y el 4/4 núcleo es zig-cc ⇒ baseline of_tree intacto.
- VALIDADO REAL: lz4 (importado de Alpine) con `compiler = "gcc"` declarativo (fase SIN CC=gcc)
→ construye + corre (lz4 v1.10.0). El paquete que moría en el lld de zig ahora sella. 31 suites verde.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cierra el hueco que pack/install advertían: un paquete con build-deps (bwrap→libcap,
openssh→zlib,openssl) ahora se instala por nombre reproduciéndose desde fuente CON sus deps.
Modelo: las build-deps viajan por NOMBRE en el source_patch y en la PackageEntry; install
resuelve el cierre transitivo desde el índice y reconstruye un catálogo de recetas que el lab
consulta al materializar deps en el sandbox.
- hammer-core: `Mutation::SourcePatch.deps` (Deps, serde-skip si vacío) + `from_recipe` lo
carga. `Deps::is_empty`. `RepoIndex`/`PackageEntry.deps` + `resolve_closure(name)` (DFS
topológico, deps antes que dependientes, detecta dep faltante y ciclo). 8 tests nuevos.
- hammer-build/swm_bridge: refactor — `recipe_from_source_patch` (síntesis pública, setea deps
+ base_dir=catálogo) + `catalog_dir_for` (dir determinista compartido). build_source_patch
lo reusa. synthesize_recipe ahora setea recipe.deps + base_dir al catálogo (no "/").
- hammer-cli: pack puebla PackageEntry.deps; install resuelve el cierre y escribe un {dep}.toml
por dep en el catalog_dir ANTES de aplicar el target (mismo dir determinista que usa
build_source_patch ⇒ el lab resuelve {dep}.toml por nombre). Warning de pack actualizado.
- VALIDADO E2E REAL contra ./store: `install bwrap` resuelve libcap del catálogo y reproduce
el artefacto CACHEADO EXACTO (b3:f89e716…) → hidrata bwrap (1.8MB ELF). El paquete con dep
hashea bit-idéntico al original. Resolución/diamante/faltante/ciclo unit-tested. 31 suites verde.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cierra la dirección forward que faltaba (SDD 06 §6 la marcaba "para más adelante"):
una receta que el sistema ya sabe construir se vuelve un paquete distribuible y
reproducible-desde-fuente, inversa de `hammer apply`.
- hammer-core: `Swm::from_recipe(recipe, target_bin, patch_text, expected, distro)`
(constructor puro: el caller lee los patches). `SwmBuild` gana `phases`+`zig_version`
y `SourcePatch` gana `strip_components` (Option/skip ⇒ .swm viejos parsean igual) para
reproducir con fidelidad el corpus real (22/34 recetas usan phases, 8 usan zig 0.13).
- hammer-build/swm_bridge: la dirección inversa (source_patch→Recipe→build) ahora traslada
phases/zig_version/strip_components a la receta efímera ⇒ apply rehace idéntico.
- hammer-cli: `hammer pack <recipe> [--target-bin] [--out] [--expected|--build] [--sign]`.
Concatena los patches inline; avisa si la receta declara deps (el source_patch aún no
las modela = pieza posterior). `export` también enriquece su source_patch.
- Validado en host: ripgrep (git+patch+install custom), openssl (tarball+zig 0.13+phases),
coreutils (multicall), findutils firmado → swm-verify "trusted". Tests core+bridge verde.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mata gcc para 5 de las 7 recetas que lo forzaban, vía una escotilla nueva:
- hammer-core/hammer-build: campo `[build].zig_version` por receta. Cuando se
fija, el lab resuelve ese zig (hermano del por defecto, `zig-x86_64-linux-<v>`)
en vez del global, y entra al hash SÓLO si está presente (baseline 9adefb82
intacto). `effective_zig_dir` lo aplica en ensure_layout + Sandbox.
- Causa: BISECT con oráculo flex (reproducido sólo vía lab: musl DINÁMICO) — el
miscompile es una REGRESIÓN de zig 0.14; 0.13.0 compila limpio, 0.14/0.15/0.16
fallan. Es C/musl-dinámico, NO afecta C++.
- Flip a zig_version="0.13.0" (quitando CC=gcc): flex, openssl, elfutils,
binutils, python3. Verificados: `as` 2.45.1 corre, python3 3.12.10 corre
(deepfreeze OK), libcrypto/libelf sellan. Todas son tools (no inputs del 4/4).
cmake queda en gcc: su segfault es C++ (libc++/musl), bug distinto que 0.13 NO
arregla (ni con -static). El kernel queda pendiente de verificar.
Tests: hammer-core/hammer-build verdes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cierra la Etapa A del camino a la distro (SDD 11 §5):
- `hammer_bootstrap::all(seed, recipes_dir, base_cfg, store) -> AllReport`
encadena stage0→stage1→stage2 en una corrida del host y deja el
`bootstrap.json` poblado. El veredicto ✓ REPRODUCIBLE lo sigue sellando el
rebuild in-VM (selfhost-verify.sh); `all` ancla la referencia para comparar.
- CLI: `hammer bootstrap all --url … --sha256 … --version …` y
`hammer bootstrap manifest` (imprime el log de transparencia, §4).
- Refactor: `parse_seed_kind` factoriza el match de `--seed` (estaba duplicado
en 4 sitios del despacho).
- hammer-build: arregla el test stale `detect_cargo` — la fase Cargo evolucionó
a `RF=…`+`-C link-self-contained=no` condicional, pero la aserción esperaba la
forma vieja `RUSTFLAGS="-C linker=…`. Restaura el workspace en verde (43/43).
Tests: cargo test --workspace verde.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fix de 503e09a, que añadía el flag SIEMPRE: el rust de Alpine (host x86_64-alpine-linux-
musl) NO sólo tiene self-contained apagado — PARCHEA la opción para que sea un error
("option `-C link-self-contained` is not supported on this target"), así que pasarla
rompía el build baseline (verificado: baseline-check falló en el build-script de
proc-macro2). Un rust vanilla (host x86_64-unknown-linux-musl, p. ej. hammer-rust) SÍ la
soporta y la NECESITA (su musl trae self-contained/rcrt1.o que choca con el crt1.o de zig
⇒ duplicate _start).
Ahora condicional por host-triple del rustc: se añade el flag SÓLO si el host NO es
`-alpine-`. Para Alpine ⇒ comando idéntico al pre-503e09a (sin flag) ⇒ 9adefb82 intacto
por construcción. Para hammer-rust ⇒ con flag ⇒ linkea con zig sin duplicar _start.
(`--print`/`--version` no validan -C; sólo el link real lo hace — por eso se discrimina
por host-triple, no por probe.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El link de las recetas Cargo nativas lo hace `zig cc` (un driver de compilador completo
que ya aporta sus startfiles crt1.o). El target musl vanilla `x86_64-unknown-linux-musl`
trae CRT AUTOCONTENIDO (self-contained/rcrt1.o) y lo pasaría también al linker ⇒
`ld.lld: duplicate symbol: _start` (rcrt1.o vs crt1.o de zig). Añadir
`-C link-self-contained=no` a RUSTFLAGS hace que rustc NO aporte startfiles propios y
deje que zig los provea — para TODAS las unidades (deps, build-scripts, proc-macros y la
crate top; RUSTFLAGS las alcanza a todas, los bin_flags de `cargo rustc --` sólo a la top).
No-op para el rust de Alpine (host x86_64-alpine-linux-musl): su self-contained ya está
APAGADO — de hecho el baseline 9adefb82 linkea sin chocar con zig, lo que sólo es posible
si Alpine NO aporta startfiles self-contained. Imprescindible para un rust vanilla, p. ej.
el hammer-rust del frente self-host (SWAP_RUST): con él, hammerd YA compila y sella con
hammer-rust (antes fallaba en el build-script de proc-macro2).
Verificación baseline (of_tree(stage1) sin swap == 9adefb82 con este hammer) en cola.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Deja de ser un error "pendiente": hydrate(.., DynamicSpec{interpreter,rpath})
detecta ELF por magic, copia los que necesitan parcheo (no hardlink: patchelf
mutaría el store) y aplica --set-interpreter/--set-rpath atómicamente
(copy→patch→rename); no-ELF y spec vacío siguen hardlinkeando. ensure_patchelf
falla limpio antes de tocar el FHS si falta la herramienta. HydrateReport.patched
cuenta los reescritos. Callers (bus/cli/bootstrap/e2e) pasan None=estático.
4 tests nuevos (incl. camino de error sin patchelf y real gated). Doc §4.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mutation::SourcePatch y RecipeInline ganan campos repo/commit XOR tarball/sha256
(Option, serde-default ⇒ compat con .swm git existentes), resueltos por
swm::swm_source_kind con la misma regla que recipe::Source::kind. swm_bridge
sintetiza la receta según el modo; hammer export reconstruye fuentes tarball
como source_patch en vez de caer a file_drop (provenance fina recuperada).
Prompt del traductor y roadmap actualizados. Tests nuevos para tarball + XOR.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cuarta pieza del toolchain hammer-from-source (variante b, SDD 11 §7.2b): bubblewrap, el
sandbox del propio lab. Binario estático ⇒ swap de archivo sobre /toolchain/usr/bin/bwrap.
Como herramienta del toolchain (no input del 4/4) no necesita casar byte-a-byte con Alpine,
sólo aislar igual.
Tres piezas:
- recipes/libcap.toml (2.78): dep obligatoria de bwrap; el toolchain Alpine no trae el -dev
(libcap.a / sys/capability.h). Build estático musl con zig cc, sin patches (es tool, no input).
- materialización de build-deps (hammer-build): deps.build ahora se CONSTRUYE recursivamente
(build() llama build() por cada dep) y cada artefacto sellado se apila como capa --overlay-src
bajo el rootfs del sandbox, dejando usr/{include,lib,lib/pkgconfig} en /usr. pkgconf y zig cc
las hallan sin plumbing de flags. Recetas sin deps: sandbox byte-igual (baseline intacto).
Tests nuevos: no_deps_emits_single_overlay_src, deps_stack_as_overlay_layers_under_rootfs.
- recipes/bwrap.toml (0.11.0): el toolchain no trae meson/ninja/python, así que bypaseamos meson
compilando los 4 .c de bubblewrap directo con zig cc (+config.h trivial). deps.build=["libcap"].
Validación host fuerte: hammer-bwrap es estático, corre --version y sandboxea, y musl rebuildeó
BYTE-IDÉNTICO usándolo de sandbox (bisección). Expuesto con SWAP_BWRAP=1. Tests verdes.
Pendiente: corrida in-VM acumulando swaps para el sello ✓ REPRODUCIBLE.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
[VERIFICACIÓN EN CURSO — no confirmado aún] Stage 2 variante (b) destapó que
codegen-units=1 NO basta para reproducibilidad: el backend paralelo de rustc/LLVM
(ThinLTO + codegen) dimensiona su pool de hilos por el paralelismo disponible y sus
decisiones varían build-a-build a >1 hilo.
Evidencia: arje-zero divergía ~62 KB entre dos builds host idénticos (mismo rustc
1.91.1, mismo commit fijado, mismos flags), mientras un build serial (la VM tiene
1 vCPU, o taskset -c 0 en host) reproducía bit-a-bit a of_tree=9adefb82. El baseline
0039b2b9 se construyó en paralelo ⇒ referencia no-fiable. make/musl/busybox/hammerd
están limpios (descartados uno a uno); el único flaky es arje-zero por paralelismo.
CARGO_BUILD_JOBS=1 limita el jobserver a 1 token para serializar el backend. Test
in-flight: rebuild de arje-zero con todas las CPUs + JOBS=1 debe dar 9adefb82. Si el
pool de LLVM ignora el jobserver (usa affinity), habrá que fijar afinidad o LTO.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El rebuild in-VM daba ✗ DIVERGENTE (of_tree host 984e002f ≠ VM 94b93585) pese a
que los 4 componentes sellaban bajo el mismo key of_inputs. Bisección por componente
en el host: musl, busybox y hammerd reconstruyen byte-idéntico, y la ensambladura del
rootfs (of_tree) es determinista e independiente del entorno. El culpable era arje-zero:
dos builds del MISMO host daban binarios distintos (Δ ~9.6 KB por .text/.rodata/.eh_frame/
.gcc_except_table) — firma del codegen paralelo de rustc.
Raíz: el workspace de hammer pinea [profile.release] codegen-units=1 (por eso hammerd
reproducía), pero el monorepo tawasuyu no declara perfil → cargo default codegen-units=16,
cuyo reparto del crate en N objetos varía build-a-build.
Fix: el sandbox impone codegen-units=1 para TODAS las crates Cargo (el var de cargo gana
sobre el profile del repo fuente). El lab elimina el no-determinismo en vez de confiar en
upstream (SDD 09 §2). Verificado: dos builds cu=1 de arje-zero → byte-idénticos.
Nueva referencia reproducible 4/4: of_tree(stage1)=b3:0039b2b9… (reemplaza 198f209f… de
cu=16). Actualizados EXPECT_REF (selfhost-verify.sh) y runbook §8b/§8c.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dos bugs latentes que sólo aparecían en un build real (in-VM); en el host
quedan tapados porque todo sale de caché.
1. scripts/selfhost-verify.sh: los módulos se sacaban de /lib/modules/$(uname -r)
pero la VM bootea $KERNEL (/boot/vmlinuz-linux), que puede ser otra versión.
Mismatch de version-magic ⇒ overlay.ko no carga ⇒ bwrap muere "No such device".
Ahora la versión se deriva del bzImage booteado (file -bL "$KERNEL"; fallback uname -r).
2. crates/hammer-build/src/sandbox.rs: spawn_pump teeaba el stdout del build al
stdout del padre. rebuild-stage1 hace PRIME=$(hammer ... bootstrap stage1); en
un build real son miles de líneas de configure/make capturadas en $PRIME ⇒ el
paso siguiente --rootfs "$PRIME" exec con un arg gigante ⇒ E2BIG (Argument list
too long). Ahora el tee va a stderr; stdout queda para el hash legible-por-máquina.
Con esto el verify corre end-to-end in-VM: los 4 componentes reproducen bit a bit,
pero of_tree(stage1) diverge (host vs VM vs baseline) — no-determinismo del ensamblado
a cazar (SDD 09 §2).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
El rebuild in-rootfs de Stage 2 cazó un no-determinismo real: stage1 reconstruido
en la VM divergía del host (of_tree host=b3:94a4f1… vs VM=b3:ab615b…), aun con el
seal hash idéntico (el seal es of_inputs: mismo key ≠ mismos bytes).
Diagnóstico (control en el host, sin VM): un rebuild de hammerd en el host es
byte-idéntico al cacheado, así que la receta es determinista host-a-host y el cache
no está stale. La divergencia es del entorno de build de la VM: `zig cc` default a
`-mcpu=native` y hornea la ISA del builder en el C/asm de los build-scripts (blake3,
curve25519-dalek). El host tiene SHA-NI+AVX2; `-cpu Broadwell` (VM bajo TCG) no tiene
SHA-NI ⇒ codegen distinto ⇒ bytes distintos ⇒ of_tree distinto. Misma raíz que el
SIGILL de AVX del runbook §7 (binarios al CPU del builder, no a un baseline genérico).
Fix: `-mcpu=baseline` en TODOS los sitios zig cc — CC/CXX del sandbox, los dos
wrappers Cargo (.hammer-zig-cc) y CC/HOSTCC de la receta de busybox. Las rutas SIMD
del runtime (blake3/sha2) son asm con dispatch en runtime: siguen presentes. Bonus:
cierra el AVX/SIGILL del §7 (corre en qemu64 sin -cpu Broadwell).
Validado en el host: con baseline hammerd cambia de bytes (1672448 vs 1677584 —
confirma que el default no era baseline) y los 3 componentes rebuildan limpio
(stage1 rc=0). Nueva referencia CPU-independiente of_tree(stage1-baseline)=
b3:4408e44e2ec51c3769deffcd64dd1f6c2010d414418937c3fa7930a0ded3f845. Runbook §8c
documenta el cruce del muro Rust-offline (cargo vendor por la NIC, hammerd sellado
in-VM en 74m35s) y este diagnóstico+fix.
Nota (plan C.2): el flag de CPU no entra en of_inputs ⇒ cambio de toolchain/flags da
cache-hit con bytes viejos; cerrar ese "pin de toolchain al hash" es trabajo aparte.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stage 2 compara el content-hash (of_tree) de stage1 vs stage1' rebuildeado; para
que coincidan, el build debe ser determinista. Las rutas ya lo son (el source se
bindea en /src, constante entre rebuilds); el knob que faltaba es el timestamp:
SOURCE_DATE_EPOCH=1 (+ TZ=UTC) fija los que tar/ar/gzip y __DATE__/__TIME__ embeben.
No cambia ningún ArtifactHash (input-addressed) ⇒ caché intacta; sólo normaliza
los bytes de salida de builds futuros. Plan C.2 #5.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tercera iteración del fix de la corrida real (runbook §8). +crt-static (y
relocation-model=static) en RUSTFLAGS rompen los proc-macros en build nativo:
RUSTFLAGS alcanza a las deps, y un proc-macro es un dylib PIC que no puede ser
estático ("cannot produce proc-macro for clap_derive ... crate types").
Fix: pasar los flags de codegen del binario por `cargo rustc --release … -- <flags>`,
que los aplica SÓLO a la crate top-level, dejando proc-macros/deps intactos. Así el
binario final es crt-static + relocation-model=static (AUTOCONTENIDO y ET_EXEC sin
interpreter, como busybox: sin libc.so, sin loader, sin soname). El linker (zig cc)
sigue por RUSTFLAGS. musl queda --disable-shared (no se necesita loader).
Validado por unit tests; la corrida real rebuildea hammerd/arje-zero crt-static.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cierra el último bloqueo del boot que la corrida en QEMU destapó (runbook §8):
arje-zero/hammerd buildeaban DINÁMICOS contra musl (el rust de Alpine es dinámico
por defecto) → DT_NEEDED libc.musl-x86_64.so.1, y nuestro libc.so (musl shared con
zig cc) no exporta memcpy/memset/… → el loader falla y el init muere.
Fix golden-path: para link=static, RUSTFLAGS ahora fuerza
-C target-feature=+crt-static -C relocation-model=static
⇒ binarios AUTOCONTENIDOS (musl dentro), ET_EXEC sin interpreter, como busybox:
sin libc.so, sin loader, sin el soname de Alpine. musl vuelve a --disable-shared
(no se necesita el loader). Boot en QEMU: usar -cpu Broadwell (el qemu64 default no
tiene el AVX que zig emite).
Validado por la corrida real: los 4 componentes buildan y el rootfs se sella; el
kernel arranca el initramfs y ejecuta arje-zero como PID 1. Falta rebuildear
hammerd/arje-zero con crt-static para cerrar el boot (cache-bust + rebuild).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>