#!/bin/sh # atuq-nested.sh — hidrata `atuq` con su cierre de runtime y lo abre como VENTANA ANIDADA en el # compositor que ya tenés delante (waypipe, mirada, sway…). No toca la GPU como master: atuq es un # cliente wayland más. # # scripts/atuq-nested.sh # abre atuq y lo deja corriendo SECS segundos # SECS=60 scripts/atuq-nested.sh # # ── POR QUÉ HIDRATAR Y NO EJECUTAR DESDE EL STORE ────────────────────────────────────────────── # El binario es musl y el host es glibc: no hay `/lib/ld-musl-x86_64.so.1` afuera. Un artefacto que # no arranca desde la ruta del store NO está roto — corre HIDRATADO, y ésa es la prueba que vale. # # ── LAS FUENTES NO SON OPCIONALES ────────────────────────────────────────────────────────────── # Un navegador sin una sola fuente arranca, pinta y muestra cuadraditos. Es el modo de fallo que ya # nos costó una tarde en GNOME: la clausura decía 100% y la pantalla decía otra cosa. `dejavu-fonts` # va en la lista de raíces por eso, no por completismo. set -eu ROOT="$(cd "$(dirname "$0")/.." && pwd)"; cd "$ROOT" H="$ROOT/target/release/hammer" # ── EL ROOTFS VA EN EL MISMO MOUNT QUE EL STORE, Y NO ES UN CAPRICHO ─────────────────────────── # `hydrate` proyecta con HARDLINKS, y `linkat()` rechaza cruzar un punto de montaje aunque los dos # lados sean el mismo filesystem. Acá el store es `/dev/sdb` bind-monteado en `hammer/store` # mientras `work/` vive en `/dev/sdc`: hidratar a `work/…` muere con # «Invalid cross-device link (os error 18)» — medido, no supuesto. # El volumen entero está montado en /mnt/cosecha, así que store y destino comparten mount ahí. # Se comprueba con `findmnt -T`, NUNCA con `stat -c %d` (que dice el device, no el mount). STORE="${STORE:-/mnt/cosecha/store}" RFS="${RFS:-/mnt/cosecha/escritorios/atuq-rootfs}" ALPINE="$ROOT/.dev-fs/alpine" SECS="${SECS:-25}" URL="${URL:-about:support}" # Las raíces: atuq y lo que su motor abre en RUNTIME. `wayland-protocols` y compañía no están —son # de build— y meterlas engordaría el rootfs sin cambiar nada de lo que se ve. # Las variantes `-shared` y NO las estáticas: en runtime una `.a` no sirve para nada, y son las que # gtk3 declara NEEDED desde que se arregló el cuadro de las dos Pango. RAICES="atuq gcc-libs gtk3 atk gdk-pixbuf-shared pango-shared cairo-shared libepoxy glib-shared pcre2-shared libffi-shared zlib-shared harfbuzz-shared fribidi freetype-shared fontconfig-shared pixman libpng-shared libjpeg-turbo-shared wayland libxkbcommon mesa libdrm dbus pipewire pulseaudio alsa-lib libxml2-shared dejavu-fonts bzip2-shared expat-shared ncurses-shared libsndfile libudev-zero zstd dbus-shared libnotify ffmpeg libva xkeyboard-config grim" # ⚠ `gcc-libs` FALTABA, y su ausencia hacía MENTIR a esta prueba entera (2026-09-07). `atuq` declara # `NEEDED libstdc++.so.6` y `libgcc_s.so.1` —lo hereda de firefox, que va con clang++ y la libstdc++ # COMPARTIDA de Alpine—, y esas dos sólo existían en el lab. Como la jaula monta `.dev-fs/alpine` # como capa de abajo, el navegador arrancaba resolviéndolas contra el LAB y todos decíamos «atuq # corre». Corría, sí: en una máquina con Alpine debajo, que es exactamente lo que la distro no es. # La clausura ya estaba bien —`recipes/atuq.toml` declara `runtime = ["gcc-libs"]` y el vigía de # sonames da CERO—, lo que estaba mal era ESTA lista, o sea el instrumento y no el artefacto. # Se nota a simple vista porque los dos ficheros no se parecen: el del corpus pesa 20.850.968 bytes # y el del lab 2.804.104. Si algún día hay duda de cuál se cargó, se mira el tamaño. # # La segunda tanda salió del guardián que nació de ese fallo, `scripts/test-atuq-rootfs.py`, que # mira el rootfs HIDRATADO en vez del grafo: `bzip2-shared expat-shared ncurses-shared libsndfile # libudev-zero zstd dbus-shared` — siete sonames más que también se resolvían contra el lab. # `libexpat` y `libzstd` los pide mesa (iris_dri, libEGL, libgbm), o sea el camino de pintado; # `libdbus-1.so.3` lo pide pipewire y NO lo trae `dbus` sino `dbus-shared`, el mismo cuadro de las # variantes `-shared` de gtk3. Mantener esta lista a mano es lo que falla: por eso ahora hay un # guardián que la audita en vez de confiar en que alguien se acuerde. # `ffmpeg` y `libva` entran el 2026-09-07 y NO son recetas nuevas: las dos estaban selladas en el # corpus y ya viven en la CLAUSURA de los cuatro escritorios, arrastradas por `mpv`. Lo que faltaba # era esta lista, o sea otra vez el instrumento. Firefox abre `libavcodec.so.<53..63>` por `dlopen` # literal —está en las cadenas de `libxul.so`— y nuestro ffmpeg 7.1 publica `libavcodec.so.61`, o # sea H.264/AAC por software. Sin estas dos raíces el runner mide un rootfs MÁS POBRE que la imagen # y reporta un hueco que la imagen no tiene. Ver SDD 26 §6.10. # ⚠ `libva` va por simetría con la imagen, no porque encienda VA-API: las tres mesa construyen con # `-Dgallium-va=disabled -Dvideo-codecs=`, así que el cargador está y no hay UN SOLO driver # (`*_drv_video.so`) que cargar. Es media función, como libnotify sin daemon. # `grim` no lo necesita atuq: lo necesita QUIEN LO MIRA. Con `CAPTURA=` este script # saca la pantalla real desde dentro de la jaula, contra el mismo socket wayland. Es lo que separó # «el proceso vive» de «la ventana muestra lo que queremos»: durante horas dije «mirá tu pantalla» # y la ventana estaba ahí desde el principio — lo que fallaba era el branding DENTRO del zip. # La receta vive en `recipes/incoming-wlr/grim.toml`; se resuelve más abajo con el resto. # `xkeyboard-config` son DATOS, no una librería, y sin ellos el navegador ni pinta: # xkbcommon: ERROR: failed to add default include path /usr/share/X11/xkb # Es el mismo modo de fallo silencioso que las fuentes: nada en la clausura lo ve venir. # ── EL ROOTFS SE REVALIDA POR HASH, NO POR EXISTENCIA ────────────────────────────────────────── # «Ya está hidratado» es la pregunta equivocada: tras cualquier re-hash del corpus el directorio # sigue ahí, con los artefactos VIEJOS, y el script abriría contento la versión que acabás de # arreglar. Es la misma forma del cache-hit que congela regresiones. La pregunta correcta es si los # hashes VIGENTES son los que están puestos, así que se anota la lista resuelta y se compara. VIGENTES="" faltan="" for r in $RAICES; do # La receta puede vivir en el corpus o en una cola: `grim` es de `incoming-wlr`. Se prueban las # dos, en ese orden, que es el mismo que usa la resolución de deps (sibling-first, luego padre). rec="recipes/$r.toml"; [ -f "$rec" ] || rec="recipes/incoming-wlr/$r.toml" h="$("$H" --store "$STORE" hash "$rec" 2>/dev/null | tail -1 | sed 's/^b3://')" if [ -z "$h" ] || [ ! -d "$STORE/$h-$r" ]; then faltan="$faltan $r"; continue; fi VIGENTES="$VIGENTES$h-$r " done # Un nodo sin artefacto NO se saltea en silencio: un rootfs al que le faltan tres paquetes arranca # y falla tres capas más abajo. [ -z "$faltan" ] || { echo "✗ faltan artefactos:$faltan" >&2; exit 1; } SELLO="$RFS/.raices" # ⚠ LOS DOS LADOS DE LA COMPARACIÓN TIENEN QUE PASAR POR LA MISMA TRITURADORA. `$VIGENTES` termina # en `\n` y `$(cat …)` RECORTA los `\n` finales ⇒ comparados en crudo NUNCA son iguales y el rootfs # se rehidrataba entero en CADA corrida (3013 ficheros), diciendo «DESACTUALIZADO» aunque estuviera # perfecto. La caché existía y no cacheaba nada; medido 2026-09-07. Metiendo el de la izquierda en # otra sustitución de comandos, los dos pierden el mismo `\n` y la comparación vuelve a significar # lo que dice. if [ "${REHIDRATAR:-0}" = "1" ] || [ ! -f "$SELLO" ] || [ "$(printf '%s' "$VIGENTES")" != "$(cat "$SELLO")" ]; then # Nada de `diff <(...)`: esto es /bin/sh, y la sustitución de proceso es de bash. Un script que # sólo funciona cuando /bin/sh resulta ser bash es una trampa que salta en otra máquina. [ -f "$SELLO" ] && echo "==> el rootfs está DESACTUALIZADO — se rehidrata" echo "==> hidratando $(echo $RAICES | wc -w) raíces en $RFS" rm -rf "$RFS"; mkdir -p "$RFS" # ⚠ ESTE BUCLE SALÍA 1 SIEMPRE, Y CON `set -e` MATABA EL SCRIPT SIN IMPRIMIR NADA (2026-09-07). # `$VIGENTES` termina en `\n` y `echo` agrega otro ⇒ la última vuelta lee la línea VACÍA, el # `[ -n "$d" ]` da 1, ése es el estado de la última vuelta, ése es el estado del `while`, ése es # el del pipeline, y `set -e` sale. Justo DESPUÉS de hidratar bien las 41 raíces: la única señal # era `exit=1` tras «==> hidratando». No se veía porque esta rama sólo corre cuando cambian los # hashes —con el sello al día el script ni entra—, así que el bug vivía escondido detrás de la # caché. Lo destapó agregar `ffmpeg` a las raíces. Ahora: `continue` en la vacía, y un `hydrate` # que falla GRITA en vez de dejar un rootfs a medias con el sello sin escribir. echo "$VIGENTES" | while read -r d; do [ -n "$d" ] || continue "$H" --store "$STORE" hydrate "${d%%-*}" --into "$RFS" >/dev/null || { echo "✗ hydrate falló en $d" >&2; exit 1; } done || exit 1 printf '%s' "$VIGENTES" > "$SELLO" echo "==> hidratado: $(find "$RFS" -type f | wc -l) ficheros" else echo "==> rootfs al día ($(echo "$VIGENTES" | grep -c . ) raíces, hashes vigentes)" fi # `ROOTFS_ONLY=1` corta acá: deja el rootfs hidratado y al día, y no abre ventana ni pide pantalla. # Lo usa `scripts/test-atuq-codecs.sh`, que corre headless y necesita el MISMO árbol que esta # ventana — duplicar la lista de raíces en dos scripts es exactamente cómo se desincronizan. [ "${ROOTFS_ONLY:-0}" = "1" ] && exit 0 [ -n "${WAYLAND_DISPLAY:-}" ] || { echo "✗ no hay WAYLAND_DISPLAY — esto necesita una pantalla" >&2; exit 1; } SOCK="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/$WAYLAND_DISPLAY" [ -S "$SOCK" ] || { echo "✗ no existe el socket $SOCK" >&2; exit 1; } echo "==> pantalla: $SOCK" exec bwrap --overlay-src "$ALPINE" --overlay-src "$RFS" --tmp-overlay / \ --dev-bind /dev /dev --proc /proc --bind /sys /sys --tmpfs /tmp --tmpfs /run \ --ro-bind "$SOCK" /run/wayland-0 \ --bind "$(dirname "${CAPTURA:-/tmp/atuq-captura.png}")" /salida \ --uid 0 --gid 0 \ --setenv PATH /usr/bin:/bin:/usr/sbin:/sbin \ --setenv LD_LIBRARY_PATH /usr/lib:/lib \ --setenv XDG_RUNTIME_DIR /run --setenv WAYLAND_DISPLAY wayland-0 \ --setenv XDG_DATA_DIRS /usr/share --setenv HOME /root \ --setenv GDK_BACKEND wayland --setenv MOZ_ENABLE_WAYLAND 1 \ --setenv LIBGL_DRIVERS_PATH /usr/lib/dri \ --setenv MOZ_LOG_FILE /tmp/atuq-moz.log \ sh -c ' mkdir -p /root /run/cache /etc [ -s /etc/machine-id ] || tr -d - < /proc/sys/kernel/random/uuid > /etc/machine-id echo "=== identidad del artefacto ===" sed -n "/^\[App\]/,/^$/p" /usr/lib/atuq/application.ini echo "=== fuentes visibles ===" fc-list 2>/dev/null | wc -l timeout '"$SECS"' /usr/bin/atuq --no-remote '"$URL"' >/tmp/atuq.log 2>&1 & PID=$! sleep 8 echo "=== ¿vive? ===" if kill -0 $PID 2>/dev/null; then echo " ✓ atuq corriendo (PID $PID) — mirá tu pantalla"; else echo " ✗ murió"; fi echo "=== log ==="; tail -25 /tmp/atuq.log # ── LA PRUEBA EN EL PÍXEL ───────────────────────────────────────────────────────────────── # Un proceso vivo NO es una ventana correcta. Se captura la pantalla entera (grim sin `-o` # compone todas las salidas) y se deja el PNG afuera, para mirarlo de verdad. if [ -n "'"${CAPTURA:-}"'" ]; then sleep 4 if grim "/salida/'"$(basename "${CAPTURA:-captura.png}")"'" 2>/dev/null; then echo "captura: $(wc -c < "/salida/'"$(basename "${CAPTURA:-captura.png}")"'") bytes → '"${CAPTURA:-}"'" else echo "captura: grim NO pudo — ¿el compositor expone wlr-screencopy?" fi fi wait $PID 2>/dev/null || true '