gnome: la cima ARRANCA en QEMU — muere en meta_launcher_new, causa localizada

gnome-shell bootea, compila sus esquemas, toma el DRM de virtio-gpu y se declara
Wayland display server. Después SIGSEGV. El backtrace del core no deja dudas:

  #4 g_variant_get (value=0x0, "(s&o)")     ← GVariant NULO
  #5 get_seat_proxy   src/backends/meta-launcher.c:407
  #6 meta_launcher_new (META_LAUNCHER_FLAG_TAKE_CONTROL)

Mutter pide la propiedad `Seat` del objeto **Session** de logind — `(s&o)` es su
firma estándar (nombre_del_seat, object_path). arje-logind-compat adquiere
org.freedesktop.login1 y sirve el Manager, pero NO expone un objeto Session con
esa propiedad ⇒ la lectura devuelve NULL y mutter desreferencia sin chequear. Que
mutter no valide es fragilidad suya; el hueco es nuestro.

Cinco eslabones hubo que armar antes de llegar a ese muro, ninguno anotado:
  1. gschemas.compiled NO se genera con DESTDIR seteado (meson lo dice en el log
     del build) ⇒ GSettings abortaba en el primer g_settings_new().
  2. GI_TYPELIB_PATH tiene que incluir /usr/lib/gnome-shell: St/Shell/Gvc/Shew se
     instalan aparte por ser privados del shell. Es el env sin análogo en KDE.
  3. /var/run no existía en la base metal; arje-logind-compat busca el bus en la
     ruta legacy y sin el symlink se iba a "modo idle".
  4. La política D-Bus de login1 faltaba (system.conf trae <deny own="*"/> y
     normalmente la instala systemd). PERTENECE al artefacto de arje: está en el
     script de imagen sólo para dejar visible qué falta empaquetar.
  5. /run/systemd/{sessions,seats,users} vacíos — libelogind resuelve la C-ABI
     sd-login LEYÉNDOLOS y nadie los escribe (el Announce de arje-logind-compat al
     bus del fractal falla con "identity mismatch"). gnome-start los escribe A
     MANO y está MARCADO COMO ANDAMIO. Con ese puente desaparece el "Failed to
     find any matching session".

Gotcha que costó una iteración: `kill -0` TIENE ÉXITO sobre un zombi (el padre no
lo cosechó todavía), así que el script reportaba "sin wayland-0 tras 45s" cuando
el shell había muerto en el primer segundo. Mirando State: de /proc/<pid>/status
sale el código real, 139.

Todo el ciclo (hidratar → imagen → bootear → sacar el core con debugfs → gdb) y
la lista de lo que falta quedan en docs/runbooks/gnome-qemu-desktop.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-28 21:47:11 -04:00
co-authored by Claude Opus 5
parent f6734d6ce6
commit 5295aa35c6
3 changed files with 482 additions and 0 deletions
+93
View File
@@ -0,0 +1,93 @@
# Runbook — arrancar gnome-shell en QEMU (y dónde está el muro hoy)
Estado al 2026-07-29: **gnome-shell arranca, toma el rol de display server Wayland y muere de
SIGSEGV en `meta_launcher_new`**. La causa está localizada con precisión (ver abajo); no es un
misterio pendiente de diagnosticar, es una pieza de arje que falta implementar.
Hermano de [`kde-qemu-desktop.md`](kde-qemu-desktop.md), del que reusa el andamiaje: la base
`work/metal-rootfs`, la inyección de mesa-llvmpipe/LLVM/musl, el truco del console-getty y el propio
`scripts/kde/run-qemu-desktop.sh`.
## Ciclo completo
```sh
cd ~/hammer
bash scripts/gnome/hydrate-gnome.sh # cierre de gnome-shell → work/gnome-rootfs (108/108)
bash scripts/gnome/qemu-desktop-image.sh # funde base+gnome, parchea, → work/hammer-gnome-qemu.img
IMG=work/hammer-gnome-qemu.img SERIAL=work/gnome-qemu-serial.log MON=work/gnome-monitor.sock \
TIMEOUT=150 DISP=none bash scripts/kde/run-qemu-desktop.sh
grep -a "gnome-qemu ::" work/gnome-qemu-serial.log | awk '!seen[$0]++'
```
`DISP=gtk` para ver la ventana (necesita `DISPLAY=:0 GDK_BACKEND=x11`). **Regla heredada de KDE:
validar escritorios con PANTALLA, nunca sólo por serial** — pero hoy no hace falta, porque no llega
a pintar.
## Sacar el core y el backtrace
El `gnome-start` deja el core en la raíz ext4 y hace `sync`, así que sobrevive al SIGKILL de QEMU.
```sh
sfdisk -J work/hammer-gnome-qemu.img # localizar hammer-root (part 2)
dd if=work/hammer-gnome-qemu.img of=/tmp/root.img bs=1M skip=129 count=5113 status=none
debugfs -R "dump /core.gnome-shell.<pid> /tmp/core.gnome-shell" /tmp/root.img
gdb -batch -q -ex "set sysroot $PWD/work/gnome-qemu-rootfs" \
-ex "core-file /tmp/core.gnome-shell" -ex "bt 25" work/gnome-qemu-rootfs/usr/bin/gnome-shell
```
## EL MURO DE HOY — y por qué no es un bug de mutter que haya que rodear
```
#4 g_variant_get (value=0x0, format_string="(s&o)") ← GVariant NULO
#5 get_seat_proxy (...) at src/backends/meta-launcher.c:407
#6 meta_launcher_new (flags=META_LAUNCHER_FLAG_TAKE_CONTROL) at meta-launcher.c:464
...
#15 main () at src/main.c:664
```
Mutter pide la propiedad `Seat` del objeto **Session** de logind — el `(s&o)` es la firma estándar
de esa propiedad: `(nombre_del_seat, object_path_del_seat)`. `arje-logind-compat` adquiere
`org.freedesktop.login1` y sirve el **Manager**, pero no expone un objeto Session con esa propiedad,
así que la lectura devuelve NULL. Mutter no chequea y desreferencia ⇒ SIGSEGV.
Que mutter no valide es una fragilidad suya, pero **el hueco es nuestro**: falta el objeto Session.
### La cadena completa que hubo que armar para llegar hasta acá
Cada eslabón se descubrió arrancando la imagen, y ninguno estaba anotado antes:
1. **`gschemas.compiled` no existe.** meson NO lo genera cuando `DESTDIR` está seteado (lo dice en
el log del build: *"Skipping custom install script because DESTDIR is set"*). Sin él GSettings
aborta en el primer `g_settings_new()`. Se compila en el primer arranque.
2. **`GI_TYPELIB_PATH` con `/usr/lib/gnome-shell`.** St/Shell/Gvc/Shew se instalan APARTE de
`/usr/lib/girepository-1.0` porque son privados del shell. Es el env que no tiene análogo en KDE.
3. **`/var/run` no existía.** `arje-logind-compat` busca el bus en la ruta legacy
`/var/run/dbus/system_bus_socket`; sin el symlink reporta `bus_present=false` y se va a modo idle.
4. **Política D-Bus de `login1` ausente.** `system.conf` trae `<deny own="*"/>`; normalmente systemd
instala el fichero de política del servicio. Acá no hay systemd y el artefacto de
arje-logind-compat es sólo el binario ⇒ hubo que escribirla. **Pertenece al artefacto de arje,
no a la imagen**; está en el script de imagen sólo para que se vea qué falta empaquetar.
5. **`/run/systemd/{sessions,seats,users}` vacíos.** `libelogind` (arje-sdlogin-compat) resuelve la
C-ABI sd-login LEYENDO esos ficheros, y nadie los escribe: el `Announce` de arje-logind-compat al
bus del fractal falla con `identity mismatch`, así que nunca hay sesión registrada. El
`gnome-start` los escribe A MANO — **eso es ANDAMIO, y está marcado como tal en el script**. Con
ese puente desaparece el `Failed to setup: Failed to find any matching session`.
6. **Y entonces aparece el muro real**: el objeto Session de D-Bus (arriba).
### Gotcha de diagnóstico que costó una iteración
`kill -0 <pid>` **tiene éxito sobre un ZOMBI** — el hijo existe como PID hasta que el padre lo
cosecha, y el padre es el propio `gnome-start`, que sólo hace `wait` al final. La primera versión del
script usaba `kill -0` y reportaba *"sin wayland-0 tras 45s"* cuando en realidad el shell había
muerto en el primer segundo. Se mira el `State:` de `/proc/<pid>/status` (vacío o `Z` = muerto), y
así sale el código real: **139 = SIGSEGV**.
## Lo que falta, en orden
1. **Objeto Session en `arje-logind-compat`** con la propiedad `Seat` (y `TakeControl`/`TakeDevice`
sobre él, que es lo que mutter llama enseguida para el DRM master). Esto destraba el arranque.
2. **Que escriba `/run/systemd/{sessions,seats,users}`** en vez del puente a mano — y de paso
arreglar el `identity mismatch` del Announce.
3. **Empaquetar la política D-Bus** dentro del artefacto de arje-logind-compat.
4. Recién después: el muro gráfico (GBM/EGL software sobre virtio-gpu), donde KDE ya dejó los fixes
pagos y precargados en `gnome-start-qemu.sh` (`GBM_ALWAYS_SOFTWARE`, `kms_swrast`, `LP_NUM_THREADS=1`).
+217
View File
@@ -0,0 +1,217 @@
#!/bin/sh
# gnome-start-qemu.sh — arranca gnome-shell (Wayland) en una VM QEMU con render por SOFTWARE
# (kms_swrast/llvmpipe) sobre el DRM de virtio-gpu. Análogo de plasma-start-qemu.sh, y hereda sus
# lecciones caras: el diagnóstico va POR SERIAL porque la ventana gtk se congela si mirada reinicia
# su Xwayland (QEMU sigue vivo por dentro). Ver [[kde-metal-qemu-desktop]].
#
# Se instala como /usr/bin/gnome-start y lo ejecuta DIRECTO el console-getty (no vía login shell).
# Salir: matar gnome-shell. Logs: /tmp/gnome-shell.log + tee al serial.
#
# DIFERENCIA CLAVE CON PLASMA: acá NO hay dos procesos (compositor + shell). gnome-shell ES el
# compositor — embebe mutter como librería (libmutter-16.so) y `--wayland` lo hace display server.
# Por eso no hay un equivalente de "kwin arriba pero plasmashell murió": si el shell cae, cae todo.
set -u
# getty ejecuta este script DIRECTO ⇒ PATH viene vacío. Primero eso, antes de invocar nada.
export PATH=/usr/bin:/bin:/usr/sbin:/sbin
say() { echo "== gnome-qemu :: $*" | tee -a /dev/console /dev/ttyS0 2>/dev/null; }
dump() { echo "---- $1 ----" > /dev/ttyS0 2>/dev/null; cat "$1" > /dev/ttyS0 2>/dev/null; }
export LD_LIBRARY_PATH=/usr/lib:/lib:/usr/lib/gnome-shell:/usr/lib/mutter-16
# GI_TYPELIB_PATH es EL env crítico de esta sesión, y el que no tiene análogo en KDE: gnome-shell es
# JavaScript sobre gjs y carga TODO por typelib. Los del sistema van en /usr/lib/girepository-1.0,
# pero St/Shell/Gvc/Shew se instalan APARTE en /usr/lib/gnome-shell (son privados del shell). Sin
# esta ruta, gjs no encuentra imports.gi.St y el shell muere en el primer require.
export GI_TYPELIB_PATH=/usr/lib/girepository-1.0:/usr/lib/gnome-shell:/usr/lib/gnome-shell/girepository-1.0
export XDG_DATA_DIRS=/usr/share
export XDG_RUNTIME_DIR=/run/user/0
export XDG_CONFIG_HOME=/root/.config
export XDG_CACHE_HOME=/root/.cache
export XDG_SESSION_TYPE=wayland
export HOME=/root
export GSETTINGS_SCHEMA_DIR=/usr/share/glib-2.0/schemas
export LANG=C.UTF-8
# --- render por software sobre virtio-gpu (los mismos fixes que costaron la campaña KDE) ---
export LIBGL_ALWAYS_SOFTWARE=1
export GALLIUM_DRIVER=llvmpipe
# GBM_ALWAYS_SOFTWARE=1 — virtio-gpu-pci SIN 3D expone un nodo KMS-only. Sin esto gbm carga kms_swrast
# pero deja gbm_dri->software=FALSE y dri2_initialize_drm entra al bloque `if(!software)` llamando
# queryCompatibleRenderOnlyDeviceFd(), NULL en el driver software ⇒ SIGSEGV. Fue el MURO 2 de KDE.
export GBM_ALWAYS_SOFTWARE=1
# kms_swrast y NO swrast: el compositor abre card0 como KMS y crea un gbm device encima ⇒ necesita el
# driver software PARA KMS (dumb buffers). Con 'swrast' pelado gbm_create_device falla.
export MESA_LOADER_DRIVER_OVERRIDE=kms_swrast
export LIBGL_DRIVERS_PATH=/usr/lib/dri
# llvmpipe SINCRÓNICO: su JIT multi-thread segfaultea al componer la 1ra superficie en esta VM.
export LP_NUM_THREADS=1
# Análogos mutter de dos fixes de kwin: cursor por software (el plano de cursor por HW no se presenta
# en virtio-gpu con render software ⇒ cursor invisible) y modeset simple en vez de atomic.
export MUTTER_DEBUG_DISABLE_HW_CURSORS=1
export MUTTER_DEBUG_FORCE_KMS_MODE=simple
if [ "${DIAG:-1}" = 1 ]; then
export G_MESSAGES_DEBUG=all
export MUTTER_DEBUG=1
export EGL_LOG_LEVEL=debug
export LIBGL_DEBUG=verbose
fi
mkdir -p "$XDG_RUNTIME_DIR" "$XDG_CONFIG_HOME" "$XDG_CACHE_HOME" /run/dbus /var/lib/dbus /root
chmod 700 "$XDG_RUNTIME_DIR"
[ -s /etc/machine-id ] || cat /proc/sys/kernel/random/uuid 2>/dev/null | tr -d - > /etc/machine-id
cp -f /etc/machine-id /var/lib/dbus/machine-id 2>/dev/null || true
# gschemas.compiled: meson NO lo genera cuando DESTDIR está seteado (lo dice en el log del build:
# "Skipping custom install script because DESTDIR is set"). Sin él, GSettings aborta al primer
# g_settings_new() y el shell no llega ni a abrir el DRM. Se compila acá, en el primer arranque.
if [ ! -f "$GSETTINGS_SCHEMA_DIR/gschemas.compiled" ]; then
say "compilando $(ls $GSETTINGS_SCHEMA_DIR/*.xml 2>/dev/null | wc -l) esquemas GSettings..."
glib-compile-schemas "$GSETTINGS_SCHEMA_DIR" 2>/tmp/schemas.log \
&& say "esquemas OK" || { say "!! glib-compile-schemas FALLÓ:"; dump /tmp/schemas.log; }
fi
say "GPU DRM: $(ls /dev/dri/ 2>/dev/null | tr '\n' ' ')"
[ -e /dev/dri/card0 ] || say "!! no hay /dev/dri/card0 — ¿virtio-gpu no cargó?"
# D-Bus de SISTEMA: mutter habla con logind por el bus de sistema. En esta distro login1 lo provee
# arje-logind-compat; si no está, mutter cae a su ruta sin sesión (que es justo lo que libelogind
# vino a cubrir del lado de la C-ABI). El bus tiene que existir igual.
if [ ! -S /run/dbus/system_bus_socket ]; then
if dbus-daemon --system --fork 2>/tmp/dbus-system.log; then
say "system bus ON"
else
say "!! system bus NO arrancó:"; dump /tmp/dbus-system.log
fi
fi
# D-Bus de sesión: gnome-shell EXIGE uno (registra org.gnome.Shell y org.gnome.Mutter.*).
if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]; then
DBUS_SESSION_BUS_ADDRESS=$(dbus-daemon --session --fork --print-address 2>/dev/null)
export DBUS_SESSION_BUS_ADDRESS
fi
say "dbus sesión: ${DBUS_SESSION_BUS_ADDRESS:-NO ARRANCO}"
# arje-logind-compat: el login1 del fractal. Va DESPUÉS del bus de sistema (registra
# org.freedesktop.login1 ahí) y ANTES del shell. Mutter no sólo lo consulta por D-Bus: pregunta por
# SU sesión vía la C-ABI sd-login, que libelogind resuelve LEYENDO /run/systemd/sessions. Sin este
# daemon esos ficheros no existen y mutter muere en "Failed to find any matching session".
if [ -x /usr/bin/arje-logind-compat ]; then
/usr/bin/arje-logind-compat >/tmp/logind-compat.log 2>&1 &
i=0
while [ $i -lt 40 ]; do
[ -d /run/systemd/sessions ] && [ -n "$(ls /run/systemd/sessions 2>/dev/null)" ] && break
i=$((i+1)); sleep 0.25
done
say "logind-compat: sesiones=$(ls /run/systemd/sessions 2>/dev/null | tr '\n' ' ') seats=$(ls /run/systemd/seats 2>/dev/null | tr '\n' ' ')"
[ -n "$(ls /run/systemd/sessions 2>/dev/null)" ] || dump /tmp/logind-compat.log
else
say "!! sin /usr/bin/arje-logind-compat — mutter no va a hallar sesión"
fi
# ── PUENTE EXPLÍCITO: el estado de sesión que arje-logind-compat todavía no escribe ─────────────
# ESTO ES ANDAMIO, NO LA SOLUCIÓN. arje-logind-compat adquiere org.freedesktop.login1 y sirve el
# Manager por D-Bus, pero NO deja nada en /run/systemd/{sessions,seats,users} — su handshake con el
# bus de arje falla ("Announce respuesta inesperada: identity mismatch"), así que nunca hay una
# sesión registrada por el fractal. Y mutter no pregunta por D-Bus: usa la C-ABI sd-login, que
# libelogind (arje-sdlogin-compat) resuelve LEYENDO esos ficheros. De ahí "Failed to find any
# matching session" aun con login1 arriba.
#
# Escribimos el estado a mano, en el formato que libelogind lee (claves sacadas del propio .so:
# LEADER/SEAT/CLASS/TYPE/ACTIVE/STATE/VTNR y CAN_GRAPHICAL del seat). Sirve para PROBAR que el resto
# de la pila funciona; el arreglo de verdad es que arje-logind-compat registre la sesión del fractal
# —o que el Announce deje de fallar— y escriba esto solo. Queda marcado para no confundir andamio
# con producto.
if [ -z "$(ls /run/systemd/sessions 2>/dev/null)" ]; then
say "puente: escribiendo estado de sesión a mano (arje-logind-compat no lo hizo)"
mkdir -p /run/systemd/sessions /run/systemd/seats /run/systemd/users
cat > /run/systemd/sessions/1 <<EOF
UID=0
NAME=root
LEADER=$$
SEAT=seat0
TYPE=wayland
CLASS=user
ACTIVE=1
STATE=active
VTNR=1
REMOTE=0
EOF
cat > /run/systemd/seats/seat0 <<EOF
CAN_GRAPHICAL=1
CAN_MULTI_SESSION=1
ACTIVE=1
ACTIVE_SESSIONS=1
SESSIONS=1
EOF
cat > /run/systemd/users/0 <<EOF
NAME=root
STATE=active
SESSIONS=1
ACTIVE_SESSIONS=1
EOF
fi
# XDG_SESSION_ID/SEAT/VTNR: libelogind los menciona entre sus símbolos, y son además el contrato
# estándar por el que un proceso sabe cuál es SU sesión sin consultar por PID.
export XDG_SESSION_ID=1
export XDG_SEAT=seat0
export XDG_VTNR=1
if [ "${DIAG:-1}" = 1 ]; then
ulimit -c unlimited 2>/dev/null || true
echo '/core.%e.%p' > /proc/sys/kernel/core_pattern 2>/dev/null || true
fi
say "typelibs visibles: $(ls /usr/lib/girepository-1.0/*.typelib 2>/dev/null | wc -l) sistema + $(ls /usr/lib/gnome-shell/*.typelib 2>/dev/null | wc -l) del shell"
say "lanzando gnome-shell --wayland (DRM virtio, software GL)..."
/usr/bin/gnome-shell --wayland >/tmp/gnome-shell.log 2>&1 &
SHELL_PID=$!
# stream EN VIVO al serial (busybox sed no soporta -u ⇒ tail -f directo, sin pipes).
( echo "==== gnome-shell.log (stream vivo) ===="; tail -f /tmp/gnome-shell.log ) > /dev/ttyS0 2>/dev/null &
# OJO con el test de vivo: `kill -0` TIENE ÉXITO sobre un ZOMBI (el hijo existe como PID hasta que
# el padre lo cosecha, y acá el padre es este script, que sólo hace wait al final). La primera
# versión usaba kill -0 y por eso reportó "sin wayland-0 tras 45s" cuando en realidad gnome-shell ya
# había muerto en el primer segundo: el diagnóstico decía "0 hilos" porque no quedaba proceso, sólo
# la entrada zombi. Se mira el State de /proc en su lugar.
alive() { s=$(sed -n 's/^State:[[:space:]]*//p' /proc/$1/status 2>/dev/null); case "$s" in ""|Z*) return 1;; *) return 0;; esac; }
i=0
while [ $i -lt 90 ]; do
[ -S "$XDG_RUNTIME_DIR/wayland-0" ] && break
alive $SHELL_PID || { wait $SHELL_PID; RC=$?; say "!! gnome-shell murió (código $RC) antes de exponer wayland-0:"; dump /tmp/gnome-shell.log; say "cores: $(ls /core.* 2>/dev/null | tr '\n' ' ' || echo NINGUNO)"; exec /bin/sh; }
i=$((i+1)); sleep 0.5
done
if [ -S "$XDG_RUNTIME_DIR/wayland-0" ]; then
say "compositor OK (wayland-0) — el shell ES el display server"
else
say "!! sin wayland-0 tras 45s:"; dump /tmp/gnome-shell.log
# El shell NO murió: quedó COLGADO. Sin gdb en la imagen, el diagnóstico barato es dónde está
# bloqueado el hilo principal (wchan) y contra qué socket, que es lo que distingue "esperando una
# respuesta D-Bus que no llega" de "spinning en el renderer".
say "--- diagnóstico del cuelgue (pid $SHELL_PID) ---"
say "wchan: $(cat /proc/$SHELL_PID/wchan 2>/dev/null)"
say "state: $(grep -a ^State /proc/$SHELL_PID/status 2>/dev/null)"
say "stack principal:"; cat /proc/$SHELL_PID/stack > /dev/ttyS0 2>/dev/null
say "syscall: $(cat /proc/$SHELL_PID/syscall 2>/dev/null)"
say "fds abiertos:"; ls -l /proc/$SHELL_PID/fd 2>/dev/null > /dev/ttyS0
say "hilos: $(ls /proc/$SHELL_PID/task 2>/dev/null | wc -l)"
for t in $(ls /proc/$SHELL_PID/task 2>/dev/null); do
say " tid $t wchan=$(cat /proc/$SHELL_PID/task/$t/wchan 2>/dev/null) comm=$(cat /proc/$SHELL_PID/task/$t/comm 2>/dev/null)"
done
exec /bin/sh
fi
export WAYLAND_DISPLAY=wayland-0
( n=0; while sleep 15; do
n=$((n+1))
say "STATUS +$((n*15))s: gnome-shell=$(pgrep gnome-shell 2>/dev/null | wc -l)"
done ) &
wait $SHELL_PID
RC=$?
say "gnome-shell salió (código $RC) — volcando log:"
dump /tmp/gnome-shell.log
cp /tmp/gnome-shell.log /gnome-shell.log 2>/dev/null || true
say "cores en /: $(ls -la /core.* 2>/dev/null | tr '\n' ' ' || echo NINGUNO)"
sync; sync
say "== disco SYNCED. serial CONGELADO. matá QEMU desde el host. =="
sleep 3600
+172
View File
@@ -0,0 +1,172 @@
#!/usr/bin/env bash
# qemu-desktop-image.sh — imagen de disco EFI GNOME para QEMU (virtio-gpu + software GL), auto-lanza
# gnome-shell. Hermano de scripts/kde/qemu-desktop-image.sh; comparte con él la base metal, la
# inyección de mesa/LLVM/musl y el truco del console-getty, y sólo diverge en lo que GNOME necesita.
#
# work/metal-rootfs base: arje-zero PID1 + busybox + kernel(EFI-stub) + firmware
# work/gnome-rootfs cierre de gnome-shell (lo arma scripts/gnome/hydrate-gnome.sh, 108/108)
# + store/*-mesa-llvmpipe GBM/EGL de software (el rootfs GNOME sólo trae iris_dri, que es Intel real)
# + store/*-llvm18 libLLVM que llvmpipe JIT-linkea
# + store/*-dejavu-fonts el rootfs GNOME NO trae NINGUNA fuente
# → work/gnome-qemu-rootfs (hardlinks) → work/hammer-gnome-qemu.img
#
# Uso: scripts/gnome/qemu-desktop-image.sh
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"; cd "$ROOT"
BASE="${BASE:-work/metal-rootfs}"
GNOME="${GNOME:-work/gnome-rootfs}"
MESA="${MESA:-$(ls -dt store/*-mesa-llvmpipe 2>/dev/null | head -1)}"
LLVM="${LLVM:-$(ls -dt store/*-llvm18 2>/dev/null | head -1)}"
FONTS="${FONTS:-$(ls -dt store/*-dejavu-fonts 2>/dev/null | head -1)}"
MERGED="${MERGED:-work/gnome-qemu-rootfs}"
IMG="${IMG:-work/hammer-gnome-qemu.img}"
[ -d "$BASE" ] || { echo "falta $BASE" >&2; exit 1; }
[ -d "$GNOME" ] || { echo "falta $GNOME — corré scripts/gnome/hydrate-gnome.sh" >&2; exit 1; }
[ -n "$MESA" ] && [ -d "$MESA" ] || { echo "falta mesa-llvmpipe en store/" >&2; exit 1; }
echo "==> fundiendo rootfs por hardlinks (base metal + cierre GNOME)"
rm -rf "$MERGED"
cp -al "$BASE" "$MERGED"
cp -al --remove-destination "$GNOME"/. "$MERGED"/
echo "==> parcheando mesa software desde $MESA"
# El cierre GNOME trae mesa con SÓLO iris_dri.so (Intel real). En QEMU no hay GPU: hace falta
# kms_swrast. Se copian los .so de mesa-llvmpipe ENCIMA (--remove-destination rompe el hardlink).
mkdir -p "$MERGED"/usr/lib/dri
cp -a --remove-destination "$MESA"/usr/lib/dri/. "$MERGED"/usr/lib/dri/ 2>/dev/null || true
for so in libglapi libgbm libEGL libGLESv2 libGL libgallium; do
cp -a --remove-destination "$MESA"/usr/lib/$so* "$MERGED"/usr/lib/ 2>/dev/null || true
done
ls "$MERGED"/usr/lib/dri/*swrast* >/dev/null 2>&1 && echo " ✓ kms_swrast instalado" || { echo " ✗ swrast no entró" >&2; exit 1; }
echo "==> inyectando libLLVM.18 (llvmpipe lo JIT-linkea)"
[ -n "$LLVM" ] && [ -d "$LLVM" ] || { echo " ✗ falta llvm18 en store/ (set LLVM=)" >&2; exit 1; }
cp -a --remove-destination "$LLVM"/usr/lib/libLLVM.so.18.1 "$MERGED"/usr/lib/
ln -sf libLLVM.so.18.1 "$MERGED"/usr/lib/libLLVM.so.18
ln -sf libLLVM.so.18.1 "$MERGED"/usr/lib/libLLVM.so
echo "==> inyectando el runtime C++ de Alpine (deuda CONOCIDA, no accidente)"
# libLLVM y mesa-llvmpipe se construyen con gcc ⇒ NEEDean libstdc++/libgcc_s. Y además —esto es
# propio de GNOME y quedó documentado al auditar el cierre— **libmozjs-128.so también los NEEDea**,
# o sea que la cadena gnome-shell → libgjs → libmozjs arrastra el runtime C++ de Alpine. Es la
# "última milla" de matar-gcc que se cerró para cmake con -static-libstdc++ y que spidermonkey no
# cubrió. Mientras eso no se re-selle (radio medido: 2), la imagen los inyecta o el shell no carga.
for lib in libgcc_s.so.1 libstdc++.so.6; do
src=".dev-fs/alpine/usr/lib/$lib"
if [ -r "$src" ]; then cp -a --remove-destination "$src"* "$MERGED"/usr/lib/ 2>/dev/null || true
else echo " ⚠ sin $lib ($src)"; fi
done
echo "==> inyectando musl — y los DOS sonames"
# La base metal es 100% estática (arje-zero+busybox) y el cierre GNOME no trae libc. En musl el
# loader ES libc (mismo fichero). Van dos nombres porque conviven dos toolchains: zig-cc emite
# NEEDED `libc.so` (359 consumidores) y el gcc/clang de Alpine emite `libc.musl-x86_64.so.1`
# (17: todo nss + spidermonkey). Es la MISMA libc; sin el segundo symlink esos 17 no cargan.
MUSL="${MUSL:-/usr/lib/musl/lib/libc.so}"
[ -r "$MUSL" ] || { echo "no encuentro musl libc ($MUSL) — set MUSL=" >&2; exit 1; }
mkdir -p "$MERGED/usr/lib/musl/lib" "$MERGED/lib"
cp -a --remove-destination "$MUSL" "$MERGED/usr/lib/musl/lib/libc.so"
ln -sf /usr/lib/musl/lib/libc.so "$MERGED/lib/ld-musl-x86_64.so.1"
ln -sf /usr/lib/musl/lib/libc.so "$MERGED/usr/lib/libc.so"
ln -sf /usr/lib/musl/lib/libc.so "$MERGED/usr/lib/libc.musl-x86_64.so.1"
echo " ✓ libc.so + libc.musl-x86_64.so.1"
if [ -n "$FONTS" ] && [ -d "$FONTS" ]; then
echo "==> inyectando fuentes ($(basename "$FONTS"))"
cp -a --remove-destination "$FONTS"/usr/share/fonts/. "$MERGED"/usr/share/fonts/ 2>/dev/null || \
{ mkdir -p "$MERGED"/usr/share/fonts && cp -a "$FONTS"/usr/share/fonts/. "$MERGED"/usr/share/fonts/; }
else
echo " ⚠ sin dejavu-fonts en el store: el escritorio saldrá con tofu"
fi
echo "==> inyectando arje-logind-compat (el login1 del fractal)"
# EL ESLABÓN QUE FALTABA, descubierto arrancando la imagen: mutter levanta como display server y
# muere en `Failed to setup: Failed to find any matching session`. Ese chequeo es de logind — mutter
# pregunta por su sesión vía la C-ABI sd-login, que acá provee libelogind (arje-sdlogin-compat)
# LEYENDO /run/systemd/{sessions,users,seats}. Pero alguien tiene que ESCRIBIR esos ficheros, y ese
# alguien es arje-logind-compat, el daemon D-Bus que se hace pasar por org.freedesktop.login1.
# libelogind entra solo (es dep de mutter); el daemon no, porque nadie lo declara como dep de build.
ALC="${ALC:-$(ls -dt store/*-arje-logind-compat 2>/dev/null | head -1)}"
if [ -n "$ALC" ] && [ -d "$ALC" ]; then
install -Dm755 "$ALC"/usr/bin/arje-logind-compat "$MERGED"/usr/bin/arje-logind-compat
echo "$(basename "$ALC" | cut -c1-12)"
else
echo " ✗ falta arje-logind-compat en store/ — mutter no hallará sesión" >&2; exit 1
fi
echo "==> política D-Bus para org.freedesktop.login1"
# Segundo muro del mismo eslabón: con el bus ya visible, dbus RECHAZA que el daemon se adueñe del
# nombre — "not allowed to own the service org.freedesktop.login1 due to security policies". El
# system.conf de dbus trae `<deny own="*"/>` por defecto: cada servicio de sistema aporta su propio
# fichero de política, y systemd normalmente instala el de login1. Acá no hay systemd, y el artefacto
# de arje-logind-compat es sólo el binario ⇒ nadie pone la política. La escribimos: root puede
# adueñarse del nombre, y cualquiera puede hablarle (mutter consulta login1 sin ser root).
# TODO: esto pertenece al artefacto de arje-logind-compat, no a la imagen; acá está de forma
# explícita para que se vea qué falta empaquetar del lado de arje.
install -d "$MERGED/usr/share/dbus-1/system.d"
cat > "$MERGED/usr/share/dbus-1/system.d/org.freedesktop.login1.conf" <<'XML'
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<policy user="root">
<allow own="org.freedesktop.login1"/>
</policy>
<policy context="default">
<allow send_destination="org.freedesktop.login1"/>
<allow receive_sender="org.freedesktop.login1"/>
</policy>
</busconfig>
XML
chmod 644 "$MERGED/usr/share/dbus-1/system.d/org.freedesktop.login1.conf"
echo " ✓ org.freedesktop.login1.conf"
echo "==> usuario messagebus (el system bus hace setuid a él)"
for f in passwd:messagebus:x:81:81:messagebus:/dev/null:/sbin/nologin group:messagebus:x:81:; do
file="${f%%:*}"; line="${f#*:}"
if ! grep -q '^messagebus:' "$MERGED/etc/$file" 2>/dev/null; then
tmp="$MERGED/etc/$file.new"
{ cat "$MERGED/etc/$file"; echo "$line"; } > "$tmp"
chmod 644 "$tmp"; mv "$tmp" "$MERGED/etc/$file"
fi
done
echo "==> instalando /usr/bin/gnome-start"
install -Dm755 scripts/gnome/gnome-start-qemu.sh "$MERGED/usr/bin/gnome-start"
echo "==> auto-lanzar GNOME: console-getty ejecuta gnome-start (no /bin/sh)"
python3 - "$MERGED/ente/seed.card.json" <<'PY'
import json, sys, os
p = sys.argv[1]
c = json.load(open(p))
for g in c.get("genesis", []):
if g.get("label") == "console-getty":
argv = g["payload"]["Native"]["argv"]
argv[:] = [("/usr/bin/gnome-start" if a == "/bin/sh" else a) for a in argv]
print(" console-getty argv →", argv)
tmp = p + ".new"
with open(tmp, "w") as f:
json.dump(c, f, indent=2)
os.chmod(tmp, 0o644)
os.replace(tmp, p)
PY
# /var/run → /run: arje-logind-compat busca el bus de sistema en la ruta LEGACY
# (/var/run/dbus/system_bus_socket) y dbus-daemon escucha en /run/dbus/system_bus_socket. En casi
# toda distro /var/run es un symlink a /run desde hace 15 años; esta base metal es tan mínima que ni
# siquiera tiene /var/run. Sin el enlace el daemon reporta `bus_present=false` y se va a "modo idle",
# o sea que nunca registra login1 ni escribe /run/systemd/sessions ⇒ mutter no halla sesión.
ln -sfn /run "$MERGED/var/run"
mkdir -p "$MERGED/root/.config" "$MERGED/root/.cache" "$MERGED/run/user/0" "$MERGED/tmp"
chmod 1777 "$MERGED/tmp"
echo "==> rootfs fundido: $(du -sh "$MERGED" | cut -f1)"
need_mb=$(du -sm "$MERGED" | cut -f1)
ROOT_SIZE="${ROOT_SIZE:-$(( need_mb + 1536 ))}"
echo "==> ROOT_SIZE=${ROOT_SIZE} MiB"
export ROOTFS="$MERGED" IMG ROOT_SIZE
export ESP_MB="${ESP_MB:-128}" STORE_SIZE="${STORE_SIZE:-512}" STATE_SIZE="${STATE_SIZE:-768}"
exec ./scripts/install-image-efi.sh