Files
takana/scripts/atuq-nested.sh
T
SergioandClaude Opus 5 6886b1541c libnotify: el primer hueco que el mapa del §6.10 encontró Y cerró
`libxul.so` lleva la cadena `libnotify.so.4` adentro y la abre por dlopen cuando
una página pide permiso para notificar. Nadie en el corpus la proveía, así que la
función quedaba apagada SIN UN SOLO MENSAJE: el navegador arranca, la web pide
notificaciones, y no pasa nada. No lo veía ninguna herramienta porque no hay
NEEDED en ningún ELF — es el tercer escalón, y sobrevivió a vigia-sonames con los
cinco perfiles en CERO.

Receta nueva, 0.8.8, SÓLO en variante compartida y eso no es un olvido: a un
`dlopen("libnotify.so.4")` una `.a` no le sirve de nada, así que una receta
estática sería un artefacto que nadie puede consumir.

Dos cosas que la receta se comió y quedan escritas:

1. `libpng-shared` hace falta porque las deps de hammer NO son transitivas: el
   `.pc` de gdk-pixbuf-2.0 declara `Requires: libpng` y el meson muere con un
   mensaje que nombra a gdk-pixbuf —que sí está— en vez de a lo que falta.
2. El guardián informaba «18 bytes» y parecía una librería vacía: `stat -c%s` no
   sigue el symlink, y `libnotify.so.4` apunta a `libnotify.so.4.0.0`. El
   artefacto estaba bien y el MENSAJE mentía. Con `-L` son 162.928 bytes. Se
   arregla el mensaje porque es lo que alguien va a leer a las tres de la mañana,
   y de paso el guardián exige el fichero real, no sólo el nombre.

⚠ Y lo que NO arregla, escrito en la receta para que nadie lea de más: libnotify
no trae daemon, manda org.freedesktop.Notifications por D-Bus. Tenerla resuelve
la mitad —que firefox la encuentre—; la otra mitad es que en la imagen haya
alguien escuchando ese nombre.

Verificado con el propio mapa: `--dlopen` pasa de 25 huecos a 24 y libnotify.so.4
desaparece; el soname viejo `.so.1` se reclasifica de «hueco» a «ruido», que es lo
que ahora es. El guardián normal sigue en CERO con un soname más pedido (66).

Queda pendiente la membresía de perfil en `docs/state/targets.toml`, que es
catálogo compartido y no lo toco sin decidirlo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UgNtJEFetMYbXax5dVUjZZ
2026-09-07 00:51:56 +00:00

150 lines
9.6 KiB
Bash
Executable File

#!/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
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.
# `grim` no lo necesita atuq: lo necesita QUIEN LO MIRA. Con `CAPTURA=<fichero.png>` 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"
if [ "${REHIDRATAR:-0}" = "1" ] || [ ! -f "$SELLO" ] || [ "$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"
echo "$VIGENTES" | while read -r d; do
[ -n "$d" ] && "$H" --store "$STORE" hydrate "${d%%-*}" --into "$RFS" >/dev/null
done
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
[ -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
'