1# Runbook — GNOME en QEMU: el escritorio PINTA
2
3Estado al 2026-07-29: **cerrado.** gnome-shell arranca desde fuente sobre el kernel de hammer, toma
4el DRM master y los tres dispositivos de input por logind, expone `wayland-0`, y **dibuja el Overview
5de GNOME**. Cinco muros caídos, cada uno por medición y no por conjetura:
6
7| muro | causa real | cómo se midió |
8|---|---|---|
9| 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 |
10| cuelgue de la hebra de input | el fd de `TakeDevice` sin `O_NONBLOCK` | `wchan=evdev_read` en `/proc`, no el core |
11| `Requiring <X>` en bucle | 7 typelibs de runtime + 2 capas más abajo | `js/misc/dependencies.js` + cierre de `<include>` |
12| pantalla negra | `Main.start()` abortaba por un gschema de g-s-d | el stack trace, que estaba en el log desde el principio |
13| (descartado) KMS legacy | NO era la causa | atomic tomó efecto y la pantalla siguió con 2 colores |
14
15Hermano de [`kde-qemu-desktop.md`](kde-qemu-desktop.md), del que reusa el andamiaje: la base
16`work/metal-rootfs`, la inyección de mesa-llvmpipe/LLVM/musl, el truco del console-getty y el propio
17`scripts/kde/run-qemu-desktop.sh`.
18
19## Ciclo completo
20
21```sh
22cd ~/hammer
23bash scripts/gnome/hydrate-gnome.sh # cierre de gnome-shell + las raíces de RUNTIME → work/gnome-rootfs
24bash scripts/gnome/qemu-desktop-image.sh # funde base+gnome, parchea, → work/hammer-gnome-qemu.img
25IMG=work/hammer-gnome-qemu.img SERIAL=work/gnome-qemu-serial.log MON=work/gnome-monitor.sock \
26 TIMEOUT=150 DISP=none bash scripts/kde/run-qemu-desktop.sh
27grep -a "gnome-qemu ::" work/gnome-qemu-serial.log | awk '!seen[$0]++'
28```
29
30`DISP=gtk` para ver la ventana (necesita `DISPLAY=:0 GDK_BACKEND=x11`). **Regla heredada de KDE:
31validar escritorios con PANTALLA, nunca sólo por serial** — y sin humano delante se cumple con el
32`screendump` del monitor de QEMU, ver abajo. Aplicarla es lo que destapó el muro de hoy: el serial
33decía «sesión viva» y la pantalla estaba negra.
34
35## Sacar el core y el backtrace
36
37El `gnome-start` deja el core en la raíz ext4 y hace `sync`, así que sobrevive al SIGKILL de QEMU.
38
39```sh
40sfdisk -J work/hammer-gnome-qemu.img # localizar hammer-root (part 2)
41dd if=work/hammer-gnome-qemu.img of=/tmp/root.img bs=1M skip=129 count=5113 status=none
42debugfs -R "dump /core.gnome-shell.<pid> /tmp/core.gnome-shell" /tmp/root.img
43gdb -batch -q -ex "set sysroot $PWD/work/gnome-qemu-rootfs" \
44 -ex "core-file /tmp/core.gnome-shell" -ex "bt 25" work/gnome-qemu-rootfs/usr/bin/gnome-shell
45```
46
47## 🏔 GNOME PINTA (2026-07-29)
48
49
50
51El Overview de GNOME Shell, construido entero desde fuente sobre el kernel de hammer, arje-zero como
52PID1 y musl: el conmutador de espacios de trabajo arriba a la izquierda, el reloj en el panel, la
53miniatura del escritorio, el dash abajo, y la notificación estándar de GNOME por sesión de root
54(«Logged in as a privileged user»).
55
56**Cómo se validó, y es la parte reusable**: `screendump` por el monitor de QEMU, sin humano delante.
57
58```sh
59printf 'screendump /tmp/gnome.ppm\n' | socat - UNIX-CONNECT:work/gnome-monitor.sock
60python3 -c "from PIL import Image; Image.open('/tmp/gnome.ppm').save('/tmp/gnome.png')"
61```
62
63Métrica barata para saber si pinta antes de mirar: **contar colores distintos**. Con la pantalla
64negra eran **2** (negro + el gris de la consola del kernel); pintando son **582**, con el azul GNOME
65`(2,60,136)` y los grises del shell `(34,34,38)` al frente.
66
67### El muro que parecía de KMS y era de GSettings
68
69El diagnóstico anterior de este runbook culpaba al KMS: «el plano primario de virtio-gpu no publica
70formatos, sólo hay `Queue mode set` y ningún page flip». **Era una correlación, no la causa.** Se
71probó quitando `MUTTER_DEBUG_FORCE_KMS_MODE=simple` —el log pasó a decir `using atomic mode setting`,
72o sea que el cambio SÍ tomó efecto— y la pantalla siguió con exactamente 2 colores. Refutada.
73
74La causa estaba en el stack trace, en el log, desde el principio:
75
76```
77GNOME Shell-CRITICAL: Failed to setup quick settings:
78 Error: GSettings schema org.gnome.settings-daemon.peripherals.touchscreen not found
79Stack trace: systemActions.js:156 → status/system.js:236 → quickSettings.js:29
80```
81
82Esa excepción ocurre **dentro de `Main.start()`**, construyendo los quick settings: el panel nunca se
83termina de armar, así que no hay nada que pintar. El proceso queda vivo y el compositor con DRM
84master — de ahí la pantalla negra con `gnome-shell=2`. **Mutter no tenía un frame que presentar; no
85es que no supiera presentarlo.**
86
87Lo resolvió la receta `gsd-schemas`: los gschemas de gnome-settings-daemon **sin** g-s-d (que sigue
88aparcada por GTK3/X11). Tercera vez que aparece la misma lección — un gschema no es un fichero de
89datos del demonio, es una interfaz publicada que consume otro programa.
90
91### Dos trampas del camino, las dos de diagnóstico
92
931. **`glib-compile-schemas` sale con 0 aunque RECHACE un esquema.** Instalé los once XML, el log dijo
94 `esquemas OK`, y el shell seguía diciendo `schema ... not found`. Lo que faltaba era
95 `org.gnome.settings-daemon.enums.xml` —que meson genera con `glib-mkenums` y no es uno de los
96 `.in`—: sin los `<enum>`, glib descarta el esquema entero, avisa por stderr y sigue. `gnome-start`
97 ahora **vuelca ese stderr siempre**, no sólo cuando el comando falla.
982. **El log serial persiste entre arranques.** Dos veces leí un `STATUS +30s` que era del arranque
99 anterior y saqué conclusiones de una VM muerta. Truncar (`: > serial.log`) antes de arrancar, y
100 esperar a que la imagen esté hecha Y no haya otro QEMU con el disco tomado (`Failed to get "write"
101 lock` es el aviso de que hay uno vivo).
102
103### Iconos y cursor: `adwaita-icon-theme` (cerrado)
104
105`No cursor theme available` no era cosmético: **con el cursor por software** —obligatorio en
106virtio-gpu con render software— mutter dibuja la imagen que le da el tema, y sin tema el puntero se
107movía invisible. Con `adwaita-icon-theme` (+ `hicolor-icon-theme`, que su `index.theme` hereda) el
108cursor aparece y la notificación del shell tiene su icono. Colores distintos: 582 → 629.
109
110Dos gotchas de esas dos recetas: **hicolor 0.18 pasó de autotools a meson** (su tarball ya no trae
111`configure`, y un `./configure` heredado sale con 127), y adwaita declara `gtk-update-icon-cache`
112como `required: true` para un `add_install_script` que **su propio autor marcó
113`skip_if_destdir: true`** — exige un binario que en un build empaquetado no puede ejecutar. Se borran
114los dos bloques; `required: false` no alcanza porque meson no propaga el disabler ahí dentro.
115
116### El setuid del launch-helper: una causa, dos síntomas (cerrado)
117
118`dbus-daemon-launch-helper` es el que `dbus-daemon` ejecuta para activar un servicio del bus de
119SISTEMA bajo demanda, y **comprueba sus propios permisos antes de hacer nada**: si no es setuid root
120se niega, y el cliente recibe `The permission of the setuid helper is not correct` — que es lo que
121reportaba colord. Sin eso **ninguna** activación por bus de sistema funciona. La imagen ahora lo deja
122`root:messagebus 4750`, y el síntoma de colord cambió de «permiso incorrecto» a «timed out»: la
123activación ya se intenta de verdad, y lo que queda es del demonio, no del bus.
124
125### El bus de sistema, completo (cerrado): faltaba polkit
126
127```
128== gnome-qemu :: bus: org.freedesktop.login1 OK
129== gnome-qemu :: bus: org.freedesktop.PolicyKit1 OK
130== gnome-qemu :: bus: org.freedesktop.Accounts OK
131== gnome-qemu :: bus: org.freedesktop.UPower OK
132```
133
134`accounts-daemon` y `upowerd` arrancaban, seguían vivos, y **nunca adquirían su nombre**: los dos se
135bloquean en `polkit_authority_get_sync()` al iniciar, y `org.freedesktop.PolicyKit1` no lo servía
136nadie —nuestra receta `polkit` es `libs-only` a propósito, porque el demonio lo pone arje—. gnome-shell
137esperaba después 25 s por cada uno.
138
139**El dato que lo resolvió no salió del log de los daemons sino de la LISTA DE NOMBRES DEL BUS.** Ahí
140estaban como conexiones anónimas `:1.0`, `:1.1` — sin nombre bien conocido. Eso distingue tres cosas
141que en el log del cliente se ven igual: «no arrancó», «arrancó y no llegó a pedir el nombre» y «lo
142pidió y se lo negaron». Era la segunda. `gnome-start` ahora **espera cada nombre** con
143`NameHasOwner` y, si no aparece, vuelca el log del daemon y la lista de nombres.
144
145Lo sirve `recipes/arje-polkit-compat.toml` (b3:03e0a86f), el tercer shim de este tipo tras
146arje-logind-compat y arje-sdlogin-compat. **⚠ Autoriza TODO** — es la postura de sistema confiado que
147arje ya tenía tomada, no algo que la receta introduzca; está escrito en su comentario.
148
149Dos gotchas del empaquetado, los dos medidos:
150- **La política de polkit ya existe y NO alcanza**: permite `own` sólo al usuario `polkitd`, y el shim
151 corre como root. Se agrega un `zz-arje-polkit-compat.conf` en vez de reescribir la de upstream —
152 dbus lee `system.d` en orden alfabético y las reglas posteriores ganan.
153- **Los ficheros del rootfs fundido son hardlinks de SÓLO LECTURA del store** (0444). Un `cat >`
154 encima falla con `Permission denied` y rompió un build. Regla de este script: nunca sobrescribir un
155 fichero que venga de un artefacto; agregar al lado, o `rm -f` primero — y entonces se está pisando
156 algo sellado, que es una decisión, no un descuido.
157
158### Audio: PipeWire (cerrado — decisión del usuario, 2026-07-29)
159
160**El servidor de audio de la distro es PipeWire, no PulseAudio.** Cierra una pregunta que estaba
161explícitamente abierta en varias recetas, y por eso `pulseaudio` se había construido `-Ddaemon=false`:
162sólo el cliente, para que gvc hablara el protocolo sin cerrar la decisión de prestado.
163
164Los dos conviven, y eso es lo que hace que la decisión no rompa nada: `pipewire` va con
165`-Dlibpulse=enabled`, que produce **`pipewire-pulse`** — un servidor que habla el protocolo de
166PulseAudio. gnome-shell usa libpulse sin enterarse de qué hay del otro lado. **No hubo que tocar una
167línea del cliente.**
168
169```
170== gnome-qemu :: socket pipewire-0 OK
171== gnome-qemu :: socket pulse/native OK — gvc va a poder conectar
172Gvc-DEBUG: get server info / update server
173Gvc-DEBUG: Updating client: index=32 name='pipewire'
174Gvc-DEBUG: Updating sink: index=33 name='auto_null' description='Dummy Output'
175```
176
177El `Failed to connect context: Connection refused` desapareció y gvc ve el servidor. El sink es
178`auto_null` («Dummy Output»), que es la respuesta honesta: la VM no tiene tarjeta de sonido.
179
180### 🔊 AUDIO FUNCIONANDO (cerrado): el muro era `libudev-zero`
181
182```
183Audio
184 ├─ Devices: 48. HDA Intel [alsa]
185 ├─ Sinks: * 52. HDA Intel Analog Stereo [vol: 0.40]
186 ├─ Sources: * 53. HDA Intel Analog Stereo [vol: 1.00]
187```
188
189Y gvc lo ve con perfiles reales: `output:analog-stereo+input:analog-stereo (Current)`. Ya no hay
190`auto_null`.
191
192Hicieron falta TRES cosas, en este orden, y cada una parecía la última:
193
1941. **ALSA en el kernel** (`linux-metal` b3:613bca15). Estaba `-d SOUND -d SND`.
1952. **`wireplumber` + `lua`** — el gestor de sesión, sin el cual PipeWire acepta clientes y no enumera.
1963. **Parchear `libudev-zero`** para enumerar devices de CLASE. Esto era el muro real: SPA pide el
197 device `card` (`alsa-udev.c:178-183`), que no tiene `major:minor`, y libudev-zero recorría
198 **sólo** `/sys/dev/{block,char}` — o sea sólo devices CON nodo. **El device que SPA necesita nunca
199 entraba en la enumeración**: no faltaba una propiedad, faltaba la mitad del espacio de búsqueda.
200
201**⚠ Y la primera versión del parche, que recorría `/sys/class` ENTERO, arregló el audio y ROMPIÓ EL
202VÍDEO.** `/sys/class/drm` no trae sólo tarjetas: trae los conectores (`card0-Virtual-1`) y un fichero
203`version`, ninguno con nodo. Al entrar en la enumeración, mutter los toma como candidatos:
204
205```
206Failed to use linear monitor configuration: No available CRTC for monitor 'RHT QEMU Monitor'
207Page flip failed: drmModeAtomicCommit: Invalid argument (en bucle)
208```
209
210Pantalla negra con el compositor vivo — el mismo cuadro de antes, por una causa nueva. El udev real
211también enumera esos conectores, pero les adjunta las propiedades por las que mutter los descarta, y
212ésas libudev-zero no las sintetiza (su tabla es fija y está orientada a input).
213
214**La lección, y vale para cualquier reimplementación parcial de una API: ensanchar la enumeración
215expone los huecos de PROPIEDADES a más consumidores. Los dos lados del contrato tienen que crecer
216juntos.** El parche final va con una **lista acotada de subsistemas** (`sound` hoy); agregar otro es
217una línea *y* una prueba de que el consumidor lo necesita y no se rompe.
218
219**Por qué no eudev**, que era el camino que parecía obvio: el repo ya lo había probado en otro frente y
220**rompió el input** — su `libudev.so.1` pisa el de libudev-zero y, sin udevd corriendo, no expone
221`ID_INPUT`, así que libinput ignora todos los dispositivos en silencio. Verificación A/B en QEMU: eudev
222= 0 «New device», libudev-zero = 5. eudev sin udevd es PEOR, y correr udevd contradice la arquitectura.
223
224Costo del parche: `yupana radio libudev-zero` = **51 sellados a deuda** (9 en el cierre GNOME, que se
225reconstruyeron acá; el resto —base, cli, KDE— quedan para el latido).
226
227**`wireplumber` cerrado** (b3:39a9a06c), y con él `lua` (b3:e6c35f98), que hubo que autorar: el
228Makefile de Lua **no produce `.so` ni `.pc`** —los agrega cada distro—, así que la receta enlaza la
229compartida a mano desde la `.a` con `--whole-archive` y escribe el pkg-config. `-fPIC` es obligatorio:
230`libwplua` es un objeto compartido y una `.a` no-PIC no entra (la regla del corpus que ya obligó a las
231variantes `-shared` de zlib, sqlite, libxml2, libuuid y dbus).
232
233Los TRES procesos y su orden, porque cada uno es cliente del anterior: `pipewire` → `pipewire-pulse`
234→ `wireplumber`. `gnome-start` verifica cada paso y termina volcando `wpctl status`, que es el dato que
235importa: no el pid, sino **qué ve la política**.
236
237```
238PipeWire 'pipewire-0' [1.2.7]
239 └─ Clients: 32. pipewire · 34. WirePlumber · 47. WirePlumber [export] · 48. wpctl
240Audio
241 ├─ Devices: ← vacío
242 ├─ Sinks: * 33. Dummy Output
243```
244
245**Y ese `Devices:` vacío NO es un fallo de wireplumber: el kernel de esta imagen no tiene ALSA.**
246`recipes/linux-metal.toml:111` lo apaga explícitamente — `-d SOUND -d SND`. No hay `/dev/snd` ni
247`/sys/class/sound` que enumerar. Se verificó agregando una `ich9-intel-hda` emulada a QEMU
248(`AUDIO=1`, ahora el default de `run-qemu-desktop.sh`): con tarjeta y sin ALSA en el kernel, el
249resultado no cambia. **Encender el sonido en la distro es una decisión con costo** —re-sellar el
250kernel y rehacer la imagen metal— y queda para quien la tome, no escondida en un default.
251
252Un bug propio que esto destapó: el bloque de wireplumber quedó DUPLICADO en `gnome-start` por un
253merge descuidado, y el resultado eran **dos wireplumber vivos peleándose el grafo**. El síntoma no era
254un error sino una lista de clientes que se lee normal si no se cuenta: dos «WirePlumber» con pids
255distintos en `wpctl status`.
256
257Gotcha de empaquetado, medido: ya existía `incoming-kde/pipewire.toml` sellada y **no se puede reusar**
258hidratándola acá — los dos cierres traen glib y **no son la misma glib**: `incoming-kde/glib-shared` y
259`incoming-gnome/glib` producen `libglib-2.0.so.0.8800.1` con bytes distintos (`cmp`) y comparten 370
260rutas. Proyectar las dos deja que una gane por orden ⇒ dos registros de GType en un proceso, el cuadro
261de colord. Hay copia en la cola GNOME. Lo que SÍ fue gratis: `alsa-lib`, cuya única dep resuelve al
262catálogo padre ⇒ hash idéntico y cache hit.
263
264### Lo que queda, y ninguno impide el escritorio
265
266- **colord** sigue sin arrancar (gestión de color de pantalla). Su activación por bus ya está
267 permitida; falta ver por qué el demonio no sube.
268- `Missing required core component Settings` — es gnome-control-center, que no está en el corpus.
269
270### Dos lecciones de método que costaron una iteración cada una
271
2721. **Un pid no es un servicio.** La primera versión del lanzamiento de upowerd decía «lanzado» y el
273 shell seguía esperando. Reportar el arranque de un daemon sin comprobar que adquirió su NOMBRE es
274 reportar una intención, no un hecho.
2752. **El log serial persiste, y la imagen tarda más que el polling.** Tres veces leí un marcador del
276 arranque anterior y saqué conclusiones de una VM muerta. El orden correcto: esperar a que la imagen
277 esté hecha, *después* esperar el marcador. Truncar no alcanza si la truncada ocurre a mitad de la
278 cadena.
279
280## CÓMO SE CERRÓ LA CAPA JS (2026-07-29)
281
282Era el muro anterior, y quedó cerrado. Vale la pena dejar cómo, porque el método es reusable.
283
284El síntoma era: el compositor sube, expone `wayland-0`, y el shell sale con código 1 porque a
285`imports.gi.*` le falta un typelib. Cada vez que se cerraba uno, aparecía el siguiente.
286
287**Y ESA ITERACIÓN ERA INNECESARIA — la lista está declarada en el propio shell.**
288`js/misc/dependencies.js` enumera EXACTAMENTE lo que el shell exige al arrancar. Cruzada contra el
289rootfs hidratado, la frontera completa es de **siete typelibs**, de cinco recetas nuevas y una a
290rehacer (medido, no estimado):
291
292| typelib | lo provee | estado |
293|---|---|---|
294| `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 |
295| `Geoclue-2.0` | geoclue | sin receta |
296| `GWeather-4.0` | libgweather | sin receta |
297| `IBus-1.0` | ibus | sin receta |
298| `Rsvg-2.0` | librsvg | sin receta (**es Rust**) |
299| `UPowerGlib-1.0` | upower | sin receta |
300
301Lo demás de esa lista ya está: AccountsService, Atk, Atspi, Gcr, Gdk, Gdm, Gio, GioUnix,
302GDesktopEnums, GdkPixbuf, Graphene, Pango, Polkit, PolkitAgent, Soup, y los cinco de mutter/shell
303(Meta, Clutter, Cogl, Shell, St). `GnomeBluetooth`, `NM`/`NMA4` y `Malcontent` son condicionales de
304compilación o de runtime y no bloquean.
305
306**La regla que sale de acá, y es el precio de no haberla aplicado antes: cuando el muro es
307`Requiring X`, no cierres X y vuelvas a arrancar — leé el fichero donde el programa DECLARA sus
308dependencias de runtime y cerralas todas de una.** Cada ronda de este bucle cuesta un rebuild de
309imagen y un arranque completo.
310
311Cerrados por este camino hasta hoy, en orden: `AccountsService` → `DBus-1.0` (que incluye `Atspi`) →
312`cairo-1.0` (que incluyen `Gdk` y `Pango`) → `Gdm-1.0`.
313
314## CÓMO CAYÓ EL MURO ANTERIOR — la hebra de input (cerrado 2026-07-29)
315
316Vale la pena dejarlo escrito porque **el diagnóstico que había acá era falso** y la forma de
317refutarlo es reusable.
318
319Decía: «el hilo principal espera en un condvar a que la Mutter Input Thread avise que terminó de
320inicializarse; nunca avisa» —eso era correcto— «hipótesis principal: `libudev-zero`, que
321`libinput_udev_assign_seat()` enumera por udev y el rootfs no trae udev de verdad». Eso era la
322conjetura, apoyada en un episodio parecido de otro frente ([[mirada-usb-nvidia]]).
323
324**La sonda que la refutó**: `libinput list-devices`, que hace exactamente lo que hace
325`init_libinput()` de mutter —`udev_new` + `libinput_udev_create_context` +
326`libinput_udev_assign_seat("seat0")`—. Volvió con `rc=0` y los tres dispositivos listados. La
327enumeración por /sys con libudev-zero funciona perfecto. Está horneada en `gnome-start` y corre en
328cada arranque, antes del shell.
329
330**El dato que dio la causa**: `tid 201 [Mutter Input Th] wchan=evdev_read syscall=0`. El hilo estaba
331dormido en un `read()` de evdev DENTRO DEL KERNEL. Ese volcado por hilo ya estaba en el script pero
332corría *después* del `kill -ABRT`, sobre un `/proc/<pid>/task` que ya no existía: salía vacío y no
333decía nada. El core tampoco servía — gdb no desenrolla a través de musl y devuelve `?? ()` para los
33411 hilos que no son el principal. **Acá `/proc` le gana al core.**
335
336**La causa**: `arje-logind-compat` abría el device de `TakeDevice` sin `O_NONBLOCK`. El compositor
337no elige los flags —recibe el fd ya abierto por el bus—, y libinput asume no-bloqueante para poder
338leer en bucle hasta `EAGAIN`; lo dice donde abre (`libinput/src/evdev.c:2401`) y le pasa
339`O_RDWR|O_NONBLOCK|O_CLOEXEC` a `open_restricted()`, pero mutter, cuando hay logind, ignora esos
340flags y delega la apertura. systemd-logind abre con `O_NONBLOCK`, así que la dependencia no se nota
341hasta que la implementa otro. Arreglado en tawasuyu `3dd88f582`, con test.
342
343Y por qué la sonda no lo veía: `libinput list-devices` abre el devnode **él mismo**, con
344`O_NONBLOCK`. Sólo el camino por logind pasaba por el fd malo. Esa asimetría es justamente lo que la
345convirtió en un experimento y no en una repetición.
346
347Tres perillas de diagnóstico que quedaron y valen para el próximo muro:
348`MUTTER_DEBUG` es una **lista de tópicos** (`g_parse_debug_string` sobre `meta_debug_keys`,
349`src/core/util.c:42`), no un booleano — con `1` no encendía nada; con `backend` imprime
350«Opening and taking control of device file '<path>'» por cada device y dice en cuál se para. El ping
351D-Bus a login1 separa «daemon tildado» de «daemon vivo que ya contestó». Y los logs de `/tmp` (tmpfs)
352se copian a la raíz ext4 antes de abortar, para sacarlos con `debugfs` junto al core.
353
354## LO QUE YA SE ARREGLÓ ANTES (dos bugs reales en arje-logind-compat)
355
356Ambos encontrados arrancando esta imagen, ambos con test, ambos ya en tawasuyu y sellados:
357
3581. **La sesión se creaba PEREZOSAMENTE.** `ensure_user` sólo corría dentro de un método del Manager,
359 y `write_login_state` vive adentro. Pero mutter no empieza por D-Bus: lo primero que hace es
360 `sd_pid_get_session()`, que arje-sdlogin-compat resuelve LEYENDO `/run/systemd/sessions`. Huevo y
361 gallina — el estado en disco sólo se escribía después de un pedido que sólo ocurre si el estado
362 ya existe. Ahora la sesión se crea al arrancar (`eager_session`, apagable con
363 `ARJE_LOGIN_EAGER=0`, uid por `ARJE_LOGIN_UID`).
3642. **El object path de la Session estaba mal escapado.** Era `/session/_1`; la convención de systemd
365 (`bus_label_escape`) codifica como `_<hex>` todo lo que no sea `[A-Za-z0-9]` **y también el primer
366 carácter si es dígito**, así que el id `"1"` da **`_31`**. Importa porque el cliente calcula el
367 path por su cuenta y no pregunta: mutter reimplementa la misma regla en
368 `meta-dbus-utils.c:escape_dbus_component`. Con `_1` no había nadie sirviendo ahí, la propiedad
369 `Seat` volvía NULL y mutter —que no chequea— moría de **SIGSEGV en `get_seat_proxy`**. Los objetos
370 `User` NO usan este escapado (systemd hardcodea `_<uid>`), así que `user_path()` queda igual.
371
372De paso, un arreglo de portabilidad musl preexistente: `libc::ioctl` toma `c_ulong` en glibc y
373`c_int` en musl, así que `DRM_IOCTL_SET_MASTER` se guarda pelado y se convierte en el sitio de uso.
374
375**Y `ARJE_LOGIN_STATE=1` ya existía**: el comentario del daemon dice literalmente «en arje (sin
376systemd) el launcher de sesión lo prende. Default off» — y el launcher es `gnome-start`. El puente
377que escribía `/run/systemd/` a mano fue reinventar esa perilla; queda como fallback inerte.
378
379El muro gráfico que este runbook listaba como «el que viene después» **nunca apareció**: los fixes
380que KDE dejó pagos y precargados en `gnome-start-qemu.sh` (`GBM_ALWAYS_SOFTWARE`, `kms_swrast`,
381`LP_NUM_THREADS=1`, `MUTTER_DEBUG_SEND_KMS_MODIFIERS=0`) alcanzaron: mutter crea el renderer gbm y
382elige `/dev/dri/card0` como primaria sin quejarse. Es el rédito directo de la campaña anterior.
383
384## Lo que falta, en orden
385
3861. **Los seis proveedores de typelib de la tabla de arriba**, en una sola tanda. `gnome-desktop`
387 dinámica+introspectada primero (ya está sellada, sólo cambia el modo), después geoclue,
388 libgweather, upower, librsvg e ibus. Ojo al costo de `libelogind`: re-sellarla re-hashea todo lo
389 que la declara (mutter, gnome-shell…) ⇒ pasar `yupana radio libelogind` ANTES de tocarla — ya
390 pasó dos veces en esta campaña.
3912. **Empaquetar la política D-Bus de login1** dentro del artefacto de arje-logind-compat (hoy la
392 escribe el script de imagen).
3933. Arreglar el `identity mismatch` del `Announce` al bus del fractal (hoy sólo un WARN).
3944. Con la capa JS viva: validar CON PANTALLA (`DISP=gtk`), nunca sólo por serial — regla heredada de
395 KDE ([[metal-dual-desktop-en-curso]]).
396
397## Recetas nuevas de esta etapa
398
399`accountsservice` (b3:0328b0d0, + parche `fgetspent_r` para musl) · `gi-foreign-typelibs`
400(b3:c8a4d2f8 — un `.gir` NO es un typelib: gjs resuelve contra el compilado) · `libgdm`
401(b3:46fac484 — sólo la librería cliente; el demonio sigue aparcado por linux-pam).
402`libelogind` pasó de 14 a 25 símbolos en tres tandas.
gnome-qemu-desktop.md
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-schemas sale con 0 aunque RECHACE un esquema. Instalé los once XML, el log dijo
esquemas OK, y el shell seguía diciendo schema ... not found. Lo que faltaba era
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 cuando el comando falla.
- El log serial persiste entre arranques. Dos veces leí un
STATUS +30s que 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" lock es el aviso de que hay uno vivo).
Iconos y cursor: adwaita-icon-theme (cerrado)
No cursor theme available no era cosmético: con el cursor por software —obligatorio en
virtio-gpu con render software— mutter dibuja la imagen que le da el tema, y sin tema el puntero se
movía invisible. Con adwaita-icon-theme (+ hicolor-icon-theme, que su index.theme hereda) el
cursor aparece y la notificación del shell tiene su icono. Colores distintos: 582 → 629.
Dos gotchas de esas dos recetas: hicolor 0.18 pasó de autotools a meson (su tarball ya no trae
configure, y un ./configure heredado sale con 127), y adwaita declara gtk-update-icon-cache
como required: true para un add_install_script que su propio autor marcó
skip_if_destdir: true — exige un binario que en un build empaquetado no puede ejecutar. Se borran
los dos bloques; required: false no alcanza porque meson no propaga el disabler ahí dentro.
El setuid del launch-helper: una causa, dos síntomas (cerrado)
dbus-daemon-launch-helper es el que dbus-daemon ejecuta para activar un servicio del bus de
SISTEMA bajo demanda, y comprueba sus propios permisos antes de hacer nada: si no es setuid root
se niega, y el cliente recibe The permission of the setuid helper is not correct — que es lo que
reportaba colord. Sin eso ninguna activación por bus de sistema funciona. La imagen ahora lo deja
root:messagebus 4750, y el síntoma de colord cambió de «permiso incorrecto» a «timed out»: la
activación ya se intenta de verdad, y lo que queda es del demonio, no del bus.
El bus de sistema, completo (cerrado): faltaba polkit
== gnome-qemu :: bus: org.freedesktop.login1 OK
== gnome-qemu :: bus: org.freedesktop.PolicyKit1 OK
== gnome-qemu :: bus: org.freedesktop.Accounts OK
== gnome-qemu :: bus: org.freedesktop.UPower OK
accounts-daemon y upowerd arrancaban, seguían vivos, y nunca adquirían su nombre: los dos se
bloquean en polkit_authority_get_sync() al iniciar, y org.freedesktop.PolicyKit1 no lo servía
nadie —nuestra receta polkit es libs-only a propósito, porque el demonio lo pone arje—. gnome-shell
esperaba después 25 s por cada uno.
El dato que lo resolvió no salió del log de los daemons sino de la LISTA DE NOMBRES DEL BUS. Ahí
estaban como conexiones anónimas :1.0, :1.1 — sin nombre bien conocido. Eso distingue tres cosas
que en el log del cliente se ven igual: «no arrancó», «arrancó y no llegó a pedir el nombre» y «lo
pidió y se lo negaron». Era la segunda. gnome-start ahora espera cada nombre con
NameHasOwner y, si no aparece, vuelca el log del daemon y la lista de nombres.
Lo sirve recipes/arje-polkit-compat.toml (b3:03e0a86f), el tercer shim de este tipo tras
arje-logind-compat y arje-sdlogin-compat. ⚠ Autoriza TODO — es la postura de sistema confiado que
arje ya tenía tomada, no algo que la receta introduzca; está escrito en su comentario.
Dos gotchas del empaquetado, los dos medidos:
- La política de polkit ya existe y NO alcanza: permite
own sólo al usuario polkitd, y el shim
corre como root. Se agrega un zz-arje-polkit-compat.conf en vez de reescribir la de upstream —
dbus lee system.d en orden alfabético y las reglas posteriores ganan.
- Los ficheros del rootfs fundido son hardlinks de SÓLO LECTURA del store (0444). Un
cat >
encima falla con Permission denied y rompió un build. Regla de este script: nunca sobrescribir un
fichero que venga de un artefacto; agregar al lado, o rm -f primero — y entonces se está pisando
algo sellado, que es una decisión, no un descuido.
Audio: PipeWire (cerrado — decisión del usuario, 2026-07-29)
El servidor de audio de la distro es PipeWire, no PulseAudio. Cierra una pregunta que estaba
explícitamente abierta en varias recetas, y por eso pulseaudio se había construido -Ddaemon=false:
sólo el cliente, para que gvc hablara el protocolo sin cerrar la decisión de prestado.
Los dos conviven, y eso es lo que hace que la decisión no rompa nada: pipewire va con
-Dlibpulse=enabled, que produce pipewire-pulse — un servidor que habla el protocolo de
PulseAudio. gnome-shell usa libpulse sin enterarse de qué hay del otro lado. No hubo que tocar una
línea del cliente.
== gnome-qemu :: socket pipewire-0 OK
== gnome-qemu :: socket pulse/native OK — gvc va a poder conectar
Gvc-DEBUG: get server info / update server
Gvc-DEBUG: Updating client: index=32 name='pipewire'
Gvc-DEBUG: Updating sink: index=33 name='auto_null' description='Dummy Output'
El Failed to connect context: Connection refused desapareció y gvc ve el servidor. El sink es
auto_null («Dummy Output»), que es la respuesta honesta: la VM no tiene tarjeta de sonido.
🔊 AUDIO FUNCIONANDO (cerrado): el muro era libudev-zero
Audio
├─ Devices: 48. HDA Intel [alsa]
├─ Sinks: * 52. HDA Intel Analog Stereo [vol: 0.40]
├─ Sources: * 53. HDA Intel Analog Stereo [vol: 1.00]
Y gvc lo ve con perfiles reales: output:analog-stereo+input:analog-stereo (Current). Ya no hay
auto_null.
Hicieron falta TRES cosas, en este orden, y cada una parecía la última:
- ALSA en el kernel (
linux-metal b3:613bca15). Estaba -d SOUND -d SND.
wireplumber + lua — el gestor de sesión, sin el cual PipeWire acepta clientes y no enumera.
- Parchear
libudev-zero para enumerar devices de CLASE. Esto era el muro real: SPA pide el
device card (alsa-udev.c:178-183), que no tiene major:minor, y libudev-zero recorría
sólo /sys/dev/{block,char} — o sea sólo devices CON nodo. El device que SPA necesita nunca
entraba en la enumeración: no faltaba una propiedad, faltaba la mitad del espacio de búsqueda.
⚠ Y la primera versión del parche, que recorría /sys/class ENTERO, arregló el audio y ROMPIÓ EL
VÍDEO. /sys/class/drm no trae sólo tarjetas: trae los conectores (card0-Virtual-1) y un fichero
version, ninguno con nodo. Al entrar en la enumeración, mutter los toma como candidatos:
Failed to use linear monitor configuration: No available CRTC for monitor 'RHT QEMU Monitor'
Page flip failed: drmModeAtomicCommit: Invalid argument (en bucle)
Pantalla negra con el compositor vivo — el mismo cuadro de antes, por una causa nueva. El udev real
también enumera esos conectores, pero les adjunta las propiedades por las que mutter los descarta, y
ésas libudev-zero no las sintetiza (su tabla es fija y está orientada a input).
La lección, y vale para cualquier reimplementación parcial de una API: ensanchar la enumeración
expone los huecos de PROPIEDADES a más consumidores. Los dos lados del contrato tienen que crecer
juntos. El parche final va con una lista acotada de subsistemas (sound hoy); agregar otro es
una línea y una prueba de que el consumidor lo necesita y no se rompe.
Por qué no eudev, que era el camino que parecía obvio: el repo ya lo había probado en otro frente y
rompió el input — su libudev.so.1 pisa el de libudev-zero y, sin udevd corriendo, no expone
ID_INPUT, así que libinput ignora todos los dispositivos en silencio. Verificación A/B en QEMU: eudev
= 0 «New device», libudev-zero = 5. eudev sin udevd es PEOR, y correr udevd contradice la arquitectura.
Costo del parche: yupana radio libudev-zero = 51 sellados a deuda (9 en el cierre GNOME, que se
reconstruyeron acá; el resto —base, cli, KDE— quedan para el latido).
wireplumber cerrado (b3:39a9a06c), y con él lua (b3:e6c35f98), que hubo que autorar: el
Makefile de Lua no produce .so ni .pc —los agrega cada distro—, así que la receta enlaza la
compartida a mano desde la .a con --whole-archive y escribe el pkg-config. -fPIC es obligatorio:
libwplua es un objeto compartido y una .a no-PIC no entra (la regla del corpus que ya obligó a las
variantes -shared de zlib, sqlite, libxml2, libuuid y dbus).
Los TRES procesos y su orden, porque cada uno es cliente del anterior: pipewire → pipewire-pulse
→ wireplumber. gnome-start verifica cada paso y termina volcando wpctl status, que es el dato que
importa: no el pid, sino qué ve la política.
PipeWire 'pipewire-0' [1.2.7]
└─ Clients: 32. pipewire · 34. WirePlumber · 47. WirePlumber [export] · 48. wpctl
Audio
├─ Devices: ← vacío
├─ Sinks: * 33. Dummy Output
Y ese Devices: vacío NO es un fallo de wireplumber: el kernel de esta imagen no tiene ALSA.
recipes/linux-metal.toml:111 lo apaga explícitamente — -d SOUND -d SND. No hay /dev/snd ni
/sys/class/sound que enumerar. Se verificó agregando una ich9-intel-hda emulada a QEMU
(AUDIO=1, ahora el default de run-qemu-desktop.sh): con tarjeta y sin ALSA en el kernel, el
resultado no cambia. Encender el sonido en la distro es una decisión con costo —re-sellar el
kernel y rehacer la imagen metal— y queda para quien la tome, no escondida en un default.
Un bug propio que esto destapó: el bloque de wireplumber quedó DUPLICADO en gnome-start por un
merge descuidado, y el resultado eran dos wireplumber vivos peleándose el grafo. El síntoma no era
un error sino una lista de clientes que se lee normal si no se cuenta: dos «WirePlumber» con pids
distintos en wpctl status.
Gotcha de empaquetado, medido: ya existía incoming-kde/pipewire.toml sellada y no se puede reusar
hidratándola acá — los dos cierres traen glib y no son la misma glib: incoming-kde/glib-shared y
incoming-gnome/glib producen libglib-2.0.so.0.8800.1 con bytes distintos (cmp) y comparten 370
rutas. Proyectar las dos deja que una gane por orden ⇒ dos registros de GType en un proceso, el cuadro
de colord. Hay copia en la cola GNOME. Lo que SÍ fue gratis: alsa-lib, cuya única dep resuelve al
catálogo padre ⇒ hash idéntico y cache hit.
Lo que queda, y ninguno impide el escritorio
- colord sigue sin arrancar (gestión de color de pantalla). Su activación por bus ya está
permitida; falta ver por qué el demonio no sube.
Missing required core component Settings — es gnome-control-center, que no está en el corpus.
Dos lecciones de método que costaron una iteración cada una
- Un pid no es un servicio. La primera versión del lanzamiento de upowerd decía «lanzado» y el
shell seguía esperando. Reportar el arranque de un daemon sin comprobar que adquirió su NOMBRE es
reportar una intención, no un hecho.
- El log serial persiste, y la imagen tarda más que el polling. Tres veces leí un marcador del
arranque anterior y saqué conclusiones de una VM muerta. El orden correcto: esperar a que la imagen
esté hecha, después esperar el marcador. Truncar no alcanza si la truncada ocurre a mitad de la
cadena.
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 '<path>'» 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_user sólo corría dentro de un método del Manager,
y write_login_state vive adentro. Pero mutter no empieza por D-Bus: lo primero que hace es
sd_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 con
ARJE_LOGIN_EAGER=0, uid por ARJE_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 en
meta-dbus-utils.c:escape_dbus_component. Con _1 no había nadie sirviendo ahí, la propiedad
Seat volvía NULL y mutter —que no chequea— moría de SIGSEGV en get_seat_proxy. Los objetos
User NO usan este escapado (systemd hardcodea _<uid>), así que user_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-desktop
dinámica+introspectada primero (ya está sellada, sólo cambia el modo), después geoclue,
libgweather, upower, librsvg e ibus. Ojo al costo de libelogind: re-sellarla re-hashea todo lo
que la declara (mutter, gnome-shell…) ⇒ pasar yupana radio libelogind ANTES 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 mismatch del Announce al 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.