From 5295aa35c6c4a0ef057aa2fc751f964bfda5df0f Mon Sep 17 00:00:00 2001 From: sergio Date: Tue, 28 Jul 2026 21:47:11 -0400 Subject: [PATCH] =?UTF-8?q?gnome:=20la=20cima=20ARRANCA=20en=20QEMU=20?= =?UTF-8?q?=E2=80=94=20muere=20en=20meta=5Flauncher=5Fnew,=20causa=20local?= =?UTF-8?q?izada?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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//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) --- docs/runbooks/gnome-qemu-desktop.md | 93 ++++++++++++ scripts/gnome/gnome-start-qemu.sh | 217 ++++++++++++++++++++++++++++ scripts/gnome/qemu-desktop-image.sh | 172 ++++++++++++++++++++++ 3 files changed, 482 insertions(+) create mode 100644 docs/runbooks/gnome-qemu-desktop.md create mode 100755 scripts/gnome/gnome-start-qemu.sh create mode 100755 scripts/gnome/qemu-desktop-image.sh diff --git a/docs/runbooks/gnome-qemu-desktop.md b/docs/runbooks/gnome-qemu-desktop.md new file mode 100644 index 00000000..6f690d53 --- /dev/null +++ b/docs/runbooks/gnome-qemu-desktop.md @@ -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. /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 ``; 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 ` **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//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`). diff --git a/scripts/gnome/gnome-start-qemu.sh b/scripts/gnome/gnome-start-qemu.sh new file mode 100755 index 00000000..1c4ba993 --- /dev/null +++ b/scripts/gnome/gnome-start-qemu.sh @@ -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 < /run/systemd/seats/seat0 < /run/systemd/users/0 </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 diff --git a/scripts/gnome/qemu-desktop-image.sh b/scripts/gnome/qemu-desktop-image.sh new file mode 100755 index 00000000..26cc6f76 --- /dev/null +++ b/scripts/gnome/qemu-desktop-image.sh @@ -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 `` 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' + + + + + + + + + + +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