docs/evidencia/gnome-shell-qemu-2026-07-29.png: el conmutador de espacios de trabajo, el reloj en el panel, la miniatura del escritorio, el dash, y la notificación estándar de GNOME por sesión de root. Todo construido desde fuente sobre el kernel de hammer, arje-zero como PID1 y musl. **EL MURO NO ERA DE KMS.** El diagnóstico anterior culpaba al plano primario de virtio-gpu («no advertised formats», sólo `Queue mode set`, ningún page flip). Era correlación. Se probó quitando MUTTER_DEBUG_FORCE_KMS_MODE=simple —el log pasó a decir `using atomic mode setting`, o sea que el cambio SÍ tomó efecto— y la pantalla siguió con exactamente 2 colores. Refutada. Mutter no tenía un frame que presentar; no es que no supiera presentarlo. La causa estaba en el stack trace, en el log, desde el principio: la excepción por el gschema `org.gnome.settings-daemon.peripherals.touchscreen` ocurre DENTRO de `Main.start()`, construyendo los quick settings ⇒ el panel nunca se arma. Proceso vivo, compositor con DRM master, nada que pintar. Receta `gsd-schemas`: los gschemas de gnome-settings-daemon SIN g-s-d, que sigue aparcada por GTK3/X11. **Tercera vez que aparece la misma lección** (tras la política D-Bus de login1 y el `org.gnome.login-screen` de libgdm): un gschema no es un fichero de datos del demonio, es una interfaz publicada que consume otro programa. Dos trampas de diagnóstico, las dos ahora blindadas: - **`glib-compile-schemas` sale con 0 aunque RECHACE un esquema.** Instalé los once XML, el log dijo `esquemas OK` y el shell seguía diciendo `not found`: faltaba `org.gnome.settings-daemon.enums.xml`, que meson genera con glib-mkenums y no es uno de los `.in`. Sin los `<enum>`, glib descarta el esquema entero, avisa por stderr y sigue. `gnome-start` ahora vuelca ese stderr SIEMPRE, no sólo al fallar. - El log serial persiste entre arranques y me hizo leer dos veces un `STATUS +30s` de una VM ya muerta. Truncarlo antes de arrancar; `Failed to get "write" lock` avisa de que hay otro QEMU con el disco tomado. Y queda escrito cómo validar CON PANTALLA sin humano delante: screendump por el monitor de QEMU, con una métrica barata previa a mirar — contar colores distintos. Negro = 2; pintando = 582, con el azul GNOME (2,60,136) al frente. Lo que falta son detalles, ninguno impide el escritorio: lanzar upowerd desde gnome-start, un tema de cursor, el setuid de colord y gnome-control-center. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
14 KiB
Runbook — GNOME en QEMU: el escritorio PINTA
Estado al 2026-07-29: cerrado. gnome-shell arranca desde fuente sobre el kernel de hammer, toma
el DRM master y los tres dispositivos de input por logind, expone wayland-0, y dibuja el Overview
de GNOME. Cinco muros caídos, cada uno por medición y no por conjetura:
| muro | causa real | cómo se midió |
|---|---|---|
SIGSEGV en get_seat_proxy |
object path de la Session mal escapado (_1 en vez de _31) |
el cliente calcula el path solo y no pregunta |
| cuelgue de la hebra de input | el fd de TakeDevice sin O_NONBLOCK |
wchan=evdev_read en /proc, no el core |
Requiring <X> en bucle |
7 typelibs de runtime + 2 capas más abajo | js/misc/dependencies.js + cierre de <include> |
| pantalla negra | Main.start() abortaba por un gschema de g-s-d |
el stack trace, que estaba en el log desde el principio |
| (descartado) KMS legacy | NO era la causa | atomic tomó efecto y la pantalla siguió con 2 colores |
Hermano de 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
cd ~/hammer
bash scripts/gnome/hydrate-gnome.sh # cierre de gnome-shell + las raíces de RUNTIME → work/gnome-rootfs
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 — y sin humano delante se cumple con el
screendump del monitor de QEMU, ver abajo. Aplicarla es lo que destapó el muro de hoy: el serial
decía «sesión viva» y la pantalla estaba negra.
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.
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
🏔 GNOME PINTA (2026-07-29)
El Overview de GNOME Shell, construido entero desde fuente sobre el kernel de hammer, arje-zero como PID1 y musl: el conmutador de espacios de trabajo arriba a la izquierda, el reloj en el panel, la miniatura del escritorio, el dash abajo, y la notificación estándar de GNOME por sesión de root («Logged in as a privileged user»).
Cómo se validó, y es la parte reusable: screendump por el monitor de QEMU, sin humano delante.
printf 'screendump /tmp/gnome.ppm\n' | socat - UNIX-CONNECT:work/gnome-monitor.sock
python3 -c "from PIL import Image; Image.open('/tmp/gnome.ppm').save('/tmp/gnome.png')"
Métrica barata para saber si pinta antes de mirar: contar colores distintos. Con la pantalla
negra eran 2 (negro + el gris de la consola del kernel); pintando son 582, con el azul GNOME
(2,60,136) y los grises del shell (34,34,38) al frente.
El muro que parecía de KMS y era de GSettings
El diagnóstico anterior de este runbook culpaba al KMS: «el plano primario de virtio-gpu no publica
formatos, sólo hay Queue mode set y ningún page flip». Era una correlación, no la causa. Se
probó quitando MUTTER_DEBUG_FORCE_KMS_MODE=simple —el log pasó a decir using atomic mode setting,
o sea que el cambio SÍ tomó efecto— y la pantalla siguió con exactamente 2 colores. Refutada.
La causa estaba en el stack trace, en el log, desde el principio:
GNOME Shell-CRITICAL: Failed to setup quick settings:
Error: GSettings schema org.gnome.settings-daemon.peripherals.touchscreen not found
Stack trace: systemActions.js:156 → status/system.js:236 → quickSettings.js:29
Esa excepción ocurre dentro de Main.start(), construyendo los quick settings: el panel nunca se
termina de armar, así que no hay nada que pintar. El proceso queda vivo y el compositor con DRM
master — de ahí la pantalla negra con gnome-shell=2. Mutter no tenía un frame que presentar; no
es que no supiera presentarlo.
Lo resolvió la receta gsd-schemas: los gschemas de gnome-settings-daemon sin g-s-d (que sigue
aparcada por GTK3/X11). Tercera vez que aparece la misma lección — un gschema no es un fichero de
datos del demonio, es una interfaz publicada que consume otro programa.
Dos trampas del camino, las dos de diagnóstico
glib-compile-schemassale con 0 aunque RECHACE un esquema. Instalé los once XML, el log dijoesquemas OK, y el shell seguía diciendoschema ... not found. Lo que faltaba eraorg.gnome.settings-daemon.enums.xml—que meson genera conglib-mkenumsy no es uno de los.in—: sin los<enum>, glib descarta el esquema entero, avisa por stderr y sigue.gnome-startahora vuelca ese stderr siempre, no sólo cuando el comando falla.- El log serial persiste entre arranques. Dos veces leí un
STATUS +30sque era del arranque anterior y saqué conclusiones de una VM muerta. Truncar (: > serial.log) antes de arrancar, y esperar a que la imagen esté hecha Y no haya otro QEMU con el disco tomado (Failed to get "write" lockes el aviso de que hay uno vivo).
Lo que queda, y ninguno impide el escritorio
Failed to activate service 'org.freedesktop.UPower': timed out— falta lanzarupowerdengnome-start, como se hace conaccounts-daemon.No cursor theme available— falta un tema de cursor (adwaita-icon-theme).- colord no arranca: su helper setuid no tiene los permisos correctos en la imagen.
Missing required core component Settings— es gnome-control-center, que no está en el corpus.
CÓMO SE CERRÓ LA CAPA JS (2026-07-29)
Era el muro anterior, y quedó cerrado. Vale la pena dejar cómo, porque el método es reusable.
El síntoma era: el compositor sube, expone wayland-0, y el shell sale con código 1 porque a
imports.gi.* le falta un typelib. Cada vez que se cerraba uno, aparecía el siguiente.
Y ESA ITERACIÓN ERA INNECESARIA — la lista está declarada en el propio shell.
js/misc/dependencies.js enumera EXACTAMENTE lo que el shell exige al arrancar. Cruzada contra el
rootfs hidratado, la frontera completa es de siete typelibs, de cinco recetas nuevas y una a
rehacer (medido, no estimado):
| typelib | lo provee | estado |
|---|---|---|
GnomeDesktop-4.0, GnomeBG-4.0 |
gnome-desktop |
ya sellada, pero con -Dintrospection=false y estática ⇒ hace falta la variante de isla dinámica |
Geoclue-2.0 |
geoclue | sin receta |
GWeather-4.0 |
libgweather | sin receta |
IBus-1.0 |
ibus | sin receta |
Rsvg-2.0 |
librsvg | sin receta (es Rust) |
UPowerGlib-1.0 |
upower | sin receta |
Lo demás de esa lista ya está: AccountsService, Atk, Atspi, Gcr, Gdk, Gdm, Gio, GioUnix,
GDesktopEnums, GdkPixbuf, Graphene, Pango, Polkit, PolkitAgent, Soup, y los cinco de mutter/shell
(Meta, Clutter, Cogl, Shell, St). GnomeBluetooth, NM/NMA4 y Malcontent son condicionales de
compilación o de runtime y no bloquean.
La regla que sale de acá, y es el precio de no haberla aplicado antes: cuando el muro es
Requiring X, no cierres X y vuelvas a arrancar — leé el fichero donde el programa DECLARA sus
dependencias de runtime y cerralas todas de una. Cada ronda de este bucle cuesta un rebuild de
imagen y un arranque completo.
Cerrados por este camino hasta hoy, en orden: AccountsService → DBus-1.0 (que incluye Atspi) →
cairo-1.0 (que incluyen Gdk y Pango) → Gdm-1.0.
CÓMO CAYÓ EL MURO ANTERIOR — la hebra de input (cerrado 2026-07-29)
Vale la pena dejarlo escrito porque el diagnóstico que había acá era falso y la forma de refutarlo es reusable.
Decía: «el hilo principal espera en un condvar a que la Mutter Input Thread avise que terminó de
inicializarse; nunca avisa» —eso era correcto— «hipótesis principal: libudev-zero, que
libinput_udev_assign_seat() enumera por udev y el rootfs no trae udev de verdad». Eso era la
conjetura, apoyada en un episodio parecido de otro frente (mirada-usb-nvidia).
La sonda que la refutó: libinput list-devices, que hace exactamente lo que hace
init_libinput() de mutter —udev_new + libinput_udev_create_context +
libinput_udev_assign_seat("seat0")—. Volvió con rc=0 y los tres dispositivos listados. La
enumeración por /sys con libudev-zero funciona perfecto. Está horneada en gnome-start y corre en
cada arranque, antes del shell.
El dato que dio la causa: tid 201 [Mutter Input Th] wchan=evdev_read syscall=0. El hilo estaba
dormido en un read() de evdev DENTRO DEL KERNEL. Ese volcado por hilo ya estaba en el script pero
corría después del kill -ABRT, sobre un /proc/<pid>/task que ya no existía: salía vacío y no
decía nada. El core tampoco servía — gdb no desenrolla a través de musl y devuelve ?? () para los
11 hilos que no son el principal. Acá /proc le gana al core.
La causa: arje-logind-compat abría el device de TakeDevice sin O_NONBLOCK. El compositor
no elige los flags —recibe el fd ya abierto por el bus—, y libinput asume no-bloqueante para poder
leer en bucle hasta EAGAIN; lo dice donde abre (libinput/src/evdev.c:2401) y le pasa
O_RDWR|O_NONBLOCK|O_CLOEXEC a open_restricted(), pero mutter, cuando hay logind, ignora esos
flags y delega la apertura. systemd-logind abre con O_NONBLOCK, así que la dependencia no se nota
hasta que la implementa otro. Arreglado en tawasuyu 3dd88f582, con test.
Y por qué la sonda no lo veía: libinput list-devices abre el devnode él mismo, con
O_NONBLOCK. Sólo el camino por logind pasaba por el fd malo. Esa asimetría es justamente lo que la
convirtió en un experimento y no en una repetición.
Tres perillas de diagnóstico que quedaron y valen para el próximo muro:
MUTTER_DEBUG es una lista de tópicos (g_parse_debug_string sobre meta_debug_keys,
src/core/util.c:42), no un booleano — con 1 no encendía nada; con backend imprime
«Opening and taking control of device file ''» por cada device y dice en cuál se para. El ping
D-Bus a login1 separa «daemon tildado» de «daemon vivo que ya contestó». Y los logs de /tmp (tmpfs)
se copian a la raíz ext4 antes de abortar, para sacarlos con debugfs junto al core.
LO QUE YA SE ARREGLÓ ANTES (dos bugs reales en arje-logind-compat)
Ambos encontrados arrancando esta imagen, ambos con test, ambos ya en tawasuyu y sellados:
- La sesión se creaba PEREZOSAMENTE.
ensure_usersólo corría dentro de un método del Manager, ywrite_login_statevive adentro. Pero mutter no empieza por D-Bus: lo primero que hace essd_pid_get_session(), que arje-sdlogin-compat resuelve LEYENDO/run/systemd/sessions. Huevo y gallina — el estado en disco sólo se escribía después de un pedido que sólo ocurre si el estado ya existe. Ahora la sesión se crea al arrancar (eager_session, apagable conARJE_LOGIN_EAGER=0, uid porARJE_LOGIN_UID). - El object path de la Session estaba mal escapado. Era
/session/_1; la convención de systemd (bus_label_escape) codifica como_<hex>todo lo que no sea[A-Za-z0-9]y también el primer carácter si es dígito, así que el id"1"da_31. Importa porque el cliente calcula el path por su cuenta y no pregunta: mutter reimplementa la misma regla enmeta-dbus-utils.c:escape_dbus_component. Con_1no había nadie sirviendo ahí, la propiedadSeatvolvía NULL y mutter —que no chequea— moría de SIGSEGV enget_seat_proxy. Los objetosUserNO usan este escapado (systemd hardcodea_<uid>), así queuser_path()queda igual.
De paso, un arreglo de portabilidad musl preexistente: libc::ioctl toma c_ulong en glibc y
c_int en musl, así que DRM_IOCTL_SET_MASTER se guarda pelado y se convierte en el sitio de uso.
Y ARJE_LOGIN_STATE=1 ya existía: el comentario del daemon dice literalmente «en arje (sin
systemd) el launcher de sesión lo prende. Default off» — y el launcher es gnome-start. El puente
que escribía /run/systemd/ a mano fue reinventar esa perilla; queda como fallback inerte.
El muro gráfico que este runbook listaba como «el que viene después» nunca apareció: los fixes
que KDE dejó pagos y precargados en gnome-start-qemu.sh (GBM_ALWAYS_SOFTWARE, kms_swrast,
LP_NUM_THREADS=1, MUTTER_DEBUG_SEND_KMS_MODIFIERS=0) alcanzaron: mutter crea el renderer gbm y
elige /dev/dri/card0 como primaria sin quejarse. Es el rédito directo de la campaña anterior.
Lo que falta, en orden
- Los seis proveedores de typelib de la tabla de arriba, en una sola tanda.
gnome-desktopdinámica+introspectada primero (ya está sellada, sólo cambia el modo), después geoclue, libgweather, upower, librsvg e ibus. Ojo al costo delibelogind: re-sellarla re-hashea todo lo que la declara (mutter, gnome-shell…) ⇒ pasaryupana radio libelogindANTES de tocarla — ya pasó dos veces en esta campaña. - Empaquetar la política D-Bus de login1 dentro del artefacto de arje-logind-compat (hoy la escribe el script de imagen).
- Arreglar el
identity mismatchdelAnnounceal bus del fractal (hoy sólo un WARN). - Con la capa JS viva: validar CON PANTALLA (
DISP=gtk), nunca sólo por serial — regla heredada de KDE (metal-dual-desktop-en-curso).
Recetas nuevas de esta etapa
accountsservice (b3:0328b0d0, + parche fgetspent_r para musl) · gi-foreign-typelibs
(b3:c8a4d2f8 — un .gir NO es un typelib: gjs resuelve contra el compilado) · libgdm
(b3:46fac484 — sólo la librería cliente; el demonio sigue aparcado por linux-pam).
libelogind pasó de 14 a 25 símbolos en tres tandas.
