# herramientas-desde-store.sh — las herramientas que ENSAMBLAN una imagen salen del STORE, no del host. # # ══ POR QUÉ EXISTE (medido el 2026-09-22) ══════════════════════════════════════════════════════ # Armar una imagen necesita `sfdisk mke2fs cpio gzip mformat mcopy xorriso` y, en la rama BIOS, # `grub-mkimage`/`grub-bios-setup`. Los scripts los tomaban del `PATH` del host — y **en este hub, en # su anfitrión y en el worker no existe NINGUNO de ellos**: # # grub-mkimage AUSENTE · grub-bios-setup AUSENTE · cpio AUSENTE · mformat AUSENTE · xorriso AUSENTE # # O sea que el pipeline de imagen estaba roto en las tres máquinas que tenemos, en silencio, hasta que # alguien lo corriera. Y lo absurdo es que **seis de las siete son nuestras**: `util-linux` (sfdisk), # `e2fsprogs` (mke2fs), `libarchive` (bsdcpio), `gzip`, `mtools`, `xorriso` y `dosfstools` están # SELLADAS en el store, son estáticas musl y corren tal cual en el host. Lo único que no tenemos es # el gestor de arranque de la rama BIOS (ver `vigia-ensamblado.sh`). # # ── EL ARTEFACTO SE ELIGE POR HASH, NO POR FECHA ───────────────────────────────────────────────── # `iso-image.sh` resolvía `xorriso` con `ls store/*-xorriso/...|head -1` y `install-image-efi.sh` # mtools con `ls -dt ...|head -1`. Con dos artefactos del mismo paquete conviviendo —lo normal tras # cualquier re-hash— el más reciente puede ser el viejo: **la fecha miente; el hash no**. Es la misma # lección que `hydrate-profile.py` y `libs-compartidas-desde-store.sh` ya habían pagado. # # ── EL HOST ES CAÍDA, NO FUENTE ───────────────────────────────────────────────────────────────── # Si no está en el store se cae al host, pero **avisando**: un binario del host no tiene hash, así que # la imagen deja de ser reproducible por ese lado y eso tiene que verse en el log de la corrida, no # descubrirse después. Si no está en ninguno de los dos, se ABORTA con las dos rutas nombradas. # # Uso: . scripts/lib/herramientas-desde-store.sh # SFDISK=$(herramienta util-linux sbin/sfdisk sfdisk) || exit 1 # CPIO=$(herramienta libarchive usr/bin/bsdcpio cpio) || exit 1 # Vars: STORE_ART (dónde buscar; por defecto se detecta) · TAKANA (binario; por defecto el del árbol) _herr_store() { # Se detecta UNA vez y se dice cuál se eligió: en el hub el `./store` del clon está vacío y el # store de verdad es `/store`, así que un default mudo manda a diagnosticar al lugar equivocado. [ -n "${STORE_ART:-}" ] && { echo "$STORE_ART"; return; } [ -n "${HAMMER_STORE:-}" ] && { echo "$HAMMER_STORE"; return; } for c in store /store; do if [ -d "$c" ] && [ -n "$(find "$c" -maxdepth 1 -name '*-*' -print -quit 2>/dev/null)" ]; then echo "$c"; return fi done echo store } _herr_takana() { for c in "${TAKANA:-}" ./target/release/takana /usr/bin/takana; do [ -n "$c" ] && [ -x "$c" ] && { echo "$c"; return; } done command -v takana 2>/dev/null || echo ./target/release/takana } # herramienta [nombre-en-el-host] herramienta() { _hr_receta="$1"; _hr_rel="$2"; _hr_host="${3:-}" # ── Override explícito: `TOOL_=/ruta` ──────────────────────────────────────────────── # Existe para los entornos donde NO HAY STORE y no puede haberlo: dentro del initramfs de un # instalador, por ejemplo, que lleva los binarios adentro porque el disco todavía no está # formateado. Ahí `herramienta` no puede calcular un hash ni leer el store, y caer al host es # caer a un host que tampoco existe. El override es explícito y se anuncia igual que el resto: # quien lo pone sabe lo que hace, y el log lo dice para que no se confunda con un sellado. _hr_bin="${_hr_rel##*/}" eval "_hr_ovr=\${TOOL_$(printf '%s' "$_hr_bin" | tr -c 'A-Za-z0-9_' '_'):-}" if [ -n "${_hr_ovr:-}" ] && [ -x "$_hr_ovr" ]; then echo " · $_hr_bin ← $_hr_ovr (TOOL_ explícito, sin store)" >&2 echo "$_hr_ovr"; return 0 fi _hr_store="$(_herr_store)"; _hr_tk="$(_herr_takana)" _hr_hash=$("$_hr_tk" --store "$_hr_store" hash "recipes/$_hr_receta.toml" 2>/dev/null \ | tail -1 | sed 's/^b3://') if [ -n "$_hr_hash" ]; then _hr_p="$_hr_store/$_hr_hash-$_hr_receta/$_hr_rel" if [ -x "$_hr_p" ]; then echo " · ${_hr_rel##*/} ← $_hr_receta $(echo "$_hr_hash" | cut -c1-12) (sellado)" >&2 echo "$_hr_p"; return 0 fi fi if [ -n "$_hr_host" ] && command -v "$_hr_host" >/dev/null 2>&1; then # El aviso se lee también donde no se está armando ninguna imagen —`takana metal survey` # en una máquina ajena cae acá siempre, porque ahí NO HAY store y no puede haberlo—, así # que dice qué pasó y no qué significa: «sin hash» vale en los dos casos; «la imagen no es # reproducible» sonaba a error para el que sólo está mirando el disco. echo " ⚠ ${_hr_rel##*/} ← $(command -v "$_hr_host") del HOST, sin hash (si estás armando una imagen, por acá deja de ser reproducible)" >&2 command -v "$_hr_host"; return 0 fi echo " ✗ falta ${_hr_rel##*/}: ni sellado ($_hr_store/$_hr_hash-$_hr_receta/$_hr_rel) ni en el PATH ($_hr_host)" >&2 echo " construilo con: flock work/.farm-build.lock ./target/release/takana --store $_hr_store build recipes/$_hr_receta.toml" >&2 return 1 } # ── sonda_hardlink ─────────────────────────────────────────────────── # El staging de una imagen ENLAZA (hardlink) desde el rootfs, y `linkat()` no cruza un punto de # montaje aunque sea el mismo disco ([[hardlink-cruza-mount-exdev]]). Sin sonda, el fallo llega como # una PARED de `cp: Invalid cross-device link` fichero por fichero —cientos de líneas— y el diagnóstico # real («el STAGE está en otro montaje que el ROOTFS») no aparece en ninguna de ellas. Medido el # 2026-09-22 armando una imagen EFI con el rootfs en /store y el stage en /work. # # La sonda es FUNCIONAL —se intenta un enlace de verdad— y no una comparación de `st_dev`: en este # repo `store` y `work/out` son bind-mounts del MISMO dispositivo y `st_dev` coincide, así que # comparar números no dispara. Es la misma sonda que `hydrate-profile.py` ya tenía. sonda_hardlink() { _sh_src="$1"; _sh_dst="$2" mkdir -p "$_sh_dst" 2>/dev/null || true _sh_a="$_sh_src/.sonda-hardlink.$$"; _sh_b="$_sh_dst/.sonda-hardlink.$$" if ! : > "$_sh_a" 2>/dev/null; then # origen de sólo lectura (un artefacto del store): se prueba con un fichero que YA está _sh_a=$(find "$_sh_src" -maxdepth 2 -type f -print -quit 2>/dev/null) [ -n "$_sh_a" ] || { echo "sonda_hardlink: $_sh_src no tiene ningún fichero con el que probar" >&2; return 1; } _sh_limpiar=no else _sh_limpiar=si fi if ln "$_sh_a" "$_sh_b" 2>/dev/null; then rm -f "$_sh_b"; [ "$_sh_limpiar" = si ] && rm -f "$_sh_a" return 0 fi [ "$_sh_limpiar" = si ] && rm -f "$_sh_a" echo " ✗ EXDEV: no se puede ENLAZAR de '$_sh_src' a '$_sh_dst'." >&2 echo " El staging usa hardlinks y un enlace no cruza un montaje aunque sea el mismo disco." >&2 echo " origen está en: $(findmnt -n -T "$_sh_src" -o TARGET 2>/dev/null || echo '?')" >&2 echo " destino está en: $(findmnt -n -T "$_sh_dst" -o TARGET 2>/dev/null || echo '?')" >&2 echo " Poné STAGE= (y IMG=) bajo el MISMO montaje que el rootfs." >&2 return 1 }