tawasuyu

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

⎇ 97ceb7241181ba5e2b586b6bbc1c490c334422f4 docs/runbooks/gnome-qemu-desktop.md Markdown · 456 siq'ikuna · 28.4 KB Ñawpa willay ch'uya

takana etapa 5b: los 59 docs de diseño, runbooks y ADR 97ceb7241 · Sergio ·

1# Runbook — GNOME en QEMU: el escritorio PINTA
2
3Estado al 2026-07-29: **cerrado.** gnome-shell arranca desde fuente sobre el kernel de takana, 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 takana-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![gnome-shell en QEMU](../evidencia/gnome-shell-qemu-2026-07-29.png)
50
51El Overview de GNOME Shell, construido entero desde fuente sobre el kernel de takana, 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. ✅ **HECHO** — los seis están sellados y en la cola GNOME (verificado por `takana hash`, 2026-08-03):
387   `gnome-desktop` b3:b3aed0d6 · `geoclue` b3:50e9de94 · `libgweather` b3:d289ac66 · `ibus` b3:d4d3750a ·
388   `librsvg` b3:351f4658 · `upower` b3:9c31f20f. Es lo que hace que el shell arranque; el arranque de
389   hoy ve **59 typelibs de sistema + 3 del shell**. El texto original del plan queda abajo por el
390   costo de `libelogind`, que sigue vigente.
391
392   **Los seis proveedores de typelib de la tabla de arriba**, en una sola tanda. `gnome-desktop`
393   dinámica+introspectada primero (ya está sellada, sólo cambia el modo), después geoclue,
394   libgweather, upower, librsvg e ibus. Ojo al costo de `libelogind`: re-sellarla re-hashea todo lo
395   que la declara (mutter, gnome-shell…) ⇒ pasar `yupana radio libelogind` ANTES de tocarla — ya
396   pasó dos veces en esta campaña.
3972. ✅ **HECHO** — la política D-Bus de login1 (y la de PolicyKit1) viaja dentro del artefacto de su
398   daemon, no en el script de imagen. Un servicio que no puede adueñarse de su nombre no es un
399   servicio: la política es parte de lo que el paquete promete. El script de imagen ahora sólo las
400   COPIA y **verifica que llegaron** — y esa verificación destapó de entrada que el script inyectaba
401   el binario del shim de polkit sin su `.conf`.
4023. ✅ **HECHO, y este ítem estaba mal escrito en dos sentidos.** El `identity mismatch` no lo emite
403   arje-logind-compat sino el `bus_mediator` de **arje-zero**; y no había nada que arreglar acá,
404   porque **ya estaba arreglado río arriba** (tawasuyu `ce96cfa8d`, 2026-07-29). Lo que teníamos era
405   un PID1 viejo: nuestro log decía `rechazando request` y la fuente actual dice `sigo como anónimo`.
406   Tampoco era «sólo un WARN» — el commit del fix se llama *«apagar/reiniciar moría por un ENTE_ID
407   heredado, no por permisos»*: `ENTE_ID` lo hereda todo el árbol de procesos, cualquier nieto
408   reclamaba esa identidad con su propio pid y el mediador rechazaba la request **entera**.
409   Se movió el pin de `recipes/arje-zero.toml` al mismo commit que ya usan los dos compat daemons
410   (ver el comentario de la receta: por qué NO se cherry-pickeó, con la medición).
411   **La lección general: antes de arreglar un WARN, buscá quién lo emite y si ya está arreglado
412   upstream.** Nuestro pin estaba 3951 commits detrás y nadie lo había mirado.
4134. ✅ **HECHO (2026-08-03)** — validado CON PANTALLA de verdad (`DISP=gtk`, ventana en el laptop) y no
414   sólo por `screendump`: el Overview pinta con el PID1 sellado nuevo, **689 colores distintos**, azul
415   GNOME `(2,60,136)` y grises del shell `(34,34,38)` al frente, cursor visible.
416   Evidencia: [`gnome-shell-qemu-2026-08-03-pid1-sellado.png`](../evidencia/gnome-shell-qemu-2026-08-03-pid1-sellado.png).
417   **Y aplicar la regla pagó otra vez**: el arranque destapó una regresión que el serial daba por
418   verde — ver abajo.
419
420### La carrera de PolicyKit1: el resumen final decía OK y los clientes ya estaban muertos
421
422Arrancando para la validación con pantalla, `accounts-daemon` y `upowerd` **murieron los dos**:
423
424```
425Error calling StartServiceByName for org.freedesktop.PolicyKit1:
426  Failed to execute program org.freedesktop.PolicyKit1: Permission denied
427Failed to initialize daemon
428```
429
430El comentario del script ya decía que arje-polkit-compat «VA ANTES que accounts-daemon y upowerd», y
431**iba antes** — en el orden de las líneas. Pero se lanza en background y tarda ~2 s en adquirir
432`org.freedesktop.PolicyKit1`, mientras los otros dos arrancan de inmediato y llaman a
433`polkit_authority_get_sync()` dentro de ese hueco. Sin dueño del nombre, D-Bus intenta ACTIVAR el
434servicio, la activación falla, y los dos daemons se mueren.
435
436**Lo peligroso no fue el fallo sino el informe.** Los tres `esperar_nombre` estaban todos al final:
437para cuando corrían, el shim ya tenía el nombre y el resumen imprimía `bus: org.freedesktop.PolicyKit1
438OK` sobre dos clientes que hacía rato eran cadáveres. Un chequeo que corre después de la carrera no la
439mide.
440
441El arreglo es mover la espera al PRODUCTOR: `esperar_nombre` se define antes del primer daemon, y el
442bloque de polkit espera su propio nombre **antes** de que se lance ningún cliente suyo. Es la misma
443lección de «un pid no es un servicio» aplicada del otro lado: *lanzar en orden no es estar listo en
444orden*. Después del fix: `PolicyKit1 OK` → `accounts-daemon lanzado` → `upowerd sigue vivo` →
445`Accounts OK` → `UPower OK`.
446
447Dato suelto que dejó el mismo arranque: **colord ahora MUERE** («colord MURIÓ al arrancar») en vez de
448quedarse vivo sin adquirir el nombre. Su log sigue cortándose después de abrir las tres bases de datos.
449Cambió el síntoma, no la causa conocida; el ítem sigue aparcado.
450
451## Recetas nuevas de esta etapa
452
453`accountsservice` (b3:0328b0d0, + parche `fgetspent_r` para musl) · `gi-foreign-typelibs`
454(b3:c8a4d2f8 — un `.gir` NO es un typelib: gjs resuelve contra el compilado) · `libgdm`
455(b3:46fac484 — sólo la librería cliente; el demonio sigue aparcado por linux-pam).
456`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 takana, 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:

murocausa realcómo se midió
SIGSEGV en get_seat_proxyobject 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 inputel fd de TakeDevice sin O_NONBLOCKwchan=evdev_read en /proc, no el core
Requiring <X> en bucle7 typelibs de runtime + 2 capas más abajojs/misc/dependencies.js + cierre de <include>
pantalla negraMain.start() abortaba por un gschema de g-s-del stack trace, que estaba en el log desde el principio
(descartado) KMS legacyNO era la causaatomic 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 takana-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)

gnome-shell en QEMU

El Overview de GNOME Shell, construido entero desde fuente sobre el kernel de takana, 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

  1. 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.
  2. 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:

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:

  1. ALSA en el kernel (linux-metal b3:613bca15). Estaba -d SOUND -d SND.
  2. wireplumber + lua — el gestor de sesión, sin el cual PipeWire acepta clientes y no enumera.
  3. 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

Dos lecciones de método que costaron una iteración cada una

  1. 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.
  2. 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):

typeliblo proveeestado
GnomeDesktop-4.0, GnomeBG-4.0gnome-desktopya sellada, pero con -Dintrospection=false y estática ⇒ hace falta la variante de isla dinámica
Geoclue-2.0geocluesin receta
GWeather-4.0libgweathersin receta
IBus-1.0ibussin receta
Rsvg-2.0librsvgsin receta (es Rust)
UPowerGlib-1.0upowersin 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:

  1. 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).
  2. 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

  1. ✅ HECHO — los seis están sellados y en la cola GNOME (verificado por takana hash, 2026-08-03): gnome-desktop b3:b3aed0d6 · geoclue b3:50e9de94 · libgweather b3:d289ac66 · ibus b3:d4d3750a · librsvg b3:351f4658 · upower b3:9c31f20f. Es lo que hace que el shell arranque; el arranque de hoy ve 59 typelibs de sistema + 3 del shell. El texto original del plan queda abajo por el costo de libelogind, que sigue vigente.

    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.

  2. ✅ HECHO — la política D-Bus de login1 (y la de PolicyKit1) viaja dentro del artefacto de su daemon, no en el script de imagen. Un servicio que no puede adueñarse de su nombre no es un servicio: la política es parte de lo que el paquete promete. El script de imagen ahora sólo las COPIA y verifica que llegaron — y esa verificación destapó de entrada que el script inyectaba el binario del shim de polkit sin su .conf.

  3. ✅ HECHO, y este ítem estaba mal escrito en dos sentidos. El identity mismatch no lo emite arje-logind-compat sino el bus_mediator de arje-zero; y no había nada que arreglar acá, porque ya estaba arreglado río arriba (tawasuyu ce96cfa8d, 2026-07-29). Lo que teníamos era un PID1 viejo: nuestro log decía rechazando request y la fuente actual dice sigo como anónimo. Tampoco era «sólo un WARN» — el commit del fix se llama «apagar/reiniciar moría por un ENTE_ID heredado, no por permisos»: ENTE_ID lo hereda todo el árbol de procesos, cualquier nieto reclamaba esa identidad con su propio pid y el mediador rechazaba la request entera. Se movió el pin de recipes/arje-zero.toml al mismo commit que ya usan los dos compat daemons (ver el comentario de la receta: por qué NO se cherry-pickeó, con la medición). La lección general: antes de arreglar un WARN, buscá quién lo emite y si ya está arreglado upstream. Nuestro pin estaba 3951 commits detrás y nadie lo había mirado.

  4. ✅ HECHO (2026-08-03) — validado CON PANTALLA de verdad (DISP=gtk, ventana en el laptop) y no sólo por screendump: el Overview pinta con el PID1 sellado nuevo, 689 colores distintos, azul GNOME (2,60,136) y grises del shell (34,34,38) al frente, cursor visible. Evidencia: gnome-shell-qemu-2026-08-03-pid1-sellado.png. Y aplicar la regla pagó otra vez: el arranque destapó una regresión que el serial daba por verde — ver abajo.

La carrera de PolicyKit1: el resumen final decía OK y los clientes ya estaban muertos

Arrancando para la validación con pantalla, accounts-daemon y upowerd murieron los dos:

Error calling StartServiceByName for org.freedesktop.PolicyKit1:
  Failed to execute program org.freedesktop.PolicyKit1: Permission denied
Failed to initialize daemon

El comentario del script ya decía que arje-polkit-compat «VA ANTES que accounts-daemon y upowerd», y iba antes — en el orden de las líneas. Pero se lanza en background y tarda ~2 s en adquirir org.freedesktop.PolicyKit1, mientras los otros dos arrancan de inmediato y llaman a polkit_authority_get_sync() dentro de ese hueco. Sin dueño del nombre, D-Bus intenta ACTIVAR el servicio, la activación falla, y los dos daemons se mueren.

Lo peligroso no fue el fallo sino el informe. Los tres esperar_nombre estaban todos al final: para cuando corrían, el shim ya tenía el nombre y el resumen imprimía bus: org.freedesktop.PolicyKit1 OK sobre dos clientes que hacía rato eran cadáveres. Un chequeo que corre después de la carrera no la mide.

El arreglo es mover la espera al PRODUCTOR: esperar_nombre se define antes del primer daemon, y el bloque de polkit espera su propio nombre antes de que se lance ningún cliente suyo. Es la misma lección de «un pid no es un servicio» aplicada del otro lado: lanzar en orden no es estar listo en orden. Después del fix: PolicyKit1 OK → accounts-daemon lanzado → upowerd sigue vivo → Accounts OK → UPower OK.

Dato suelto que dejó el mismo arranque: colord ahora MUERE («colord MURIÓ al arrancar») en vez de quedarse vivo sin adquirir el nombre. Su log sigue cortándose después de abrir las tres bases de datos. Cambió el síntoma, no la causa conocida; el ítem sigue aparcado.

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.