Bug que la granja destapó: el import de Alpine de crates Rust emitía cargo/cargo-
auditable como [deps] build (el path nix ya los filtra, el Alpine no) -> la receta
abortaba buscando cargo.toml. Fix raíz en alpine_import.rs: normalize() filtra el
TOOLCHAIN (cargo/cargo-auditable/rust/rustc/go/make/cmake/meson/ninja) — lo provee
el lab (BuildSys::Cargo + detectores), no es un paquete a materializar.
xsv 0.13.0 reescrito como receta Cargo LIMPIA (sin las fases 'cargo auditable build'
del abuild que pelean con el flujo del lab, sin deps de toolchain): el lab autodetecta
Cargo. Construye+corre estático (computa stats CSV). Publicado al repo (85).
gitui 0.28.1 construye+corre estatico. openssl-src (vendorea+compila openssl 3.4
desde C) compilo con gcc (CC=gcc del fix anterior) + dep zlib para -lz. Segunda
validacion del gueto gcc para build-scripts: generaliza de libgit2-sys (delta) a
openssl-sys (gitui) — el frente crate+dep-C esta resuelto con el patron
compiler=gcc + deps.build=[libs externas]. Publicado al repo firmado (84 paquetes).
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.
La pieza que escala a miles: scripts/build-farm.sh construye N recetas a la vez
(worker pool xargs -P) sobre recipes/incoming/, y lo que CONSTRUYE+sella lo promueve
incoming/->recipes/ + publica al repo firmado (en serie: el index.json es mutable
compartido); lo que falla queda staged con su motivo. Idempotente (cache-hit del store).
Corrida sobre las 22 del lote Rust #2, JOBS=4 en la laptop libre (8 cores/19GB):
BUILD-YIELD 11/22 promovidos, los 11 corren static-musl (bandwhich/difftastic/fclones/
gping/grex/mcfly/miniserve/navi/onefetch/pastel/viu). Corpus 71->82, repo 82.
Los 11 fallos, FLAGEADOS por la granja (esto es el valor: triage automatico, no
adivinanza) y staged en incoming/ para revision:
- crate+dep-C bajo zig-cc (frente conocido, mismo family que el gueto gcc): delta
(libgit2-sys), gitui (openssl-sys), jless (libc-stdhandle).
- MSRV > 1.91.1 (techo del sandbox): ouch (1.93). watchexec: feature nightly E0658.
- LINK colgado con zig-cc en binarios Rust grandes (deadlock, hubo que matarlos):
broot, pueue, starship. Patron a investigar (zig cc + lld en binarios pesados).
- import roto (no build): xsv/zellij 'No such file' (la fuente no clasifico),
dog (importo un dog.c ajeno, no el cliente DNS Rust).
GOTCHA infra: builds Rust grandes pueden COLGARSE en el link final con zig-cc
(broot 7min frozen, pueue/starship idem); la granja deja matar el slot
(pkill -f 'build recipes/incoming/<n>.toml') y sigue con los demas.
Muestra del lote Rust #2 cerrada: eza/lsd/just/tealdeer/choose construyen+corren
(static-musl) -> 5 al corpus validado + repo firmado (71 paquetes). ouch FALLA por
MSRV (pide rustc 1.93.0; sandbox topa en 1.91.1, techo conocido) -> queda staged.
build-yield muestra = 5/6 = 83% (1 solo fallo, y por MSRV, no por toolchain musl).
Flujo staging->corpus en accion: del lote Rust #2, los 3 primeros de la muestra
construyen+corren (ELF static-musl) -> promovidos incoming/->recipes/ y publicados
al repo firmado (69 paquetes). eza (ls moderno), lsd (ls+iconos), just (task runner).
Quedan 24 staged; se promueven a medida que la granja los construye.
Respuesta a "asi de a poquitos no cubrimos miles": el grueso va por LOTE, no a mano.
import-batch sobre tandas/cli-rust-2.txt (30 CLIs) -> yield import 29/30 en ~2 min
(nix=27, alpine=2; solo rargs fallo). pin-recipes anclo 26 tag->SHA. A esta tasa,
1000 paquetes ~ 1-2h de import desatendido.
ESTRUCTURA: recipes/incoming/ = COLA DE STAGING (el import vuelca en masa); recipes/
= corpus VALIDADO (construye+corre) del que build-repo.sh arma el repo firmado. La
granja/CI promueve incoming->recipes a medida que cada uno construye. Asi el corpus
mantiene su invariante sin frenar la importacion masiva.
27 recetas staged (todas parsean): eza lsd zellij starship just delta difftastic
gitui broot choose dog gping grex jless miniserve navi onefetch pastel pueue tealdeer
watchexec xsv bandwhich fclones mcfly viu ouch.
Build-yield (muestra en curso): eza OK, lsd OK (static-musl ELF). El resto se ancla
progresivamente. Las recetas a mano de las tandas previas (libs/gueto/lib+bin) fueron
para endurecer el importador en los casos duros; el largo tail navega por este lote.
nano 9.0: editor de terminal real, segundo consumidor de ncurses tras htop/less
(API curses widec). deps.build=[ncurses, linux-headers]; gueto gcc; sale static-pie
y corre. Redondea el userland con un editor de verdad. --disable-libmagic/-nls
(libmagic/gettext no en corpus, sólo autodetección/i18n). Publicado al repo (66).
Pueblo el userland de ARRANQUE real del distro con los helpers del init arje
(hermanos de arje-zero PID 1), patrón Cargo commit 9967b02c --locked estático:
- arje-getty-stub: agetty mínimo (ciclo de vida del login).
- arje-net-bring-up: oneshot que sube el enlace de la primera interfaz (corre: eth0 up).
- arje-installer: copia kernel+initramfs+seed a una ESP / arma USB GPT booteable.
- arje-absorb: traduce la config de otro init a una Semilla brahman (migración a arje).
Los 4 construyen+corren estáticos, publicados al repo firmado (65 paquetes).
GOTCHA recetas Cargo con lib+bin: arje-installer tiene [lib]+[[bin]]; el lab hace
'cargo rustc -p X -- <crt-static>' y cargo exige UN solo target tras '--' => agregar
'--bin <name>' a flags. arje-loader DESCARTADO: bootloader EFI (no_std, target uefi),
no static-musl userland.
Sigo poblando con apps propias de tawasuyu (patrón Cargo, commit 9967b02c --locked,
estático zig-cc, bin==package, reusan el árbol fuente):
- tinkuy-sim: simulador de dinámica molecular Lennard-Jones; corre 200 steps de 343
partículas con reporte BLAKE3 por step (cómputo puro determinista).
- mirada-ctl: control CLI del compositor mirada (estilo swaymsg/hyprctl, cliente IPC).
Ambos construyen+corren estáticos, publicados al repo firmado (61 paquetes).
uya-cli DESCARTADO: requiere la lib de audio alsa (alsa-sys/libasound C), no en el
corpus -> fuera de alcance hasta portar esa lib. Texture honesta: no toda app
tawasuyu es static-musl pura; las que tocan audio/GPU/p2p necesitan libs o no aplican.
Hasta hoy el bootstrap del producto hidrataba el userland desde recetas locales
hardcodeadas (build_components sobre USERLAND_COMPONENTS/SERVICE_COMPONENTS). Cierre
del dogfood: la IMAGEN ahora puede armar su userland por la CADENA DE SUMINISTRO de
paquetes — verifica la firma del release y REPRODUCE cada componente desde su .swm.
hammer-bootstrap:
- product_from_repo(base, repo, trust, ...): igual que product() pero el userland +
servicios salen de install_components_from_repo en vez de build_components.
- install_components_from_repo: verifica firma del release (exige TRUSTED), por cada
componente resuelve el cierre de deps + puebla el catalogo dep.toml + reproduce el
source_patch con build_source_patch (chequea el expected_hash anclado). Devuelve los
mismos (nombre,hash) que build_components.
- seal_product_rootfs: pasos 2-3 comunes extraidos (seed+hash+ensamblado+sellado).
hammer-cli: bootstrap product --from-repo DIR --trust DIR.
PROPIEDAD CLAVE VERIFICADA E2E: como el hash es por-CONTENIDO, reproducir desde el
.swm da los mismos (nombre,hash) que construir la receta -> el product-rootfs via
repo firmado es BIT-IDENTICO al hardcodeado (ambas vias -> ba351f1b). Firma del
release verificada (TRUSTED by release) antes de tocar nada. "Verificar, no confiar"
aplicado al propio ensamblado de la imagen. 40 tests bootstrap verde.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Poblar el catálogo no es sólo CLI de terceros (Rust/C): tambien las APPS PROPIAS
del monorepo tawasuyu. dominium-cli es el runner headless del simulador físico de
dominium — patrón Cargo igual que arje-zero/llimphi-counter: source = tawasuyu a
commit fijado 9967b02c (con Cargo.lock committeado -> build --locked reproducible),
-p dominium-cli, estático zig-cc. bin==package (sin mismatch de workspace). Cómputo
puro (clap+serde+physics), sin GPU/red. Comparte commit con arje-zero -> reusa el
árbol fuente fetcheado.
Construye+corre: simulación de 100 ticks a 62k tps con métricas Gini/Moran.
Dogfood e2e: publicado al repo firmado (57 paq) -> install --require-signed ->
trusted -> reproduce desde fuente (hash casa) -> hidrata -> corre.
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.
htop 3.5.1 ejerce la API curses real de ncurses (ventanas/teclado/colores widec),
no sólo el lookup de terminfo de less. deps.build=[ncurses, linux-headers]; sale
static-pie y corre (--version). Valida ncurses como lib TUI completa del corpus.
Ajustes sobre el import: quito lm-sensors (opcional, no en corpus) con
--disable-sensors; gueto gcc (zig-cc miscompila); auto-detecta ncursesw widec.
ncurses es la lib fundacional del userland TUI (destraba less/htop/top/nano/ncdu).
La importo de Alpine pero RE-ANCLADA al release ESTABLE 6.5 de GNU (pineable; el
import traia el snapshot semanal de invisible-mirror, no reproducible) y
SIMPLIFICADA a estatico widec (--without-shared --enable-widec), sin el binding
C++ (rompe contra el libstdc++ del host) ni la maraña .so del APKBUILD. El install
crea los symlinks libncurses/libtinfo/libcurses -> libncursesw para que un
consumidor que enlaza -lncurses/-ltinfo resuelva.
less 704 es el primer CONSUMIDOR que lo valida: deps.build=[ncurses], enlaza
-ltinfo, sale static-pie y pagina. Confirma ncurses usable como lib del corpus.
Notas: gcc del lab = Alpine musl gcc (x86_64-alpine-linux-musl, no glibc) -> mezcla
limpia con objetos zig-cc/musl. gueto gcc en ambos (el build de ncurses corre tic
para generar terminfo; zig-cc lo miscompila). Corpus 53->55.
jq estaba bloqueado por su lib de regex faltante. Importo oniguruma de Alpine
(lib build-dep como zlib/libcap: el lab apila libonig.a + oniguruma.pc en /usr)
y cableo jq con deps.build=[oniguruma].
Fricciones C resueltas, documentadas en las recetas:
- oniguruma: el archive/ de GitHub no trae configure (Alpine corre autoreconf,
pide autoconf/automake/libtool ausentes del corpus); uso el tarball de RELEASE
que sí trae configure pregenerado -> sin autoreconf.
- jq: gueto gcc (zig-cc lo miscompila -> segfault, igual que file). Estático vía
libtool requiere -all-static en make Y en make install (libtool relinkea al
instalar y descartaba el flag -> binario dinámico). Ahora jq sale static-pie.
- el import emitia sed como build-dep; es herramienta del sandbox, no lib -> la quito.
Ambos construyen+corren: jq-1.8.1 static-pie evalua JSON. Corpus 51->53.
BUILD-YIELD C MEDIDO (no especulacion): 6/8 de la tanda construyen+corren como ELF
estatico musl. Promovidos: file 5.47, gawk 5.3.2, gzip 1.14, tar 1.35, tree 2.3.2,
which 2.23 (+ sus parches musl de Alpine).
Texture honesta del tier-2 C (la friccion que el tier-1 Rust no tiene):
- gawk/tar/tree/which: zig-cc directo, sin tocar nada.
- file: zig-cc MISCOMPILA -> el `file` recien hecho segfaultea generando magic.mgc
(mismo sintoma que binutils). Escape compiler="gcc" -> construye.
- gzip: (1) configure "C compiler cannot create executables" con zig-cc -> compiler="gcc";
(2) luego el install fallaba por `local i;` de la package() de Alpine.
- jq (oniguruma), sed (perl): NO build-friction sino dep faltante en el corpus
(completitud) -> quedan pendientes hasta importar esas libs.
Fix generico que destrabo gzip (y futuros): el importador Alpine ENVUELVE el cuerpo de
build()/package() en una funcion shell. abuild los corre COMO funciones (donde `local`
es valido); el lab corre la fase plana bajo sh -c, donde `local` fuera de funcion es
error. Envolver restaura el contexto de abuild sin tocar el sandbox ni las fases planas
del corpus (solo lo importado). test translate_wraps_body_in_function_for_local.
Confirmado: los 6 binarios corren (--version). file/gzip llevan compiler="gcc" en su
receta (escape declarativo, gueto conocido).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Arranca el tier-2 de la escalera (C clasico via Alpine, que trae los parches musl):
- import-batch.sh: env PREFER=nix|alpine. Para tandas C, PREFER=alpine antepone Alpine —
el import de nix de un C tiene exito (tarball) pero SIN parches musl => romperia al
construir. Refactor a 'tiers' ordenados (misma escalera, dos sentidos).
- tandas/cli-c.txt: primera tanda C (tree/which/sed/gawk/gzip/tar/jq/file).
- importador Alpine: build-deps = SOLO makedepends. El depends de abuild es RUNTIME
(gzip depends=less para zless) — no hace falta para compilar y rompia hammer build
(buscaba recipes/less.toml). Ahora va como comentario de provenance. Mismo patron que
el fix de buildInputs-de-nix en recetas Rust.
VALIDADO: import C 8/8 desde Alpine/main con parches musl bajados; gzip pierde el less
espurio. test extracts_source_patches_deps_phases actualizado (depends != build-dep).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Arranca el escalado del catálogo con la tanda tier-1 (tandas/cli-rust.txt) por el
pipeline completo: import-batch (escalera) → pin (tag→SHA) → build (medir) → promover.
BUILD-YIELD MEDIDO (no especulación): 11/12 de la tanda construyen+corren como ELF
estático musl en el lab. Los 11 promovidos al corpus:
bat 0.26.1, bottom 0.12.3, dust, fd 10.4.2, hexyl, hyperfine, procs, sd, tokei,
xh 0.25.3 (rustls, no openssl), zoxide.
(ripgrep ya estaba en el corpus con su patch jemalloc; no se duplica.)
El primer corte dio 8/12: los 4 con deps fallaban por buildInputs espurias de nix
(bat→zlib, fd→jemalloc, ripgrep→pcre2, xh→openssl) — backends C que el build Rust por
defecto NO usa. Arreglado en el importador (commit anterior): re-importados bat/fd/xh
SIN [deps] → los 3 construyen (bat 372s, fd 251s, xh 413s). Sube 8/12 → 11/12.
Verificado: los binarios CORREN (fd/bat/xh --version), estáticos musl stripped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hallazgo al escalar la primera tanda (12 CLI Rust): `hammer build` resuelve las deps
relativas al dir de la receta y aborta si falta `recipes/<dep>.toml`. Las recetas Rust
importadas arrastraban los buildInputs de nix como `[deps]` activas — pero nix lista el
closure MAXIMAL (todos los backends C opcionales: zlib/pcre2/openssl/jemalloc), mientras
el build Rust del lab usa las features DEFAULT de cargo (backend Rust puro: miniz_oxide
vs zlib, rustls vs openssl) o las deja opt-in (pcre2). bat→zlib, fd→jemalloc,
ripgrep→pcre2, xh→openssl fallaban al instante por deps espurias.
Las recetas Rust validadas del corpus (ripgrep/uutils) NO declaran `[deps]`: cargo
resuelve el grafo por vendoring; un sys-lib C que SÍ haga falta es adaptación per-paquete
(patch/feature, p.ej. ripgrep-no-jemalloc), no una dep de corpus.
- is_rust ⇒ los buildInputs quedan como COMENTARIO de provenance (no se pierden: señalan
qué C podría necesitarse), no como `[deps]`. Imports C (no-Rust) intactos.
- test rust_buildinputs_are_not_active_deps; filters_nix_stdenv_noise (C) sigue válido.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Faltaba el orquestador de "importar tandas a escala": las piezas per-paquete ya
existían (nix-import.sh, alpine-import.sh) pero sin un driver que aplicara la ESCALERA
DE FALLBACK en lote.
- import-batch.sh <pkg...> | -f <listfile>: por paquete intenta nix (Rust/Go/CLI,
~100% musl) y cae a Alpine main→community si el import falla (Alpine trae los parches
de musl). Mapeo opcional nixattr=alpinepkg. Escribe OUTDIR/<pkg>.toml.
- Mide el YIELD POR TIER (nix / alpine / fallaron) — el dato real al escalar, sin
especular. stdout limpio = nombres importados (encadenable con xargs/pin-recipes).
- Encadena con las piezas existentes: pin-recipes.sh (Fase 2 tag→SHA) + build-repo.sh
(Etapa F pack --build + repo firmado).
- tandas/cli-rust.txt: primera tanda curada (tier 1 Rust) para el modo -f.
VALIDADO REAL en vivo (nix 2.34.7): tanda {fd, hyperfine, jq} → yield 3/3, nix=2
(fd/hyperfine), alpine=1 (jq cae a main + baja test-portable.patch). pin en lote sobre
la tanda: fd v10.4.2 → SHA 7027d453, jq (tarball) salteado.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Los imports github salen con commit=tag flotante (v1.1.0); el pin lo resuelve al SHA inmutable
⇒ el laboratorio se vuelve determinista al estilo Nix (origen anclado a un punto fijo).
- hammer-cli: `hammer pin <recipe>` (in-place o --out). Usa `git ls-remote` (host-agnóstico, sin
API ni tokens ni rate-limits), prefiere el commit dereferenciado `^{}` para tags anotados.
Reescritura DIRIGIDA de la línea `commit = "<tag>"` (preserva comentarios/formato; no toca
version u otras que casen). No-op si ya es SHA (idempotente) o tarball (ya anclado por sha256).
is_git_sha (40 hex sha1 / 64 hex sha256). +1 test.
- scripts/pin-recipes.sh: ancla en lote (recipes/*.toml).
- Validado real: sd v1.1.0 → 4a7b216552d6… (git ls-remote), idempotente. 31 suites verde.
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 tier de mayor yield (Rust) salía como github sin --bin ni install ⇒ no buildeable. Ahora un
paquete buildRustPackage sale build-ready, con el patrón de la receta ripgrep.
- nix_import.rs: NixPkg gana is_rust + main_program. Si is_rust ⇒ flags=["--bin", <bin>] (bin =
meta.mainProgram, ripgrep→rg) + install "cp target/release/<bin> /out/usr/bin/<bin>". +1 test.
- nix-import.sh: detecta Rust por `hasAttr "cargoDeps" p`; main_program = meta.mainProgram or pname.
- Validado real: import fd → repo+commit, flags=["--bin","fd"], install template. Build-ready
(sólo el commit es tag, no SHA — refinamiento aparte). 31 suites verde.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nix resuelve `mirror://<sitio>/...` en eval; un import los deja literales y el curl de hammer no
los entiende. expand_nix_mirror() mapea los comunes (gnu/savannah/kernel/sourceforge/gnome/
apache/xorg/pypi/cpan/debian) a un espejo real; lo no mapeado se deja igual. +1 test.
Validado: import hello → tarball https://ftp.gnu.org/gnu/hello/... (antes mirror://gnu/...).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mueve las recetas de Alpine de "importan" a "casi buildean":
- alpine_import.rs: translate_abuild() en las fases — substituye $pkgdir→/out (el DESTDIR del lab),
$pkgname→nombre, $pkgver→versión. NO toca $CBUILD/$CHOST/--shared (juicio por-paquete, marcado).
+1 test.
- scripts/alpine-import.sh: baja el tarball UNA vez y calcula el sha256 (Alpine publica sha512,
hammer pide sha256), reemplazando el FIXME ⇒ receta lista sin tocar el hash a mano.
- VALIDADO real: import bzip2 → sha256 ab5a0317… resuelto, 5 parches musl bajados, install
traducido a /out. Recipe build-ready (sin $pkgdir ni FIXME en código). 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>
Segunda fuente del catálogo, y la RESPUESTA a "¿qué si el build falla en musl?": Alpine ya
porta miles de paquetes a musl CON los parches; su APKBUILD los trae. Un import de nix los pierde.
- crates/hammer-cli/alpine_import.rs: PARSEA el APKBUILD (no lo ejecuta) → receta hammer.
Extrae pkgname/pkgver (expande $var), la URL del tarball, LOS .patch (→ source.patches, lo
central), makedepends+depends → deps (filtra -dev, !negados, pins versionados, auto-refs),
build()/package() → fases. sha256 queda FIXME (Alpine publica sha512; el wrapper lo calcula). 3 tests.
- `hammer import-alpine [FILE|-]`; scripts/alpine-import.sh <pkg> [main|community] baja el
APKBUILD + sus .patch de aports.
- VALIDADO contra aports REAL: import coreutils 9.11 → patches renameat2-fakeroot.patch +
coreutils-9.10-dash-tests.patch BAJADOS a disco; deps limpias (acl/attr/bash/openssl/perl/utmps);
fases build/package capturadas. 31 suites verde.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Poblar el catálogo no es opcional: 34 recetas a mano = userland desierto. nixpkgs es el mayor set
de recetas DESDE FUENTE ⇒ semilla natural. Importamos la RECETA (source+hash+deps), nunca el
binario del cache de nix — hammer reconstruye desde fuente ("verificar, no confiar").
- crates/hammer-cli/nix_import.rs: consume el JSON normalizado de nix y emite una receta hammer.
Clasifica el origen: fetchurl flat → tarball+sha256 (convierte el hash nix SRI/base32/hex → hex);
fetchFromGitHub → repo+commit (hammer pinea por commit, no necesita el hash NAR). Filtra el ruido
de stdenv (setup-hooks, wrappers). nix_base32 decode portado. 11 tests.
- `hammer import-nix [FILE|-]` (stdin) → receta .toml; valida que parsee como Recipe.
- scripts/nix-import.sh <attr>: `nix eval --apply` produce el JSON normalizado y lo pipea al
importador. NIX_STORE= para store local si /nix/store no es escribible.
- VALIDADO contra nixpkgs REAL (nix 2.34): import hello (tarball, sha256→hex) + ripgrep (github→
repo+commit); pipeline completo nix→import→pack→.swm probado con hello. 31 suites verde.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Convierte la maquinaria de paquetería en la cadena de suministro real: cada receta del corpus
se vuelve un .swm, el repo es el catálogo firmado, install lo reproduce localmente.
- scripts/build-repo.sh: packea recipes/*.toml al repo, anclando expected_hash con `pack --build`
(cache-hit si el artefacto ya está sellado; fallback a pack sin ancla por timeout/fallo), y
firma el release (clave efímera o KEY=). Env: REPO/STORE/KEY/DISTRO/BUILD_TIMEOUT.
- /dist/ gitignoreado (el repo es artefacto reproducible desde el script).
- Validado: poblado de 34 paquetes (31 con expected_hash anclado), release firmado. install
ripgrep desde el repo FIRMADO en modo --require-signed → release trusted → reproduce
b3:9e6079fd (casa el ancla del catálogo) → hidrata → rg 14.1.1 corre. Cadena completa.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dos bordes ásperos de la paquetería:
- uninstall ahora retira los directorios que quedaron VACÍOS por el borrado (rmdir de abajo
arriba; remove_dir sólo borra dirs vacíos ⇒ se detiene solo al toparse con contenido de otro
paquete). Antes dejaba /usr/bin, etc. huérfanos.
- `install --require-signed`: modo estricto que ABORTA si el release no está firmado por una
clave confiada (Unsigned o UnknownKey ⇒ error). No basta con que el .swm reproduzca: exige
autoría verificada del catálogo. Default off (no rompe flujos sin firma).
Validado E2E: uninstall bwrap poda 4 dirs; --require-signed aborta sin firma y procede con
release trusted. 31 suites verde.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El repo son ficheros estáticos (index.json + .swm) ⇒ cualquier servidor estático lo sirve.
- hammer-cli: `RepoSource` {Local(path) | Http(url)}. `install --repo` ahora acepta path o URL.
Para HTTP: lee index.json por GET, materializa un repo LOCAL temporal bajando el índice + los
.swm del cierre de deps (curl, vía download::fetch_url_bytes), y de ahí el flujo es IDÉNTICO al
local (resolución de deps, verificación de release/firma/base, reproduce + hidrata). tempfile
pasa a dep normal de hammer-cli.
- Validado E2E: server HTTP estático + install openssh vía http:// → "release: trusted" (índice
firmado bajado por red) → "repo: bajados 3 .swm" (cierre openssh+zlib+openssl) → resuelve del
temporal. (El proxy del sandbox exige NO_PROXY para localhost; el código es correcto.) 31 verde.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Funcionalidad de gestor de paquetes: rastrear qué hay instalado y poder quitarlo.
- hammer-core/installed.rs: `InstalledDb` (name→{version,hash,files}) load/save JSON; record
(upsert), remove, `owned_by_others` (refcount por ruta). Rutas absolutas ⇒ uninstall no
necesita el root. 4 tests.
- hammer-cli: run_apply ahora DEVUELVE los ficheros que CREA (hidratados + file_drop + init_rule;
config_edit modifica, no crea ⇒ no se registra ni se deshace). install los registra en la DB
(--db, default /var/lib/hammer/installed.json). `uninstall <nombre>` borra esos ficheros salvo
los que otro paquete instalado aporta (refcount) y quita la entrada. `installed` lista.
- Validado E2E REAL: install bwrap (con dep libcap) → registra 2 ficheros → `installed` los
lista → `uninstall bwrap` los borra (prefix vacío, DB vacía). 31 suites verde.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cierra el hueco de seguridad: hoy se firmaba cada .swm (autoría del paquete) pero NO el
catálogo ⇒ un atacante podía añadir/quitar/intercambiar entradas del index.json. Firmar el
release ancla qué paquetes/versiones/hashes existen.
- hammer-core/sign.rs: extraídos `KeyPair::sign_raw` + `verify_raw` genéricos (bytes canónicos
arbitrarios); Swm::{sign,verify_signature} ahora los reusan (DRY, sin cambio de comportamiento).
- hammer-core/repo.rs: `RepoIndex.signature` (Ed25519 sobre la lista de paquetes canónica,
excluye la propia firma) + `sign`/`verify_signature`. `upsert` INVALIDA la firma (cualquier
cambio al catálogo ⇒ re-firmar). 4 tests (sign→verify, survive save/load, upsert-invalida,
tamper→BadSig).
- hammer-cli: `repo sign --key` / `repo verify`; `install` VERIFICA el release antes de resolver
(BadSig ⇒ aborta "el índice fue manipulado"); `repo list` muestra si está firmado.
- Validado E2E host: sin firmar→firmar→trusted→install lo ve; MANIPULAR el índice sin re-firmar
⇒ install ABORTA; re-publicar invalida la firma. 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 el lazo "packié un .swm → lo instalo por nombre". El repo es el namespace que
le da identidad a los .swm (que en sí no la llevan).
- hammer-core/repo.rs: `RepoIndex` + `PackageEntry` (load/save index.json, find, upsert
idempotente por nombre que reporta el .swm huérfano). Índice JSON plano, ordenado,
diffeable, firmable a futuro como release. 4 tests.
- hammer-cli:
* `pack --repo DIR` PUBLICA (escribe <repo>/<name>-<version>.swm + upsert al índice con
distro_version/expected_hash/signed_by; retira el huérfano de una versión vieja).
* `install <nombre> [--repo] [--trust] [--base-ref] [--prefix] [--skip-source-patch]`
CONSUME: resuelve nombre→.swm, verifica firma (con --trust) ANTES de reproducir, delega
en el camino de apply (reproduce source_patch + hidrata). Nunca corre binario ajeno.
Nombre inexistente → error legible con los disponibles.
* `repo list` imprime el catálogo.
- Validado E2E en host: publicar ripgrep (firmado) + findutils, repo list, index.json limpio,
install ripgrep --trust → "firma: trusted (by alice)" → apply OK. 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>
El modelo de generaciones es in-place (sin menu NixOS que ofrecer en GRUB); lo
que encaja es auto-sanar un upgrade interrumpido al boot. crates/hammer-recover
= mini-binario static-musl (lo unico que el producto necesita, sin el CLI hammer
completo): sin pending.json es no-op; con uno completa (roll-forward) o deshace
(roll-back) -> FHS siempre consistente; nunca aborta el boot. iso-image.sh
INSTALLER=1 lo compila (target musl) y bundlea al payload; hammer-live-install.sh
lo copia a /usr/sbin/hammer-recover y el wrapper /sbin/init instalado lo corre
tras montar /store y /var/lib/hammer, antes de incarnar arje-zero. iso-install-
test.sh valida el hook al boot (marker 'hammer-recover: sin upgrade interrumpido').
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El apply escribe el plan completo a pending.json ANTES de proyectar y lo limpia
al commitear; un corte a media proyección deja pending.json + FHS a medias. La
proyección se hizo re-entrante (project_plan, backups idempotentes via
backup_existing_once que nunca pisa el original) ⇒ recover COMPLETA (roll-forward,
re-verifica of_tree) o DESHACE (roll-back: restaura backups, borra la gen a
medias, current->padre). apply se niega con PendingExists si hay intento; status
lo avisa. CLI: hammer upgrade recover [--rollback]. 5 tests (corte a media
proyeccion -> ambos modos) + ejercicio en upgrade-e2e-test.sh. 14 tests verde.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
iso-image.sh EFI=1 añade un 2º El Torito EFI: ESP FAT (poblada con el mtools de
hammer, recipes/mtools.toml) con EFI/BOOT/BOOTX64.EFI = grubx64.efi
(grub-mkimage -O x86_64-efi). GRUB EFI lee el mismo grub.cfg y carga el kernel
EFI_STUB (ya en el kernel, sin rebuild) + initrd vía protocolo EFI. El mismo ISO
sigue booteando por BIOS (El Torito i386-pc) ⇒ híbrido. efi-boot-test.sh valida
ambas firmwares E2E (OVMF→grubx64.efi→sshd, y SeaBIOS→sshd).
GOTCHA: mformat -F fuerza FAT32 (min ~33MiB); sobre ESP chica deja FAT inválido
que la firmware no lee -> sin -F, auto FAT12/16.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Poblar una ESP FAT sin privilegios (ni loop-mount root) para el arranque UEFI
exige mtools (mformat/mcopy/mmd), ausente en el host. Pieza simétrica a xorriso
para la rama EFI. zig 0.13.0, estático musl, --without-x, sin deps (iconv
built-in de musl). Sella d7892990.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cierra el lazo ISO live -> disco instalado -> bootea solo. iso-image.sh
INSTALLER=1 bundlea kernel + GRUB MBR (boot/core.img + modulos) en el initramfs
bajo /usr/lib/hammer/install/ + inyecta /usr/bin/hammer-install. hammer-install
corre dentro del live como root real (busybox fdisk/mke2fs/mount/dd + uutils cp):
particiona MBR (/,/store,/var/lib/hammer), formatea, copia la propia raiz del
live (autoinstalador), escribe /boot+wrapper, instala GRUB con 2 dd (boot->MBR,
core->hueco post-MBR; punteros default 1/2 ya valen en MBR contiguo, sin parcheo).
AUTO_INSTALL=<dev> = /init desatendido (install+poweroff). iso-install-test.sh
valida E2E: ISO+disco blanco -> HAMMER-INSTALL-OK -> boot del disco solo ->
GRUB->kernel->arje-zero->sshd, particiones dedicadas montadas.
GOTCHAS: busybox fdisk CHS-alinea a 63 (hueco 62 < core 281 sectores) -> pisa el
FS de p1 -> grub>; fix sectores explicitos (p1@2048). Copiar top-level de /
enumerado, no lista fija, o se escapa /ente/seed.card.json.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
scripts/iso-image.sh arma un ISO 9660/El Torito isohybrid usando el xorriso
construido por hammer (recipes/xorriso.toml, dogfooding): GRUB core El Torito
(grub-mkimage -O i386-pc-eltorito + iso9660) carga kernel + initramfs del ISO;
el rootfs del producto va como cpio.gz (live de RAM). scripts/iso-boot-test.sh
valida E2E in-VM: SeaBIOS->GRUB 2.14->Linux 6.16.12->/init (marcador
HAMMER-ISO-LIVE-OK)->arje-zero PID1->hammerd->netup(DHCP)->sshd escuchando.
El producto entero corre desde el medio live, sin tocar disco. ISO 113M.
SDD 13 E5 marcado primer corte.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El host no trae xorriso (y grub-mkrescue lo exige) ⇒ se construye desde fuente
con hammer para fabricar el medio ISO. Tarball GNU (libburn+libisofs+libisoburn
en un árbol), zig 0.13.0, 100% estático musl, todas las libs opcionales
desactivadas (readline/edit/acl/xattr/zlib/bz2/cdio) ⇒ binario autocontenido sin
deps. Sella b63d1a64, corre standalone (xorriso 1.5.8 + personalidad xorrisofs
mkisofs-compatible). Mismo patrón -static/-target musl de openssh.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tras un rollback las generaciones más nuevas quedan inalcanzables (no hay redo).
prune(state, keep) borra esas huérfanas (la cadena viva = current+ancestros vía
parent siempre se conserva); --keep N recorta además la profundidad de rollback.
live_chain() expuesto. 2 tests nuevos + paso prune en upgrade-e2e-test.sh (3->1
gens, árbol vivo intacto).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Crate hammer-upgrade + CLI 'hammer upgrade apply|rollback|status'. Aplica un
árbol Stage1/producto del store sobre el FHS vivo dejando una generación
(manifest + backup de bytes previos), atómico fichero-a-fichero con 'current'
como commit-point. Rollback restaura EXACTO al árbol anterior (o pre-upgrades).
Cada cambio al journal (HammerHydrate, replay-able). Verificación opcional del
of_tree esperado (índice de mirror E3 / release firmada). Maneja ficheros +
symlinks. 7 tests unitarios + scripts/upgrade-e2e-test.sh (apply v1->v2->
rollback->rollback contra el binario real). SDD 13 actualizado.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>