gioser

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…

TOML 1373Shell 116Rust 74Markdown 641915 Sutichaykuna

⎇ 3786b4de9f64ef894661d9dde2ebca6d9dc4a299 docs/runbooks/cosmic-desktop.md Markdown · 1363 siq'ikuna · 83.7 KB Ñawpa willay ch'uya

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

1# Runbook — COSMIC, el cuarto escritorio
2
3Estado al 2026-08-03: **LA SESIÓN COSMIC ARRANCA ENTERA.** `cosmic-session` —el camino de producción,
4no el andamio— levanta la cadena completa sobre el kernel de takana y arje-zero como PID1:
5
6```
7cosmic-comp → cosmic-settings-daemon → cosmic-notifications → cosmic-panel
8            → cosmic-osd → cosmic-bg
9```
10
11Los que todavía no existen (`cosmic-app-library`, `cosmic-launcher`, `cosmic-workspaces`,
12`cosmic-greeter`) sólo dejan una línea de `error!` y **la sesión sigue viva** — que es exactamente lo
13que la tabla de `.expect` de más abajo predijo, verificado en vez de supuesto.
14
15![sesión COSMIC en QEMU](../evidencia/cosmic-session-qemu-2026-08-03.png)
16
17Y el paso anterior: `cosmic-comp` arranca sobre el
18kernel de takana con arje-zero como PID1, toma el DRM, expone `wayland-1` y presenta frames; y
19`cosmic-bg` se conecta, entrega su buffer y el fondo aparece **con el color exacto que se le
20configuró** — `(0.13,0.29,0.53)` → `(33,74,135)`. O sea que la cadena cliente→compositor→KMS funciona
21de punta a punta. Este documento se escribe mientras se hace, no después.
22
23![cosmic-bg pintando en QEMU](../evidencia/cosmic-bg-qemu-2026-08-03.png)
24
25Que el color sea **el que se pidió** y no uno cualquiera es lo que hace de esto una medición y no una
26impresión: descarta que se esté viendo un buffer sin inicializar o el borrado del compositor.
27
28El paso anterior —el compositor solo, gris `(39,41,42)` y el cursor, 134 colores— quedó en
29[`cosmic-comp-qemu-2026-08-03.png`](../evidencia/cosmic-comp-qemu-2026-08-03.png), porque la
30diferencia entre las dos imágenes es exactamente la pregunta que el modo `bare` separa.
31
32COSMIC es el escritorio de System76, en Rust sobre **smithay** (compositor) e **iced/libcosmic**
33(clientes). Es el cuarto de takana, tras [mirada](mirada-usb-nvidia.md),
34[KDE Plasma 6](kde-qemu-desktop.md) y [GNOME](gnome-qemu-desktop.md).
35
36## Por qué NO se parece a los dos anteriores
37
38KDE y GNOME costaron lo que costaron por la misma razón: una torre de C con Qt o
39glib/GObject/introspección debajo, donde cada capa se paga entera antes de ver un píxel. COSMIC no
40tiene esa torre. La primera receta —`cosmic-session`— salió **a la primera y sin una sola dep en C**:
41ELF estático, cero `NEEDED`.
42
43Lo que sí hereda es lo que ya está pago:
44
45| lo que COSMIC necesita | de dónde sale | costo |
46|---|---|---|
47| las deps en C de smithay (drm, gbm, seat, input, udev, xkb, pixman) | `mirada-compositor`, que ya es un compositor smithay de este lab | **cero** — misma lista |
48| `libdisplay-info` + `hwdata` | copiadas byte a byte de la cola GNOME | **cero** — mismo hash, artefacto ya sellado |
49| `bash`, `dbus-run-session`, `hicolor-icon-theme`, mesa-llvmpipe | corpus | cero |
50| el andamiaje de arranque en QEMU (`run-qemu-desktop.sh`, base rootfs, console-getty, screendump) | campañas KDE/GNOME | cero |
51
52La deuda propia que sí trae, y que ninguna de las otras campañas tocó, es la del **unwinder de la std
53de rustc** cuando el enlace no es estático — la misma que ya está documentada para las recetas Cargo
54del corpus.
55
56## El pin es de la SUITE, no de cada paquete
57
58COSMIC versiona **todos** sus repos en lockstep con el tag `epoch-N`. Comprobado repo por repo: los
59doce componentes están en `epoch-1.5.0`.
60
61**No mezclar epochs entre piezas.** Comparten protocolos propios (`cosmic-protocols`) y el formato de
62configuración (`cosmic-config`); un desajuste ahí no da error de compilación, da un escritorio que
63arranca y no se entiende consigo mismo — que es mucho más caro de diagnosticar.
64
65## La cola
66
67`recipes/incoming-cosmic/`, aislada (otro agente comparte el repo; nunca `git add -A`).
68
69Las deps se resuelven **hermano → padre**: desde `incoming-cosmic/` se ven las recetas de
70`recipes/`, pero **no** las de `incoming-gnome/` ni `incoming-kde/`. Por eso `libdisplay-info` y
71`hwdata` entran copiadas.
72
73**Copiar es gratis sólo si TODA la clausura transitiva resuelve a lo mismo.** El ArtifactHash no
74depende de la ruta de la receta, así que un fichero idéntico *cuyas deps resuelven igual* da el mismo
75hash ⇒ cache hit sobre el artefacto que ya existe. Se cumplió con `libdisplay-info`, `hwdata` y
76`nasm`, y se verificó con `takana hash` **antes** de copiar (`b3:260f0519`, `b3:bdc36cf9`,
77`b3:33383070`), no después. Dónde deja de cumplirse —y por qué importa— está en la sección de
78`cosmic-settings-daemon`, más abajo.
79
80## Qué levanta la sesión, y por lo tanto el orden de ataque
81
82`cosmic-session/src/main.rs` es la lista autoritativa —el mismo método que cerró la capa JS de GNOME:
83*leé el fichero donde el programa declara lo que arranca, no lo descubras de a uno*:
84
85```
86cosmic-comp  →  cosmic-notifications · cosmic-panel · cosmic-app-library
87             →  cosmic-launcher · cosmic-workspaces · cosmic-osd · cosmic-bg · cosmic-idle
88```
89
90Más `cosmic-settings-daemon` y `xdg-desktop-portal-cosmic` fuera de esa lista.
91
92| pieza | estado | hash |
93|---|---|---|
94| `cosmic-session` | ✅ sellado | `b3:81f02351` |
95| `cosmic-icons` | ✅ sellado (671 SVGs) | `b3:94d06f77` |
96| `dav1d` 1.5.4 | ✅ sellado (dep arrastrada, ver gotcha 8) | `b3:aab7a874` |
97| **`cosmic-comp`** | ✅ **SELLADO — el gate** (filtra `ScreenReader`, ver atajos) | `b3:e871eee4` |
98| `cosmic-bg` (dinámico, PINTA) | ✅ sellado | `b3:b2cfc410` |
99| `cosmic-idle` (dinámico) | ✅ sellado | `b3:e1806acc` |
100| `pulseaudio` · `pipewire` (cadena del settings-daemon) | ✅ sellados | `b3:e1ff4736` · `b3:338d1d8c` |
101| `cosmic-panel` | ✅ sellado | `b3:ac5e48c5` |
102| `cosmic-osd` | ✅ sellado | `b3:f15cb9c1` |
103| `cosmic-settings-daemon` | ✅ sellado (filtra `ScreenReader`, ver atajos) | `b3:642919a5` |
104| `cosmic-notifications` | ✅ sellado | `b3:3e2a5478` |
105| `cosmic-launcher` | ✅ sellado | `b3:c17b4956` |
106| `cosmic-app-library` | ✅ sellado | `b3:b2a163e7` |
107| `cosmic-workspaces-epoch` | ✅ sellado (necesitó `mesa`, ver gotcha 11) | `b3:a031249e` |
108| **`cosmic-applets`** (19 binarios en uno) | ✅ **sellado** (necesitó `dbus-shared`, ver gotcha 10) | `b3:3e53a325` |
109| `pop-launcher` 1.2.7 (otro repo, ver abajo) | ✅ sellado a la primera | `b3:64a5cbaa` |
110| `gmp` 6.3.0 · `mpfr` 4.2.2 (cadena de la calculadora) | ✅ sellados a la primera | `b3:d99a0a5d` · `b3:13982d20` |
111| `libqalculate` 5.12.0 (`qalc`) | ✅ sellado — la calculadora del lanzador ANDA (ver abajo) | `b3:edb8d14d` |
112| **`cosmic-term`** 1.5.0 — la 1ª APLICACIÓN | ✅ **sellada a la primera** (arrastra `cosmic-files` y `wgpu`) | `b3:d0ea1c1e` |
113| `cosmic-files` 1.5.0 — la 2ª aplicación | ✅ sellada, **sin `gvfs` ni `wgpu`** (ver abajo) | `b3:48d2b072` |
114| `cosmic-settings` 1.5.0 — la 3ª aplicación | ✅ sellada (necesitó el parche de la doble barra) | `b3:716dac53` |
115
116**La cola está COMPLETA: 33 recetas, `escritorio-cosmic` 71/71.**
117
118**El mínimo viable de una sesión son cuatro**: `cosmic-comp` + `cosmic-settings-daemon` +
119`cosmic-notifications` + `cosmic-panel` (los tres `.expect` de la tabla de arriba).
120
121**El orden no es el de la lista de arranque.** `cosmic-bg` va antes que el panel a propósito: habla
122Wayland con `smithay-client-toolkit` y **no toca libcosmic ni iced**, mientras que panel, launcher,
123osd y workspaces son todos clientes de iced. Con el fondo sellado, «pinta el fondo pero no el panel»
124separa la capa de UI del transporte; sin él, las dos preguntas llegan juntas.
125
126### `cosmic-settings-daemon`: por qué queda al final y NO se resuelve copiando
127
128Es el único con cadena propia. Su workspace incluye `audio-server` como dep de path **no opcional**,
129que arrastra `cosmic-pipewire` → `pipewire-sys` → `libpipewire-0.3` por pkg-config. Y `pipewire` hoy
130vive sólo en las colas GNOME y KDE.
131
132**Acá el truco de copiar deja de ser gratis, y eso corrige lo que dice más arriba.** La regla completa
133es: *copiar una receta entre colas es gratis sólo si **toda su clausura transitiva** resuelve a lo
134mismo.* Se cumplió con `libdisplay-info`, `hwdata` y `nasm` —sus deps están todas en el corpus—, y
135**no** se cumple con pipewire: de sus catorce deps, cinco viven sólo en colas ajenas
136(`dbus-shared`, `alsa-lib`, `libsndfile`, `pulseaudio`, `zlib-shared`) y —lo decisivo— **`glib`
137resolvería distinto**. Desde `incoming-gnome`, `glib` es la sombra de esa cola (`b3:f6ccdf98`); desde
138`incoming-cosmic` sería la del corpus (`b3:93d2cad0`). Son recetas distintas, no la misma en dos
139lugares.
140
141O sea que copiar produciría un **segundo pipewire** construido contra otra glib, arrastrando su cadena
142entera. Y eso no es sólo caro: es exactamente el cuadro que la campaña GNOME ya midió y evitó —dos
143glib distintas con 370 rutas solapadas y dos registros de GType en un proceso—.
144
145Antes de tocar nada se midió con `yupana radio` (la puerta única, nunca `grep`): `pipewire` tiene
146radio 1 en GNOME y 2 en KDE, pero `glib` del corpus tiene **21 dependientes transitivos y afecta la
147imagen escritorio-gnome**.
148
149**Y después se midió el costo real, que resultó mucho menor que la estimación.** Copiar pipewire a
150`incoming-cosmic` necesita **cuatro** recetas hermanas —`alsa-lib`, `dbus-shared`, `libsndfile`,
151`pulseaudio`—, no las seis contadas a ojo, y **tres de ellas dan hash idéntico y ya están selladas**.
152Sólo `pulseaudio` y `pipewire` hay que construir.
153
154Eso refina la advertencia de arriba en el punto que importa: **el peligro de las dos glib es
155mezclarlas en UNA IMAGEN**, y la imagen COSMIC tiene una sola. Acá el costo es de builds, no de
156corrección. Lo que sigue vigente es no promover a ciegas entre colas.
157
158Gotcha del copiado, que cuesta un minuto de confusión: `[source] patches` se resuelve **relativo a la
159receta**, así que copiar el `.toml` sin su `.patch` da «no pude leer patch» y el hash ni siquiera se
160calcula.
161
162## 🚪 EL GATE PASADO: cosmic-comp compila (2026-08-03)
163
164`b3:b8abc52f`, 51 min 39 s de cargo con `CARGO_BUILD_JOBS=1` y `lto = "fat"`. **A la primera**, salvo
165el `--bin` del gotcha 1. Sin un solo parche a la fuente.
166
167Lo que dice el enlace es la parte que vale, porque es la medición y no la impresión:
168
169```
170NEEDED: libdisplay-info.so.2 · libgbm.so.1 · libseat.so.1 · libudev.so.1
171        libinput.so.10 · libpixman-1.so.0 · libxkbcommon.so.0 · libc.so
172```
173
174Ocho, y las siete primeras son **exactamente** los backends de smithay que la receta declara. La
175octava es musl. **No hay `libgcc_s`** — o sea que el compositor no arrastra la deuda del unwinder que
176sí tienen varias recetas Cargo del corpus, porque acá el enlace es dinámico contra musl y no hay
177`+crt-static` que forzar. Y **no aparece nada que no se haya declarado**: la clausura de build y la de
178runtime coinciden, que es la propiedad que uno quiere y casi nunca se cumple gratis.
179
180Que un compositor Wayland completo —DRM, GBM, EGL, libinput, seat, Vulkan, XWayland, un renderer
181multi-GPU y una capa de UI en iced— cierre con ocho librerías compartidas y ningún parche es el
182resultado más limpio de las cuatro campañas de escritorio.
183
184## 🖥 EL PRIMER ARRANQUE: qué falló y qué no (2026-08-03)
185
186Tres imágenes y tres causas distintas, todas medidas. Vale dejarlas porque **ninguna estaba en el
187camino que uno anticipa**, y las tres se manifestaron a varias capas de distancia de su causa.
188
189**1. `cosmic-session` no es opcional en el sentido que yo asumí — y `cosmic-settings-daemon` tampoco.**
190El compositor arrancó perfecto y le devolvió a la sesión su `WAYLAND_DISPLAY=wayland-1`; el gestor
191entonces intentó lanzar `cosmic-settings-daemon`, no lo encontró, y **panickeó**:
192
193```
194INFO  cosmic_session: got environmental variables from cosmic-comp: [("WAYLAND_DISPLAY", "wayland-1")]
195ERROR panic: 'failed to start settings daemon: NotFound' src/main.rs:255
196```
197
198Es un `.expect()`, sin feature que lo apague. **Y NO es el único — son TRES**, y esto corrige lo que
199esta misma sección afirmaba hace unas horas:
200
201| línea | componente | qué pasa si falta |
202|---|---|---|
203| `main.rs:255` | `cosmic-settings-daemon` | panic |
204| `main.rs:306` | `cosmic-notifications` | panic |
205| `main.rs:325` | `cosmic-panel` | panic |
206
207El resto (app-library, launcher, workspaces, osd, bg, idle) va por `start_component()`, que ante un
208fallo sólo hace `error!("failed to start …")` y sigue (`main.rs:558`). O sea que **el mínimo viable de
209una sesión COSMIC son cuatro binarios**: compositor + esos tres.
210
211**Cómo se equivocó el diagnóstico, que es la parte reusable**: leí `start_component()`, vi que sólo
212logueaba, y generalicé a «todos menos el settings-daemon». Pero notifications y panel **no pasan por
213esa función** — tienen su propio `.expect()` unas líneas antes. Buscar la función que lanza no es lo
214mismo que buscar los `\.expect(` del fichero; **grepear la construcción que mata cuesta lo mismo y no
215generaliza de más**. **Esto corrige lo que decía la sección de
216cosmic-settings-daemon**: no es que sin él se pierdan el brillo y el tema, es que **no hay sesión**.
217Queda por eso el modo `bare` en `cosmic-start` —el camino que upstream ya soporta como
218`install-bare-session`—: se lanza el compositor solo y los clientes a mano contra su WAYLAND_DISPLAY.
219Separa «¿compone?» de «¿arranca la sesión?», que son dos preguntas y estaban viniendo juntas.
220
221**2. La pantalla negra era `libz.so.1`.** El corpus trae `zlib` estática, que le alcanza a todo el
222cierre de COSMIC. Pero `kms_swrast_dri.so` es un `.so` que mesa abre con `dlopen` y NEEDea
223`libz.so.1`:
224
225```
226MESA-LOADER: failed to open kms_swrast: Error loading shared library libz.so.1
227WARN  cosmic_comp::backend::kms: Failed to add device /dev/dri/card0: Failed to initialize GBM device
228```
229
230Sin driver de software no hay GBM, y **el compositor arranca igual y presenta una pantalla negra**. La
231causa está a tres capas del síntoma. Se arregla agregando `zlib-shared` como raíz del hidratador.
232
233**3. Los clientes NO pueden ser estáticos.** `cosmic-bg` moría con «The wayland library could not be
234loaded» **teniendo `libwayland-client.so.0` en el rootfs**. `wayland-client` entra con la feature
235`dlopen`: carga libwayland en runtime en vez de enlazarla, y **un binario musl estático no tiene
236dlopen funcional**. No faltaba la librería: faltaba poder abrirla. Todos los clientes pasan a
237`link = "dynamic"`.
238
239**Lo que NO falló, y conviene decirlo:** el compositor no necesitó un solo parche, ni una perilla de
240KMS, ni forzar modeset legacy. Las cuatro perillas de render por software heredadas de KDE y GNOME
241(`GBM_ALWAYS_SOFTWARE`, `kms_swrast`, `LP_NUM_THREADS=1`) alcanzaron. GNOME costó cinco muros para
242llegar a este punto; COSMIC costó tres, y ninguno era del compositor.
243
244### Dos carreras con /run, la misma causa
245
246`dbus-daemon --system` falló con «Failed to bind socket /run/dbus/system_bus_socket: No such file or
247directory» **aunque el directorio se creaba al principio del script**, y dbus-run-session se quejó
248por su lado de que `XDG_RUNTIME_DIR` no existía. Entre el arranque del getty y el uso pasan varios
249segundos en los que arje-zero sigue montando, y un tmpfs sobre `/run` se lleva puesto lo que hubiera.
250**Crear justo antes de usar** es la única forma de no depender de ese orden; comprobar al principio y
251usar al final es exactamente cómo se cuela una carrera — la misma forma del bug de PolicyKit1 que se
252cerró hoy en GNOME.
253
254## Gotchas medidos (no previstos)
255
2561. **`cargo rustc --` exige UN target.** La fase compile del lab pasa flags tras el `--`, y cualquier
257   paquete con binario + lib + ejemplos corta con «extra arguments to `rustc` can only be passed to
258   one target». Se arregla con `flags = ["--bin", "<nombre>"]`. Le pasa a cosmic-comp y a cosmic-bg
259   (workspace con `config/`), y le va a pasar a casi todos: **ponelo desde el principio.**
2602. **`start-cosmic` es bash de verdad, no sh.** Usa `mapfile`, `[[ ]]` y expansión indirecta
261   `${!var}`; con el busybox del rootfs no arranca. `bash` ya está en el corpus, pero **tiene que
262   entrar en la imagen** — y no lo pide ningún `[deps]`, así que no lo va a traer nadie por accidente.
263   Lo mismo con `dbus-run-session` (de `dbus`), que es lo que el script exec-uta al final.
2643. **`rust-toolchain.toml` con `channel = "1.93"` es inerte acá** — es una perilla de rustup, y el
265   sandbox tiene el rustc de Alpine (1.96) pelado. Conviene saberlo antes de «arreglarlo».
2664. **`cosmic.desktop` existe DOS veces** (en cosmic-session y en cosmic-comp) con contenido distinto
267   y en la misma ruta. El de cosmic-comp es el de la «bare session» y apunta a `/usr/bin/cosmic-service`,
268   que vive en la rama de systemd que acá no va. Se instala **sólo** el de cosmic-session; si entraran
269   los dos, quién gana depende del orden de hidratación y el que pierde es el bueno.
2705. **`libsystemd` no es systemd.** La feature `default = ["systemd"]` de cosmic-comp trae el crate
271   `libsystemd`, que es una **reimplementación en Rust** del protocolo, no un binding a la C. Y
272   `logind` habla `org.freedesktop.login1`, que acá lo sirve `arje-logind-compat`. No hay que apagar
273   nada: el nombre de la feature es el del protocolo, no el del programa que lo contesta.
2746. **Las features de smithay que asustan no cuestan deps de build**: `backend_vulkan` (crate `ash`),
275   `backend_x11` (`x11rb` con `dl-libxcb`) y `xwayland` cargan todo por **dlopen**. Son deuda de
276   RUNTIME —libxcb y vulkan-loader viven hoy sólo en la cola KDE—, no un muro de compilación.
2777. **Todo cliente de la suite lleva las MISMAS CUATRO deps: `libxkbcommon`, `pkgconf`,
278   `libudev-zero`, `libinput`** — incluso las piezas sin interfaz. Las tres primeras se descubrieron
279   de a una, un build por dep, hasta que la línea de enlace las mostró juntas:
280
281   ```
282   -Wl,-Bstatic  -ludev  -linput  -lxkbcommon …
283   ```
284
285   **El error dice la que falta primero; el COMANDO dice todas.** Leerlo entero la primera vez habría
286   ahorrado tres builds. Los tres llegan por la cadena de libcosmic —enumeración de monitores y de
287   dispositivos de entrada—, no por nada que el cliente haga a la vista.
288
289   Y el camino hasta ahí fue de razonamientos buenos y falsos, que es lo que vale dejar escrito:
290   - `cosmic-bg` fue sin `[deps]` porque «habla el protocolo en Rust y no toca C». Reventó en el
291     `build.rs` de `smithay-client-toolkit` pidiendo `xkbcommon.pc`: **sctk ENLAZA libxkbcommon**; el
292     que la dlopea es `winit`, que es otra capa. Hablar Wayland en Rust dice cómo viaja el byte, no
293     con qué se interpreta un teclado.
294   - `cosmic-idle` fue sin `[deps]` porque *ese* argumento ya no aplicaba —no usa sctk— y un demonio
295     de inactividad no interpreta teclas. Reventó en el ENLACE con `-lxkbcommon`, arrastrado por
296     **`cosmic-settings-config`**: de ahí sale la tabla de atajos y la usa **todo** componente de la
297     suite. La dep no viene de lo que el paquete hace, sino de la librería de configuración común.
298
299   La lección de método vale más que la dep: **la dep no se deduce de la función del paquete.** Está
300   escrita en el `build.rs` que panickea o en la línea de enlace, y las dos veces el error dijo
301   exactamente quién y por qué. El `link = "static"` sobrevive porque el artefacto de libxkbcommon
302   trae `.a` además de `.so`.
3038. **Una feature apagada en el paquete puede estar prendida por otro.** `cosmic-bg` declara `image`
304   con `default-features = false` y sin AVIF, y aun así compila `dav1d-sys`: **las features de cargo
305   se UNIFICAN en el grafo**. Se resolvió trayendo `dav1d` (b3:aab7a874) en vez de parchear el
306   Cargo.toml de upstream — si el grafo dice que sabe decodificar AVIF, que lo sepa de verdad.
307   Mismo modo de falla que un `.pc Requires` arrastrando una dep que nadie declaró.
308
3099. **`cosmic-wallpapers` no se puede empaquetar del tarball: está en git-lfs.** El default de
310   cosmic-bg apunta a `/usr/share/backgrounds/cosmic/orion_nebula_nasa_heic0601a.jpg`, y el archivo
311   de GitHub de ese repo pesa **20 KB** — son punteros LFS, no imágenes. Una receta escrita del modo
312   normal produciría un paquete de ficheros de texto que además *parecería* correcto: se instalarían
313   con el nombre bueno en la ruta buena. Mientras tanto `cosmic-start` escribe la config de USUARIO
314   con una fuente `Color`, que cosmic-bg soporta de fábrica — y va en el lanzador, no en el artefacto,
315   porque es política de la imagen de prueba y no algo que el paquete prometa.
316
31710. **`build.rs` contento ≠ linker contento: `cosmic-applets` necesita la libdbus COMPARTIDA.**
318    Los applets de red, bluetooth y status-area hablan D-Bus por `libdbus-sys` (FFI a la C, no zbus
319    como el resto de la suite). Declarar `dbus` a secas alcanza para que su `build.rs` pase —le basta
320    el `.pc`, que el paquete del daemon trae— y el fallo aparece **una hora después, en el enlace
321    final del multiplexor**: `unable to find dynamic system library 'dbus-1'`. El corpus sólo
322    empaqueta `libdbus-1.a`; el `.so` vive en `dbus-shared`. Son dos preguntas distintas y las
323    contestan dos herramientas distintas: pkg-config dice si la dep *existe*, el linker si hay algo
324    que *enlazar*. Era la única receta de la cola que pedía la estática.
325
32611. **`cosmic-workspaces` necesita `mesa`, y la dep sale de lo que el programa MUESTRA.** La vista de
327    espacios de trabajo dibuja miniaturas vivas de las ventanas; el compositor se las pasa como
328    DMA-BUF por `zcosmic-toplevel-info` y el cliente importa el fd con GBM ⇒ `-lgbm`. Es el único
329    cliente de la suite que toca el pipeline gráfico además del compositor, así que el juego fijo de
330    cuatro (`libxkbcommon`, `pkgconf`, `libudev-zero`, `libinput`) no alcanza. Corolario del gotcha
331    de siempre: la dep no se deduce de la familia del paquete, se lee de lo que hace.
332
33312. **DOS GPUs en QEMU = el panel dibuja en la pantalla que no mirás.** `-device virtio-gpu-pci` no
334    reemplaza a la VGA por defecto de QEMU: la SUMA. El invitado ve `card0` y `card1`, cosmic-comp
335    crea dos outputs (`Virtual-1` y `Unknown-1`), `cosmic-bg` pinta el fondo en ambos y el panel se
336    ancla en uno solo. El `screendump` captura el otro ⇒ **fondo perfecto, panel invisible, y ni un
337    solo error en el log**. Con `-vga none` el escritorio aparece entero a la primera.
338    **Y esto corrige un diagnóstico anterior de este mismo runbook**: cuando el panel selló y no
339    dibujó, lo atribuí a que le faltaban los applets. Los applets hacían falta —sin ellos el panel
340    está vacío— pero **no eran la causa de la pantalla lisa**; la causa era la segunda GPU, que ya
341    estaba ahí. Un síntoma puede tener dos causas suficientes y arreglar la primera no prueba nada:
342    la prueba es la pantalla, no el razonamiento.
343
34413. **`mkdir: not found` en `cosmic-start`, y la sesión arrancaba igual.** El getty entrega un PATH
345    sin `/usr/bin`, y en la imagen fundida `mkdir` sólo vive ahí (`/usr/bin/mkdir → coreutils`; la
346    base metal no tendió el symlink `/bin/mkdir → busybox` que sí tienen chmod, mount, sed y tee).
347    Los cuatro `mkdir -p` de la preparación fallaban y **no se notaba**, porque los directorios
348    venían horneados en la imagen. Es la forma más traicionera de bug: gratis hoy, fatal el día que
349    alguien cambie cómo se arma la imagen. Fix: exportar `PATH` como primera línea del script.
350
351## Cómo se construye
352
353```sh
354cd ~/hammer
355./target/release/takana --store store hash recipes/incoming-cosmic/<pieza>.toml   # dry-run, ~2ms
356./target/release/takana build recipes/incoming-cosmic/<pieza>.toml --store store
357```
358
359## 🏔 EL ESCRITORIO ENTERO, DIBUJADO (2026-08-04)
360
361`docs/evidencia/cosmic-panel-applets-qemu-2026-08-04.png` — 1280×800, **895 colores distintos**:
362
363- **panel superior** (banda `(27,27,27)`, 32 px): botones *Workspaces* y *Applications* a la
364  izquierda, reloj al centro, y a la derecha los applets de accesibilidad, audio, red, batería y
365  encendido;
366- **dock inferior** centrado y redondeado, con lanzador, workspaces, biblioteca de aplicaciones y
367  las entradas `.desktop` del rootfs;
368- fondo `(33,74,135)` = exactamente el `Color(Single((0.13,0.29,0.53)))` que escribe `cosmic-start`.
369
37048 procesos `cosmic-*` estables a los 30 s, sobre el kernel de takana con arje-zero como PID1.
371Se reproduce así (headless y automatizable; **el `-vga none` no es opcional**, ver gotcha 12):
372
373```sh
374COSMIC_MODE=session bash scripts/cosmic/qemu-desktop-image.sh
375qemu-system-x86_64 -machine q35,accel=kvm -cpu host -smp 8 -m 8192 \   # 8 vCPU: ver el deadline del panel
376  -drive if=pflash,format=raw,readonly=on,file=/usr/share/edk2/x64/OVMF_CODE.4m.fd \
377  -drive if=pflash,format=raw,file=/tmp/OVMF_VARS.fd \
378  -drive file=work/hammer-cosmic-qemu.img,format=raw,if=virtio \
379  -vga none -device virtio-gpu-pci,xres=1280,yres=800 \
380  -device qemu-xhci,id=xhci -device usb-kbd,bus=xhci.0 -device usb-tablet,bus=xhci.0 \
381  -display none -serial file:/tmp/cosmic.serial -qmp unix:/tmp/qmp.sock,server,nowait
382# y después, por QMP: {"execute":"screendump","arguments":{"filename":"/tmp/shot.ppm"}}
383```
384
385El veredicto es **contar colores distintos del PPM**, no leer el log: con dos GPUs el log salía
386impecable y la pantalla estaba lisa.
387
388## 🖱 LA ENTRADA: se usa, no sólo se ve (2026-08-04)
389
390**Hay que usar USB HID, no virtio-input.** El kernel de takana no trae `VIRTIO_INPUT` (mirá el
391`-e …` de `recipes/linux-generic.toml`), así que `-device virtio-keyboard` no produce ni un
392`/dev/input/event*`. Sí trae `USB_XHCI_HCD` + `USB_HID` + `INPUT_EVDEV`, y además es lo que se va a
393usar en metal, así que la prueba es más fiel:
394
395```
396-device qemu-xhci,id=xhci -device usb-kbd,bus=xhci.0 -device usb-tablet,bus=xhci.0
397```
398
399`usb-tablet` y no `usb-mouse`: reporta coordenadas ABSOLUTAS, y QMP sólo sabe mandar eventos `abs`
400(el eje va de 0 a 32767 sobre el ancho/alto de la pantalla, no en píxeles).
401
402**Se maneja entero por QMP**, sin ver la pantalla y sin humano:
403
404```jsonc
405// mover el puntero a (155,16) — el botón «Applications»
406{"execute":"input-send-event","arguments":{"events":[
407  {"type":"abs","data":{"axis":"x","value":3968}},
408  {"type":"abs","data":{"axis":"y","value":655}}]}}
409// click
410{"execute":"input-send-event","arguments":{"events":[{"type":"btn","data":{"down":true,"button":"left"}}]}}
411{"execute":"input-send-event","arguments":{"events":[{"type":"btn","data":{"down":false,"button":"left"}}]}}
412// una tecla
413{"execute":"input-send-event","arguments":{"events":[
414  {"type":"key","data":{"down":true,"key":{"type":"qcode","data":"c"}}},
415  {"type":"key","data":{"down":false,"key":{"type":"qcode","data":"c"}}}]}}
416```
417
418**El veredicto también es de píxeles, y conviene medirlo por REGIÓN.** El click en *Applications*
419llevó el color dominante de `(33,74,135)` (fondo, 91 %) a `(27,27,27)` (84 %): la biblioteca de
420aplicaciones se abrió a pantalla casi completa. Y para el teclado, contar colores del rectángulo del
421buscador (440..840 × 80..112) bastó: 155 → 118 colores al reemplazarse el placeholder por lo tecleado.
422Evidencia: `cosmic-app-library-click-qemu-2026-08-04.png` y `cosmic-teclado-busqueda-qemu-2026-08-04.png`.
423
424**La biblioteca sale vacía y está BIEN.** Los 20 `.desktop` de la imagen son `NoDisplay=true` —son
425applets y componentes de sesión, no aplicaciones—, así que no hay nada que listar. Que la búsqueda no
426devuelva resultados no es un fallo del escritorio: es que la distro todavía no tiene ni una sola
427aplicación gráfica. Se sabrá arreglado cuando entre `cosmic-files` o un terminal.
428
429### 🔑 Los atajos: dos ficheros envenenados por la misma línea
430
431`Super+A` abre la biblioteca y `Super+W` la vista de espacios de trabajo
432(`cosmic-super-w-workspaces-qemu-2026-08-04.png`, con **miniatura viva** del escritorio dentro — o
433sea que el camino DMA-BUF→GBM del gotcha 11 anda de verdad). Para llegar ahí hubo que sanear DOS
434ficheros de datos, no uno:
435
436| fichero | lo instala | qué liga |
437|---|---|---|
438| `…Shortcuts/v1/defaults` | `cosmic-comp` | tecla → acción |
439| `…Shortcuts/v1/system_actions` | `cosmic-settings-daemon` | acción → comando |
440
441Los dos traen una entrada `ScreenReader`, una variante que `cosmic-settings-config` —el crate que los
442parsea, pineado por cargo en la rev `8c54bbbc`— todavía no tiene en su enum. **RON no ignora lo que
443no conoce: falla la deserialización del MAPA COMPLETO.** Con `defaults` roto no hay ligaduras; con
444`system_actions` roto hay ligadura pero ningún comando que ejecutar. Hacen falta los dos `grep -v`.
445
446**Y acá me equivoqué de fichero teniendo el dato exacto delante.** El error decía
447`3:5-3:17: Unexpected variant named 'ScreenReader'` — con línea y columna. Busqué el *token* en vez
448de preguntarme *qué fichero lo tiene en esa posición*, di con `keybindings.ron` (que también lo
449traía, en la línea 94), lo arreglé, reconstruí `cosmic-comp` **cuarenta minutos** y el error salió
450igualito. Era otro fichero, de otro paquete. *Cuando el error trae coordenadas, la primera pregunta
451es de qué fichero son, no dónde más aparece esa palabra.*
452
453**Es el riesgo del pin lockstep asomando desde adentro de upstream**: el dato y el parser del *mismo*
454epoch no coinciden.
455
456**`Super` abre el lanzador** — pero hizo falta un paquete más, y de otro repo: ver abajo.
457
458### 🔎 `pop-launcher`: el lanzador es DOS programas
459
460`Super` no abría nada y el único rastro era una línea que no menciona ninguna tecla:
461`ERROR pop-launcher failed to start: No such file or directory`. **`cosmic-launcher` es sólo la
462interfaz**: dibuja la lista y manda lo tecleado por stdin a un proceso hijo, `pop-launcher`, que es
463quien busca. Sin ese binario la ventana no tiene qué mostrar, así que no se muestra — el mismo modo
464de falla que el panel sin applets: *lo que falta es un HIJO, no una librería, y el log del padre se
465lee sano.*
466
467Vive en **otro repo** (`pop-os/launcher`), no versiona por `epoch-N` y no entra en el pin lockstep.
468**La versión la fija el `Cargo.lock` de cosmic-launcher** —rev `a332a3a7` de la 1.2.7—, no el último
469tag del repo: el lock es lo que garantiza que hablen el mismo protocolo. El tarball se pide por
470commit (`/archive/<sha>.tar.gz`), porque no hay tag que nombre esa rev. Selló a la primera
471(`b3:64a5cbaa`) con **un solo `NEEDED`: `libc.so`**.
472
473Es otro **multiplexor por symlink**, como cosmic-applets: un binario, y cada plugin es un enlace con
474su nombre dentro de su propio directorio junto al `plugin.ron` que lo describe. El servicio enumera
475`plugins/*/`; si está el `.ron` y falta el enlace —o al revés— el plugin no existe para él, **sin
476error**. Ojo con el `sed 's/_/-/'` de upstream: cambia sólo el primer guión bajo, así que el
477directorio es `desktop_entries` y el ejecutable de adentro `desktop-entries`. Son dos nombres
478distintos a propósito.
479
480Verificado de punta a punta (`cosmic-super-lanzador-`, `cosmic-pop-launcher-plugins-`,
481`cosmic-pop-launcher-calc-qemu-2026-08-04.png`): `Super` abre el lanzador, `?` lista los seis plugins
482con su sintaxis —o sea que la enumeración y el IPC JSON-sobre-stdio andan— y `=2+2` **lanza el
483plugin, que contesta «qalc command is not installed»**. Esa respuesta es la mejor prueba que había
484disponible: el proceso hijo corrió, evaluó la consulta y devolvió una fila. Lo que falta ahora es
485`libqalculate` (el binario `qalc`), que es un paquete, no un problema.
486
487### 🧮 `libqalculate`: la calculadora del lanzador, y un `stdin` que era `stdout`
488
489**`=123*456` → `56088` en el lanzador** (`cosmic-calculadora-qemu-2026-08-04.png`). La cadena entera
490desde una tecla está en el catálogo: `Super` → cosmic-launcher → pop-launcher → plugin `calc` →
491`qalc` → **mpfr → gmp**.
492
493Pero el camino hasta ahí tuvo dos muros que vale la pena dejar escritos, porque ninguno se parecía a
494lo que era.
495
496**1) `stdin` apuntaba al descriptor 1.** El plugin no le pasa la cuenta por argv: arranca `qalc` SIN
497expresión, con stdin en tubería, le escribe la expresión y cierra. En ese modo el binario **no leía
498nada** — ni error ni resultado, la fila del lanzador salía en blanco. El síntoma acusaba a readline,
499al toolchain y a la versión, y no era ninguno:
500
501```
502DIAG-main isatty=0 feof=0 ferror=0 fd=1        ← fileno(stdin) == 1
503nm -S qalc | grep -E ' (stdin|stdout|stderr)$'
504  1847148 8 B stderr
505  1847148 8 B stdin                             ← las TRES en la misma dirección
506  1847148 8 B stdout
507```
508
509La causa: **el binario salía DINÁMICO aunque la receta dice `link = "static"`, porque libtool se come
510el `-static` que inyecta el harness** ([[link-static-mentira-libtool]] otra vez, cuarto caso). Y en
511un musl dinámico las tres variables de stdio se resolvieron por *copy relocation* a **una sola ranura
512de BSS compartida**: la última que escribe el loader gana, así que el `stdin` del programa era el
513`stdout`. De ahí la asimetría que parecía absurda: `qalc -f /dev/stdin` funcionaba —abre el
514descriptor por RUTA, sin tocar el `FILE*` roto— y `qalc` leyendo `stdin` no.
515
516El fix es el par que ya usa `libxml2`: `make LDFLAGS="-all-static -no-pie"`. Con él el binario queda
517estático de verdad y los tres símbolos vuelven a tener direcciones distintas, en `.rodata`:
518
519```
520  11e71c8 8 R stderr
521  11e71d0 8 R stdin
522  11e71d8 8 R stdout
523```
524
525**La regla, y es de las caras: `link = "static"` en la receta no garantiza un binario estático.**
526Cuando algo se comporta como si su `stdin`/`stdout` estuvieran cruzados, `readelf -d` y
527`nm -S | grep ' std'` contestan en dos segundos lo que el log no dice nunca.
528
529Barrido de la cola entera con ese criterio, después del fix: de las 7 recetas que declaran
530`link = "static"` (`cosmic-session`, `gmp`, `mpfr`, `nasm`, `libqalculate` y las dos que en realidad
531son `dynamic`), **ninguna tiene `NEEDED` indebido y ninguna tiene los símbolos de stdio aliasados**.
532Era un caso, no un patrón — pero el patrón que lo causa (libtool comiéndose el `-static`) ya tiene
533cuatro víctimas anteriores en el corpus.
534
535**2) La pregunta de primer arranque se come la cuenta, SIEMPRE.** Antes de leer nada, `qalc` pregunta
536si activar `autocalc`. El plugin le manda la expresión y cierra: la expresión se consume como
537respuesta, no es un sí/no válido, y qalc **no llega a guardar config** ⇒ la próxima vez vuelve a
538preguntar. No es «falla la primera vez»: no anda nunca. Se siembra
539`~/.config/qalculate/qalc.cfg` con `calculate_as_you_type=0` desde `cosmic-start` — política de la
540imagen, como el color de fondo; una distro de verdad lo pondría en `/etc/skel`.
541
542**Gotchas de empaquetado de la cadena:**
543- **`gmp` va con `--disable-assembly`.** Elige rutinas en ensamblador mirando la CPU de quien
544  compila, o sea hornearía la ISA del laptop en el artefacto y el hash dejaría de significar lo mismo
545  en la granja. Es una calculadora de escritorio: la implementación en C portable sobra.
546- **El configure de libqalculate no encuentra readline** porque prueba `-lreadline` junto a
547  `-lncurses`, `-lcurses`, `-ltermcap` y `-ltinfo`, y el corpus **sólo empaqueta `libncursesw.a`**.
548  Que el paquete se llame `ncurses` no significa que provea `-lncurses`. Los alias van en un
549  directorio LOCAL (`.curses-compat/`) y no en `/usr/lib`: esa vista es la evidencia de qué se
550  declaró, y ensuciarla para que pase un test deja mintiendo justo al mecanismo que existe para no
551  mentir. Ojo: hay que repetir el `-L` en el `make`, porque el `LDFLAGS` del `make` PISA al del
552  `configure`.
553
554### 🖥 `cosmic-term`: la primera APLICACIÓN, y la biblioteca deja de estar vacía
555
556`docs/evidencia/cosmic-terminal-qemu-2026-08-04.png`:
557
558```
559bash-5.3# uname -srm; qalc -t '6*7'
560Linux 6.16.12 x86_64
56142
562```
563
564Una ventana de verdad —con decoración, *File/Edit/View*, minimizar/maximizar/cerrar—, `bash` corriendo
565adentro, el **kernel propio de takana** contestando, y el `qalc` que empaquetamos una hora antes
566devolviendo 42. Se abre haciendo click en su icono en la biblioteca de aplicaciones
567(`cosmic-biblioteca-con-app-qemu-2026-08-04.png`), que hasta hoy salía vacía **con razón**: los 20
568`.desktop` de la imagen eran `NoDisplay=true`. Éste no lo es, y es el primero.
569
570**Selló a la primera, y eso es noticia por lo que arrastra**: su `Cargo.toml` depende de
571**`cosmic-files`** —el gestor de ficheros entra como LIBRERÍA, por el selector y el
572arrastrar-y-soltar—, así que compilar la terminal compila medio gestor de ficheros. Mismo patrón que
573`cosmic-settings-config` en los applets: **en esta suite las «aplicaciones» se usan unas a otras como
574crates**, y el grafo de deps de Cargo no se parece al mapa de componentes.
575
576`wgpu` viene en `default`, al revés que el resto de los clientes que pintan con tiny-skia. Se dejó
577como está: `wgpu` y `naga` ya se habían compilado enteros para `cosmic-workspaces`, así que el costo
578estaba pago, y apartarse de las features por defecto es apartarse de la única combinación que
579upstream prueba. Cierra con **dos `NEEDED`**: `libxkbcommon.so.0` y `libc.so`.
580
581**Terminfo, para la próxima:** la ncurses del corpus es *libs-only* y no instala la base de terminfo,
582pero trae `--with-fallbacks=…,xterm-256color,…` compilados. A la terminal no le hace falta —el que
583necesita terminfo es lo que corre ADENTRO— y el hijo acá es el `sh` de busybox, que no lo usa. El día
584que entren `vim` o `htop` en la imagen, hará falta un paquete de datos de terminfo.
585
586### 📁 `cosmic-files`: «ya está compilado» era falso por una línea del vecino
587
588`docs/evidencia/cosmic-files-qemu-2026-08-04.png`: el gestor abre desde la biblioteca y **lista el
589sistema de ficheros real de takana** — `bin` (72 ítems), `dev` (129), `ente`, `etc`, `lib`,
590`lost+found`—, con su barra lateral de Recientes/Carpeta personal/Papelera.
591
592Entró creyendo que era casi gratis, porque `cosmic-term` **ya compila cosmic-files como crate**. No
593lo era, y el motivo cabe en una línea del `Cargo.toml` del vecino: `cosmic-term` lo declara con
594`default-features = false`. **Lo que un paquete cuesta depende de con qué features lo pide quien lo
595usa**, así que «ya se compiló» puede ser falso.
596
597**Se apagaron dos features por defecto, y las dos por una razón medida:**
598
599- **`gvfs`** trae los crates `gio`/`glib`, y el enlace final pide las glib **compartidas**
600  (`unable to find dynamic system library 'gio-2.0'`, y detrás gobject, glib, ffi, pcre2, z). El
601  corpus las empaqueta **sólo estáticas**: haría falta una cadena `-shared` entera, que es una
602  campaña aparte y mete una segunda glib en la imagen. Se pierden los **montajes remotos** (SFTP,
603  MTP), no la navegación local. ⚠ Y por eso tampoco se construye `cosmic-files-applet`: su
604  `Cargo.toml` fija `gvfs` a mano, así que apagarla en el paquete raíz no lo alcanza.
605- **`wgpu`** desbordó el filesystem: `No space left on device` creando un temp dir en `/src/target`,
606  **dos veces**, con wgpu+naga+ash+glow+spirv a `codegen-units=1`. Sin él el árbol de build queda en
607  ~2 GB. No se pierde nada: todos los demás clientes de la suite pintan con tiny-skia y la imagen no
608  tiene GPU.
609
610**El intento con `glib` declarado dejó una regla que vale para toda la cola:** el error fue
611`Package 'libpcre2-8', required by 'glib-2.0', not found` — o sea que **el que falla no es la dep
612sino lo que su `.pc` declara en `Requires`**. Declarar una dep trae su artefacto, no su clausura de
613pkg-config. Se lee en dos segundos con `grep ^Requires` sobre los `.pc` del artefacto, ANTES de
614gastar el build: `glib-2.0.pc → libpcre2-8`, `gobject-2.0.pc → libffi`, `gio-2.0.pc → zlib`.
615
616### ⚙ `cosmic-settings`: un panel de control que NO arrastra medio sistema
617
618`docs/evidencia/cosmic-settings-qemu-2026-08-04.png`: abre desde el lanzador (`Super`, teclear
619«settings») con la barra lateral entera —Red, Bluetooth, Accesibilidad, Escritorio, Pantallas,
620Sonido, Energía, Dispositivos de entrada, Aplicaciones, Fecha y hora, Sistema y cuentas— y la página
621de Escritorio poblada. **Tarda ~20 s en aparecer** con render por software; no está colgado.
622
623**La sorpresa buena: no arrastra NetworkManager, ni libpulse, ni udisks, ni accountsservice.** Habla
624con los daemons por D-Bus vía `zbus`, que es Rust puro. Se verificó ANTES de escribir la receta
625listando los crates `-sys` del `Cargo.lock` —que es donde vive la verdad sobre qué C hace falta—, y
626salieron sólo: `drm-sys`, `input-sys`, `libudev-sys`, `dav1d-sys`, `wayland-sys` (dlopen) y
627`gettext-sys` (trae su propio C). O sea el juego fijo de todo cliente + `libdrm` + `dav1d`.
628**Regla barata que ahorra builds: `grep '^name = ".*-sys"' Cargo.lock | sort -u` antes de adivinar
629deps por lo que el programa *hace*.**
630
631**Volvió a morder la doble barra de `cosmic-protocols//`**, igual que en `cosmic-applets`: el
632manifiesto usa esa forma para que cargo trate la fuente como distinta y el `[patch]` no se apunte a
633sí mismo, GitHub le contesta 404, y el build muere en el *fetch* con tres reintentos de red y
634`failed to load source for dependency 'cosmic-client-toolkit'`. Mismo fix: mover la barra al medio
635(`pop-os//cosmic-protocols`) en `Cargo.toml` **y** `Cargo.lock`. **Segunda víctima ⇒ es un patrón de
636la suite, no una rareza de un repo**: conviene mirarlo apenas un `cargo vendor` da 404.
637
638Instala **32 `.desktop`**, uno por página, así que cada sección del panel aparece como entrada propia
639en la biblioteca. Y `resources/default_schema/` va con `find`, no con `cp` plano: es el árbol
640`<Componente>/v1/<clave>` de cosmic-config, donde **cada clave es un fichero cuyo nombre es la
641clave** — aplanarlo deja el escritorio sin esa configuración, callado.
642
643**Deuda que deja anotada su propio log:** `org.freedesktop.locale1 was not provided by any .service
644files` — nadie sirve `localed` en la imagen, así que la página de idioma no va a poder cambiar nada.
645Es del mismo tipo que `login1` (que sí resuelve arje-logind-compat), y se arregla del mismo modo.
646
647### ⏱ El panel tiene un DEADLINE de arranque: con 4 vCPU sale mutilado
648
649Medido: con `-smp 4` el panel arranca **sin ala izquierda, sin reloj y sin dock** —sólo la bandeja
650derecha— y el único rastro es `Failed to setup panel dbus server deadline has elapsed`. Con
651`-smp 8 -m 8192`: **4 arranques de 4 con el escritorio completo y cero deadlines vencidos**.
652La causa es la estampida del arranque: el panel registra su servidor D-Bus mientras catorce clientes
653de iced escanean fuentes al mismo tiempo.
654
655**CORRECCIÓN medida el 2026-08-04 validando `cosmic-edit`: la carga del anfitrión TAMBIÉN cuenta, y
656`-smp 8` no la compensa.** Acá decía «no es carga del anfitrión (falla con el anfitrión ocioso)» —
657cierto para el caso de 4 vCPU, pero se leía como que con 8 el anfitrión daba igual, y no. Dos
658arranques de la MISMA imagen con la MISMA línea de QEMU (`-smp 8 -m 8192`):
659
660| anfitrión | colores | veredicto |
661|---|---|---|
662| `load average` ≈ 6 (otro agente compilando) | **130** | mutilado, y `deadline has elapsed` en el log |
663| ocioso (`load` ≈ 1,8) | **1671** | completo |
664
665Tiene sentido y conviene decirlo entero: **este anfitrión tiene 8 núcleos** (`nproc`), así que
666`-smp 8` le entrega la máquina entera a la VM y cualquier otra cosa que compile compite por los
667mismos hilos. La receta práctica: antes de creerle a un arranque mutilado, mirá `/proc/loadavg` y
668repetí con el anfitrión quieto — es un minuto y evita perseguir una regresión que no existe, que es
669exactamente el error que ya está documentado dos párrafos más abajo.
670
671**Y una lección de medición que costó horas: el conteo de procesos NO sirve como veredicto.**
672Perseguí una regresión inexistente comparando «48 procesos» contra «31», revirtiendo uno por uno
673tres cambios míos que no tenían nada que ver — cuando un escritorio completo y sano muestra **32**.
674El número de procesos no distingue «arrancó» de «dibujó». El framebuffer sí, y sin ambigüedad:
675**894 colores = completo, 130 = mutilado.** (Con las cuatro aplicaciones en el dock el escritorio
676completo da ~1670: el umbral es «cientos vs. miles», no un número exacto.)
677
678## ✎ LA CUARTA APLICACIÓN: cosmic-edit (2026-08-04)
679
680`docs/evidencia/cosmic-edit-qemu-2026-08-04.png` y `…-escribiendo-…png` — 1280×800, **2105 colores**:
681ventana con menús *File/Edit/View*, pestaña *New document*, numeración de líneas, y el icono del
682editor en el dock con el punto de «en ejecución». La segunda captura tiene `takana edita` escrito
683por teclado sobre la línea 1 y el punto de modificado en la pestaña: **se escribe, no sólo se ve**.
684
685Se llegó por el lanzador, sin tocar el ratón para elegir: `Super` → teclear `text` → el primer
686resultado es *«Text editor for the COSMIC desktop»* (`Ctrl+1`) → `Enter`. Que el editor sea lo que
687responde a «text» es efecto del `.desktop` que genera `xdgen`, con sus `Keywords` y su
688`MimeType=text/plain` — el mismo fichero que lo hace manejador por defecto de los `.txt` en el
689gestor.
690
691**Lo que la receta enseñó, y que corrige un borde de la regla del `Cargo.lock`:** leer los crates
692`-sys` del lock sigue siendo lo primero que hay que hacer, pero **el lock lista lo POSIBLE, no lo
693ENCENDIDO**. En cosmic-edit aparecen `gio-sys`, `glib-sys` y `gobject-sys` —que en cualquier otro
694paquete mandarían a declarar glib, y de ahí a la campaña de la cadena compartida— y entran sólo por
695la feature `gvfs`, que se apaga. El lock dice el universo; las features dicen el recorte. La receta
696no declara ninguna glib y su binario enlaza **sólo `libxkbcommon.so.0` y `libc.so`**: dos NEEDED, el
697mínimo de la suite.
698
699## 🛍 LA QUINTA: cosmic-store, y el catálogo que hubo que fabricar (2026-08-05)
700
701`docs/evidencia/cosmic-store-qemu-2026-08-05.png` — la tienda abierta con *Popular apps* poblada
702(App Library, Files, Launcher, Settings) y *Recently updated*; y `…-busqueda-…png` con la búsqueda
703`text` devolviendo *COSMIC Text Editor*. Sellada en `b3:a06437f7`, NEEDED = `libxkbcommon` + `libc`.
704
705**Es la primera aplicación de la suite que choca con que takana es OTRA distro.** Una tienda es la
706cara de un gestor de paquetes. De sus cuatro backends (`src/backend/`, todos tras `#[cfg(feature)]`):
707`flatpak` pide `libflatpak` (GObject ⇒ glib compartida + ostree/libsoup/gpgme: la torre de C que esta
708campaña evita); `packagekit` es Rust puro pero necesita el DEMONIO en el bus; `rpm-ostree` es
709irrelevante; **`pkgar` es el único que arranca** — ni demonio ni enlace.
710
711**ALCANCE: navega, no instala.** Nada está cableado al `.swm` (Etapa F). El camino barato para
712arreglarlo quedó identificado: servir `org.freedesktop.PackageKit` sobre `.swm` y encender la feature
713`packagekit`. Es un shim de D-Bus, mismo patrón que `arje-logind-compat`, y **no cuesta un solo `.so`
714de C**.
715
716### El catálogo AppStream: dos trampas encadenadas
717
718**1. `pkgar` NO lee `/usr/share/metainfo/`.** Lee el CATÁLOGO —
719`{/usr/share,/var/lib,/var/cache}/{swcatalog,app-info}/{xml,yaml}/` — que en una distro normal genera
720`appstreamcli compose` agregando los metainfo en un `<components version=… origin=…>`. Sin ese
721fichero la tienda **abre perfecta y no lista NADA**. Lo genera ahora `qemu-desktop-image.sh` con doce
722líneas de python; no hace falta AppStream ni glib.
723
724**2. Hay que SACAR el `<provides>`.** Los metainfo que genera `xdgen` lo escriben con los envoltorios
725`<mimetypes>`/`<binaries>`, y el crate `appstream` espera los hijos planos del esquema de catálogo.
726Con el envoltorio muere con `The tag provide doesn't have a value` y **descarta el componente entero,
727callado**: listaba 2 de 7, justo los dos sin `<provides>` (App Library y Launcher).
728
729**Cómo se diagnosticó, que es lo reutilizable:** desde el lanzador la salida del proceso NO llega al
730log de la sesión. Hay que abrir `cosmic-term` DENTRO de la VM y correr
731`rm -rf ~/.cache/cosmic-store; RUST_LOG=info cosmic-store 2>/tmp/s.log` — el `rm` no es opcional, la
732tienda cachea el parseo en `~/.cache/cosmic-store/*.bitcode` y si no se borra vuelve a mostrar el
733resultado viejo. Que la imagen tenga terminal deja de ser una comodidad y pasa a ser instrumental.
734
735### Y el deadline del panel es una CARRERA, no un umbral
736
737Con el anfitrión ocioso y `-smp 8`, un arranque de esta tanda salió igual mutilado (130 colores) con
73833 procesos vivos y el `deadline has elapsed` en el log. O sea: 8 vCPU + anfitrión quieto **sube
739mucho la probabilidad pero no garantiza**. Si sale mutilado y la carga estaba baja, **rearrancá antes
740de buscar la causa en tu cambio**.
741
742## 📷 LA SEXTA: cosmic-screenshot, sellada y ROTA A PROPÓSITO (2026-08-05)
743
744`b3:8b57ec9b`, 9 minutos de build, **`NEEDED` = `libc.so` y nada más** — un solo NEEDED, y el único
745`[deps] build = []` de la campaña: 130 líneas de Rust, ni `libxkbcommon` ni `libinput`, porque no
746dibuja ni habla wayland. Todo su trabajo es `Screenshot::request()` de `ashpd`.
747
748**Se incluye en la imagen sabiendo que falla, para que el hueco sea medible.** El error exacto está
749en `docs/evidencia/cosmic-screenshot-sin-portal-2026-08-05.png`, corrido desde `cosmic-term` dentro
750de la VM:
751
752```
753panicked at src/main.rs:76:10: failed to send screenshot request: Portal(ZBus(MethodError(
754  OwnedErrorName("org.freedesktop.DBus.Error.ServiceUnknown"),
755  Some("The name org.freedesktop.portal.Desktop was not provided by any .service files"))))
756```
757
758Misma forma que la deuda de `org.freedesktop.locale1` que anota el log de settings: **un nombre de
759D-Bus que nadie sirve**.
760
761### La cadena del portal son TRES eslabones, no dos
762
763Medido en el fuente de `xdg-desktop-portal-cosmic`: `static DBUS_NAME =
764"org.freedesktop.impl.portal.desktop.cosmic"` (`src/main.rs:27`). Es un **backend**; no toma el
765nombre que los clientes buscan. La cadena es:
766
767`cosmic-screenshot` → `org.freedesktop.portal.Desktop` → **xdg-desktop-portal** (frontend, C sobre
768GIO) → `org.freedesktop.impl.portal.desktop.cosmic` → **xdp-cosmic** (backend, Rust)
769
770**Faltan los dos**, y ninguno está en ninguna cola (`xdg-desktop-portal-gnome` sí está, pero es otro
771backend). Lo que sí hay para el backend: `pipewire`, `gbm` (mesa) y `wayland` ya en el corpus. Su
772único bloqueo real es que declara `cosmic-files` con la feature `gvfs` fijada a mano — el mismo
773patrón que ya bloqueó `cosmic-files-applet`, y acá es **un parche de una línea**.
774
775### ⚠ CORRECCIÓN: la cadena glib COMPARTIDA YA EXISTE Y ESTÁ SELLADA
776
777Este documento decía que `gvfs` exigía «una campaña propia». **Es falso desde que la campaña KDE
778selló las piezas.** Medido con `takana hash` desde las dos colas, que es el método correcto para
779saber si una receta se puede compartir:
780
781| receta | cola | ¿resuelve igual desde `incoming-cosmic`? |
782|---|---|---|
783| `glib-shared` 2.88.1 | `incoming-kde` | **sí** — `b3:0584ce1f`, ya sellada |
784| `pcre2-shared` | `incoming-kde` | **sí** — `b3:f254abaa`, ya sellada |
785| `zlib-shared` | ya en `incoming-cosmic` | — |
786
787Hash idéntico ⇒ **cero rebuild**: los artefactos sirven tal cual. `libffi` entra estático dentro de
788la propia `.so`, así que no hace falta variante compartida (su `libffi` aparece en
789`Requires.private`, que pkg-config ignora fuera del modo estático).
790
791**Pero la decisión sigue teniendo costo y no es técnica:** encender `gvfs` mete una SEGUNDA glib en
792la imagen —justo lo que la campaña GNOME midió y evitó— y re-hashea `cosmic-files`, que arrastra a
793`cosmic-term` y `cosmic-edit` porque lo usan como crate.
794
795## 🚪 LA CADENA DEL PORTAL, CERRADA (2026-08-05)
796
797`docs/evidencia/cosmic-portal-captura-2026-08-05.png` — `cosmic-screenshot` corrido desde
798`cosmic-term` dentro de la VM devuelve una ruta en vez de un panic, y el fichero existe:
799
800```
801/root/Pictures/Screenshots/Screenshot_2026-08-05_10-18-14.png
802-rw-r--r-- 1 root root 42256 Aug  5 10:18 Screenshot_2026-08-05_10-18-14.png
8030000000 211   P   N   G  \r  \n 032  \n          ← la firma PNG, no un fichero vacío
804```
805
806**El veredicto es el fichero, no el código de salida**: que el binario no panickee sólo prueba que
807encontró el servicio; que haya 42 KB con la firma `\x89PNG` prueba que el compositor entregó píxeles
808por la cadena entera.
809
810Cinco recetas nuevas, en el orden en que hicieron falta:
811
812| receta | hash | por qué |
813|---|---|---|
814| `fuse3` 3.18.2 | `b3:3a84af86` | dep dura del frontend (document portal) |
815| `json-glib` 1.10.8 | `b3:dc8c8e78` | dep dura; variante SIN introspección (ver abajo) |
816| `xdg-desktop-portal` 1.18.4 | `b3:f4adb8c9` | **el frontend**: registra `org.freedesktop.portal.Desktop` |
817| `clang18` | `b3:62c6bfb5` | sólo `libclang.so`, para el bindgen de `libspa-sys` |
818| `xdg-desktop-portal-cosmic` | `b3:7ad0f124` | **el backend**: las 5 interfaces `impl.portal.*` |
819
820Son **dos raíces** del perfil y de la hidratación porque son **dos procesos** que se encuentran por
821D-Bus en runtime: ningún `[deps]` los relaciona, igual que los componentes de la sesión.
822
823### Lo que costó cada decisión
824
825**Pin a 1.18.4 por gstreamer.** Desde 1.20 el meson del frontend pide `gstreamer-pbutils-1.0` sin
826`required:` —valida sonidos de notificación— y eso arrastra gstreamer + gst-plugins-base. El día que
827se quiera 1.22, ése es el precio.
828
829**El choque de `gvdb`.** El portal vendoriza gvdb y glib también lo lleva adentro (GSettings): con
830glib estática el `.a` expone los símbolos y hay doble definición. Los dos caminos obvios se
831descartaron POR MEDICIÓN: `--allow-multiple-definition` **zig-cc no lo acepta** (`unsupported linker
832arg`, que meson reporta como `Compiler cannot compile programs` y manda a buscar donde no está), y
833`glib-shared` habría abierto la cadena entera (json-glib y gdk-pixbuf también enlazan glib). Se
834resolvió renombrando los diez símbolos del lector con `-D`: el renombrado es total porque todos los
835usos pasan por `gvdb-reader.h`, y el binario queda con lector+constructor de la MISMA copia — que
836importa, porque glib empaqueta sólo el lector.
837
838**`libclang` no salió de COSMIC.** El backend declara `pipewire`/`libspa-sys` como deps duras y
839`libspa-sys/build.rs` corre `bindgen` incondicional, sin bindings pregenerados de reserva (a
840diferencia de `aws-lc-sys`, que sí cae a `cargo:rustc-cfg=universal` y por eso cruzó a musl sin cmake
841ni perl). `clang18` NO recompila LLVM: el artefacto `llvm18` ya publica headers y `lib/cmake/llvm`.
842Instala sólo `libclang.so` y `clang-c/` — el compilador de esta distro sigue siendo zig-cc.
843
844**El parche de una línea:** el backend fija `cosmic-files` con `features = ["gvfs", "wayland"]` a
845mano. El `sed` saca `gvfs` y queda la misma combinación que ya compilaron term y edit. Se pierden los
846montajes remotos dentro del selector del portal, no el selector.
847
848### ⚠ Gotchas de andamiaje que costaron tiempo real
849
850**`--offline` SIN `--locked`** cuando la receta parchea el manifiesto: el `sed` corre en la fase
851*compile*, o sea DESPUÉS del `cargo vendor`, así que cargo ve el `Cargo.toml` cambiado y con
852`--locked` se niega. Quitar una feature sólo ENCOGE el conjunto de crates; la hermeticidad la da
853`--offline` + el árbol vendorizado.
854
855**El worker NO puede construir la cola COSMIC.** El snapshot golden es del 2026-07-15 y toda esta
856cola es posterior, así que allá nada es cache-hit: intentó reconstruir `pipewire` y murió buscando
857glib. Es la regla de «rootfs laptop ≠ worker» pero con el **STORE**. Corolario: mandar a la granja
858sólo lo que no dependa de artefactos recientes — `clang18` sí (sus deps son viejas), el backend no.
859
860**`farm-down.sh` cosecha `/opt/hammer/store`, pero `farm-up.sh` ancla el store del worker en
861`/mnt/cosecha/store`.** Son directorios distintos, NO el mismo por symlink: una cosecha normal no
862trae lo que el worker construyó ahí. Bajar el artefacto a mano ANTES de destruir nada.
863
864**Y no pedir un `farm-down` completo por un artefacto de 57 MB**: el pull de los 616 artefactos del
865store horneado dejó 67 G en `store/.dmerge` y llenó `/home` al 100 %. (La protección sí funcionó: al
866fallar el pull, no destruyó el worker.)
867
868**`pipewire` no lleva `--wrap-mode=nodownload`** ⇒ ante una dep faltante intenta bajarse un
869subproyecto de internet y, sin red, el error no dice «te falta glib» sino `Unhandled python
870exception / This is a Meson bug`. **Deuda anotada.**
871
872### Las cinco interfaces, sondeadas por D-Bus
873
874`docs/evidencia/cosmic-portal-interfaces-2026-08-05.png`. Del lado del CLIENTE, contra
875`org.freedesktop.portal.Desktop`:
876
877| interfaz | versión |
878|---|---|
879| `Screenshot` | 2 |
880| `FileChooser` | 3 |
881| `ScreenCast` | 5 |
882| `Settings` | 2 |
883| `Access` | **no existe, y está bien** |
884
885**`org.freedesktop.portal.Access` no es una interfaz de cliente**: `Access` vive sólo como
886`org.freedesktop.impl.portal.Access`, o sea del lado del BACKEND — es como el frontend le pide al
887escritorio que muestre el diálogo de permiso. Pedirla al frontend devuelve `No such interface`, que
888es el comportamiento correcto y no un hueco. (El sondeo inicial la pidió mal; el error era del
889sondeo, no del portal.)
890
891Del lado del BACKEND, contra `org.freedesktop.impl.portal.desktop.cosmic`: `Screenshot` 2,
892`ScreenCast` 4, `Settings` 2, y `Access` + `FileChooser` responden
893`UnknownProperty 'version'`. **Ese error prueba que la interfaz ESTÁ**: si faltara, D-Bus contestaría
894`No such interface`; `UnknownProperty` significa que la encontró y que esa implementación no publica
895la propiedad `version`.
896
897Y el `ListNames` del bus muestra `org.freedesktop.impl.portal.desktop.cosmic` **activado** junto a
898`org.freedesktop.impl.portal.PermissionStore`: el frontend levantó al backend por D-Bus activation,
899que es justamente el eslabón que el `.service` con `@libexecdir@` sustituido hace posible.
900
901⚠ **Alcance honesto de esta medición**: prueba DISPONIBILIDAD y ENRUTADO, no los flujos completos.
902La única interfaz ejercida de punta a punta —con fichero en disco— es `Screenshot`.
903
904### 📂 FileChooser EJERCIDO — y por qué NO se puede desde cosmic-store
905
906`docs/evidencia/cosmic-portal-filechooser-2026-08-05.png` y `…-nav-…png`.
907
908**Primero, el camino que NO existe.** `cosmic-store` se compila con la feature `xdg-portal`, pero su
909ÚNICO uso del selector está en `src/update.rs:505`, dentro de
910`Message::RepositoryAddDialog`, y arranca con:
911
912```rust
913if backend_name == BackendName::FlatpakUser {
914    #[cfg(feature = "xdg-portal")]
915    ...file_chooser::open::Dialog::new()...
916```
917
918O sea que el diálogo sólo se abre para **añadir un repositorio flatpak**, y esta imagen no construye
919el backend flatpak (ver arriba: sólo `pkgar`). La feature está compilada y el punto de llamada es
920inalcanzable. **Tener la feature encendida no es tener el flujo disponible.**
921
922**El camino que sí ejerce la interfaz** es llamarla directo, que además prueba los tres eslabones sin
923depender de qué aplicación la use:
924
925```sh
926dbus-send --session --print-reply --dest=org.freedesktop.portal.Desktop \
927  /org/freedesktop/portal/desktop org.freedesktop.portal.FileChooser.OpenFile \
928  string:"" string:Prueba dict:string:variant:handle_token,string:hammer1 &
929```
930
931Resultado medido: el frontend devuelve el objeto Request con el token que le pasamos
932(`/org/freedesktop/portal/desktop/request/1_113/hammer3`) y el **backend dibuja el diálogo real** —
933título `Prueba`, el FS de verdad (`run`, `tmp`, `dev`, `proc`, `sys`), panel de detalles
934(`inode/directory`, 963 items) y botones Cancel/Open. La navegación anda: doble clic en `dev` cambia
935la miga de pan a `Filesystem › dev` y lista sus 129 entradas; seleccionar y pulsar *Open* lo cierra.
936
937⚠ **Lo que NO quedó capturado, y por qué**: la URI devuelta. El `Response` de la especificación de
938portales es una señal **unicast al llamador**, y `dbus-send` termina apenas recibe el *method return*,
939así que ya no está para recibirla; `dbus-monitor` con un match rule normal tampoco la ve. Capturarla
940pide un cliente que implemente el ciclo Request/Response — que es justo lo que hace `ashpd` dentro de
941las aplicaciones. **El diálogo está probado; la entrega del fichero a la aplicación, no.**
942
943### 🎥 ScreenCast: dos bloqueos distintos, y el segundo es el que importa
944
945**Bloqueo 1 — `dbus-send` no puede manejarlo, al revés que FileChooser.** El selector se abre con
946UNA llamada; ScreenCast necesita un apretón de manos de tres pasos con estado (`CreateSession` →
947`SelectSources` → `Start`), y la sesión queda atada a la **conexión** que la creó. Cada `dbus-send`
948es una conexión distinta que muere al terminar, así que el paso 2 nunca encuentra la sesión del paso
9491. No es un límite del portal sino de la herramienta: hace falta un cliente `ashpd` de verdad.
950
951Lo que sí se midió con `dbus-send`: `CreateSession` **funciona** — el frontend acepta y devuelve
952`/org/freedesktop/portal/desktop/request/1_106/sc1`, con el `handle_token` que le pasamos.
953
954**Bloqueo 2 — NADIE ARRANCA EL DEMONIO DE PIPEWIRE**, y éste no lo arregla ningún cliente.
955`docs/evidencia/cosmic-screencast-sin-pipewire-2026-08-05.png`:
956
957```
958$ ls /usr/bin/ | grep -i pipewire      →  pipewire, pipewire-aes67, pipewire-avb, pipewire-pulse
959$ ps ax | grep -c pipewire             →  1     (el propio grep: NO hay demonio)
960```
961
962Los binarios están en la imagen —el backend del portal enlaza `libpipewire-0.3.so`— pero el
963**demonio** no corre: `cosmic-start` no lo lanza y no hay systemd de usuario que lo active. O sea que
964aunque se escribiera el cliente `ashpd`, `Start` no tendría con quién negociar el stream.
965
966Arreglo: lanzarlo desde `cosmic-start`. (Para AUDIO además falta `wireplumber`, que ni siquiera está
967construido — ver la decisión de audio del proyecto.)
968
969### ✅ HECHO: el demonio arranca y el portal se conecta a él (2026-08-05)
970
971`cosmic-start` lanza `/usr/bin/pipewire` justo después de asegurar `XDG_RUNTIME_DIR`, y espera **el
972socket, no el pid** — la misma disciplina que el compositor, y por el mismo motivo: un demonio que
973muere al instante deja el pid «existiendo» un rato y esperar por él da un OK falso.
974
975Va en ese punto del script a propósito: pipewire **no necesita D-Bus** para arrancar, sólo el
976runtime dir donde abre su socket. Ponerlo ahí lo hace válido para los dos modos —`bare` y
977`session`— sin duplicar el bloque.
978
979Lo medido dentro de la VM, en este orden, porque cada paso responde una pregunta distinta:
980
981```
982== cosmic-qemu :: pipewire OK (socket pipewire-0, pid 189)   ← ¿abre socket?
983
984$ pw-cli info 0
985  type: PipeWire:Interface:Core/4      version: "1.2.7"      ← ¿SIRVE a un cliente?
986  core.daemon = "true"    core.name = "pipewire-0"
987
988$ pw-cli ls Factory | grep factory.name
989  … "client-node" …                                          ← ¿tiene lo que ScreenCast usa?
990
991$ pw-cli ls Client | grep application.name
992  application.name = "pw-cli"
993  application.name = "xdg-desktop-portal"                     ← ¡EL PORTAL YA ESTÁ CONECTADO!
994```
995
996La última línea es el veredicto. **El frontend del portal aparece como cliente del demonio de
997pipewire** — una conexión que antes no podía existir, y que es exactamente la que `Start` necesita
998para negociar el nodo del stream. `client-node` es la factory con la que el backend lo crea.
999
1000**Y sin regresión, medido con la cadena entera**: `cosmic-screenshot` desde `cosmic-term` levanta la
1001**UI interactiva del portal** —un toolbar con los tres modos (región, ventana, pantalla), `Capture` y
1002`Save to Pictures`—, y el click en `Capture` deja un PNG de 53.590 bytes en
1003`/root/Pictures/Screenshots/`. Evidencia: `docs/evidencia/cosmic-portal-ui-captura-2026-08-05.png`.
1004Esa UI no estaba documentada: el registro anterior sólo probaba la ruta no-interactiva.
1005
1006El escritorio completo con pipewire corriendo:
1007`docs/evidencia/cosmic-escritorio-con-pipewire-2026-08-05.png` (2414 colores distintos — el panel
1008mutilado da 130).
1009
1010**Lo que sigue faltando, y son dos cosas separadas:**
1011
1012- **El handshake de ScreenCast de punta a punta** necesita un cliente **persistente** — resuelto
1013  abajo con `portal-probe`.
1014- **`wireplumber`**, que es lo que separa «hay stream de video» de «hay audio». Sin gestor de sesión
1015  pipewire arranca y sirve, pero no descubre tarjetas ni enruta. Para ScreenCast alcanza; para sonido
1016  no. Ni siquiera está construido.
1017
1018## 🔌 portal-probe: la SONDA, y hasta dónde llega ScreenCast de verdad (2026-08-05)
1019
1020`recipes/incoming-cosmic/portal-probe.toml` → **`b3:af49d32d`**. La única receta de la campaña cuyo
1021producto no es una pieza del escritorio sino **un instrumento para medirlo**.
1022
1023### Por qué no alcanzaba `dbus-send`, y por qué tampoco hacía falta `ashpd`
1024
1025Los portales no son RPC: son **asíncronos en dos tiempos**. Cada método devuelve al instante un
1026object path de `Request`, y el resultado llega DESPUÉS como señal `Request.Response` sobre ese path.
1027`dbus-send` ya salió para entonces. Y hay un segundo muro, más duro: **el portal ata la sesión al
1028SENDER**, así que `CreateSession` con un `dbus-send` y `SelectSources` con otro se rechazan por venir
1029de nombres únicos distintos. Hace falta UNA conexión viva durante todo el intercambio.
1030
1031`ashpd` era el camino obvio, pero el árbol fuente de una receta de código propio es el repo del
1032**propio takana** (no hay modo `path` en `[source]`: sólo git y tarball) ⇒ habría sumado ~150 crates
1033al `Cargo.lock` de la herramienta de build por una sonda de diagnóstico. En C sobre `libdbus` cuesta
1034**cero deps nuevas**, sale **estática (0 NEEDED)** —un instrumento no debe depender de lo que mide— y
1035sobre todo: **el protocolo se ve**. Una sonda cuyo valor es documentar un handshake no debería
1036esconderlo tras una biblioteca que lo abstrae.
1037
1038Detalle que la especificación regala y hay que usar: **el path del `Request` se PREDICE**
1039(`/…/request/<sender>/<token>`, con `sender` = nombre único sin `:` y con `.`→`_`). Existe justo para
1040poder suscribirse ANTES de llamar; esperar al path devuelto para recién ahí hacer `AddMatch` es una
1041carrera real, no teórica.
1042
1043### Lo medido: el stream SE CREA, y el `Response` es lo único que falta
1044
1045```
1046portal-probe version        ScreenCast v5 · Screenshot v2 · FileChooser v3 · Settings v2
1047                            Access y RemoteDesktop AUSENTES
1048portal-probe screencast
1049  [1/4] CreateSession   → Response 0, session_handle = /…/session/1_108/hammersess   ✓
1050  [2/4] SelectSources   → Response 0                                                 ✓
1051  [3/4] Start           → el backend ABRE su diálogo «Share your screen», con
1052                          miniatura EN VIVO del framebuffer y el output `Virtual-1`;
1053                          se elige, se pulsa Share… y NO llega Response en 60s        ✗
1054```
1055
1056El log del backend (`RUST_LOG=debug`) da la línea exacta:
1057
1058```
1059xdg_desktop_portal_cosmic::screencast_thread: state-changed 'Connecting' -> 'Paused'
1060```
1061
1062Y `pw-cli ls Node` durante la espera da el veredicto independiente:
1063
1064```
1065node.name   = "cosmic-screencast"
1066media.class = "Video/Source"
1067```
1068
1069**El nodo de video EXISTE en el grafo de pipewire.** O sea: la cadena entera —cliente → frontend →
1070backend → compositor → demonio de pipewire— funciona hasta **crear y negociar el stream**. Lo único
1071que no ocurre es que el backend emita el `Response` de vuelta al cliente tras quedar en `Paused`.
1072
1073Ese es un lugar muy distinto del que ocupaba el frente esta mañana («no hay demonio con quien
1074negociar»). El siguiente paso es el `Paused → Streaming`, y la sospecha razonable es
1075**`wireplumber`**: sin gestor de sesión nadie mueve el nodo de `Paused`, y el backend parece
1076esperarlo. Es una hipótesis, no una medición — construir `wireplumber` la confirma o la mata.
1077
1078### Dos gotchas que costaron corridas
1079
1080- **`NameHasOwner` no activa nada.** Los portales son servicios ACTIVABLES: el bus los arranca al
1081  pedirlos. La primera versión de la sonda rechazaba con «no hay frontend de portal en el bus» sobre
1082  un sistema donde el portal andaba perfecto, sólo que dormido. Se arregló con `StartServiceByName`
1083  (commit `ea210ba`) — y distingue «arrancado ahora» de «ya estaba corriendo», que no es lo mismo
1084  cuando lo que se investiga es una carrera.
1085- **Matar el backend se lleva puesto al frontend.** `kill` sobre `xdg-desktop-portal-cosmic` dejó
1086  también sin dueño a `org.freedesktop.portal.Desktop`. Para diagnosticar con log hay que relanzar el
1087  backend Y forzar la reactivación del frontend.
1088- **El lanzador busca por NOMBRE VISIBLE, no por binario**: `cosmic-term` no matchea nada,
1089  `Terminal` abre COSMIC Terminal. Escribir el nombre del ejecutable abre otra cosa (settings).
1090- Y uno de shell, al automatizar por QMP: **`| head -N` bufferiza** y deja la terminal en blanco hasta
1091  que el programa termina. Parece que la sonda colgó cuando en realidad está esperando.
1092
1093## 🔇 wireplumber: construido, corriendo… y NO era el bloqueo (2026-08-05)
1094
1095`recipes/incoming-cosmic/wireplumber.toml` 0.5.15 → **`b3:b8f3baf0`**, selló a la primera. Arranca
1096desde `cosmic-start` (`wireplumber OK (pid 192) — hay gestor de sesión`), se conecta al grafo (dos
1097clientes en `wpctl status`) y `wpctl` pasa a mostrar una sección **Video** que antes no existía.
1098
1099**Y el `Start` de ScreenCast sigue sin `Response` a los 60s. Exactamente el mismo resultado.**
1100
1101La hipótesis de la sección anterior —«falta el gestor de sesión que mueva el nodo de `Paused` a
1102`Streaming`»— **queda MATADA por medición**. Era mía y era razonable; no era cierta. Se deja escrita
1103junto con su refutación porque una hipótesis descartada con evidencia vale más que una lista de
1104sospechosos: acota el próximo paso a lo que queda, que es el propio backend.
1105
1106El dato nuevo, que es por dónde seguir: con wireplumber corriendo, `wpctl status` lista
1107`Video → Streams` **vacío** durante el handshake. O sea que el nodo `cosmic-screencast` que sí existe
1108en `pw-cli ls Node` **no llega a wireplumber como stream gestionado**. El próximo movimiento es el log
1109del backend con `RUST_LOG=trace` alrededor de `screencast_thread`, no otra dep.
1110
1111## 🧊 LA CAUSA REAL DE ScreenCast: EGL POR SOFTWARE, no el portal (2026-08-05)
1112
1113Con `RUST_LOG=xdg_desktop_portal_cosmic=trace` el log del backend tiene **5 líneas en total** y la
1114última es la de siempre:
1115
1116```
1117INFO xdg_desktop_portal_cosmic::screencast_thread: state-changed 'Connecting' -> 'Paused'
1118```
1119
1120**Después del `Paused` el backend no escribe NADA MÁS** — ni error, ni panic, ni progreso. O sea que
1121el trace no aportó: `screencast_thread` no tiene más instrumentación. Buscar ahí estaba agotado.
1122
1123La pieza que faltaba mirar era **el compositor**, que es quien tiene que entregar los frames.
1124`/tmp/cosmic-session.log`:
1125
1126```
112715:19:02.301568Z WARN smithay::backend::egl::error: Erroneous EGL call didn't set EGLError
112815:19:02.310127Z INFO …screencast_thread: state-changed 'Connecting' -> 'Paused'
1129                 ↑ 8 MILISEGUNDOS de diferencia
1130```
1131
1132**Y la correlación no es casual**: el contador de `Erroneous EGL` sube **de a dos por cada intento de
1133screencast** (uno al abrir el diálogo con su miniatura en vivo, otro al pulsar Share) y no se mueve
1134en ningún otro momento. Seis acumulados tras tres intentos.
1135
1136`ls /usr/lib/dri/` da `iris_dri.so`, `kms_swrast_dri.so`, `swrast_dri.so`. En QEMU con
1137`virtio-gpu-pci` (sin virgl) el que carga es **`kms_swrast`**: mesa por software, **sin exportación
1138dmabuf real**. Y eso es exactamente lo que un stream de vídeo continuo necesita y una captura de una
1139sola toma no — por eso `cosmic-screenshot` SÍ funciona (va por shm) y ScreenCast no.
1140
1141### El frente no está bloqueado por una receta, está bloqueado por el ENTORNO
1142
1143Esto reencuadra todo el día: **pipewire no era, wireplumber no era, el backend del portal no era**.
1144Las tres piezas están construidas, corriendo y haciendo su trabajo —el nodo `cosmic-screencast`
1145existe en el grafo—, y el que no puede cumplir es el compositor sobre una GPU de software.
1146
1147Es el MISMO muro que documenta el frente KDE («GBM/EGL de software; mesa es softpipe, falta llvmpipe
1148o virgl»), llegando por otro camino.
1149
1150**No se puede verificar en este QEMU**: `qemu-system-x86_64 -display help` da sólo `none`, `gtk` y
1151`sdl`, y no existe `virtio-gpu-gl-pci` — este binario no se compiló con virgl, aunque
1152`libvirglrenderer.so.1` esté instalada en el host. Las dos salidas reales son:
1153
11541. **Metal**, donde `iris_dri.so` (que YA está en la imagen) habla con la Intel de verdad. Es el
1155   viaje de [[metal-dual-desktop-en-curso]] y probaría ScreenCast de paso.
11562. **Un QEMU con virgl** (`-device virtio-gpu-gl-pci -display egl-headless`), que hoy hay que
1157   construir o instalar aparte.
1158
1159Hasta entonces, lo honesto es que ScreenCast está **verificado hasta donde el entorno permite**: la
1160cadena completa de D-Bus funciona, el stream se crea y se negocia, y el último tramo —los píxeles—
1161depende de una GPU que esta VM no tiene.
1162
1163### Por qué es una receta propia y no la de GNOME
1164
1165La de `incoming-gnome` está sellada y funciona, pero su binario NEEDea `libglib-2.0.so.0`,
1166`libgobject-2.0.so.0` y `libpipewire-0.3.so.0` **de la isla dinámica de GNOME**: traerla tal cual
1167metería una segunda glib y una segunda pipewire en la imagen. De las 19 deps, **14 dan hash idéntico**
1168y las que divergen divergen a propósito — sobre todo `pipewire`, que acá apunta a la de COSMIC
1169(`338d1d8c`), la que el escritorio realmente arranca. Un gestor de sesión contra otra pipewire que la
1170que corre no gestiona nada.
1171
1172**El cambio de fondo es `glib` → `glib-shared`.** COSMIC usa la glib ESTÁTICA del corpus, que le
1173alcanza a `xdg-desktop-portal` porque es un ejecutable. Acá no: wireplumber produce
1174`libwireplumber-0.5.so` y 17 módulos, y `libglib-2.0.a` del corpus tiene **44.275 reubicaciones
1175`R_X86_64_32/32S`** ⇒ no es PIC y no entra en un `.so`. (Regla del frente GNOME: `readelf -r` ANTES de
1176gastar el build — acá ahorró uno.)
1177
1178La salida no fue una campaña nueva sino una **medición**: `incoming-kde/glib-shared.toml` copiada a
1179esta cola resuelve al **mismo hash** (`0584ce1f`) y **ya estaba sellada** ⇒ cero rebuild. Idem
1180`pcre2-shared` (`f254abaa`), `lua` (`e6c35f98`) y `libelogind` (`f1a690b2`). Las dos glib **coexisten
1181sin pisarse** en el rootfs: `.a` y `.so` son ficheros distintos, y el portal conserva sus 3 NEEDED.
1182
1183Cierre: `escritorio-cosmic` pasa de 84 a **89 recetas**, 0 faltantes.
1184
1185### El `-L/usr/lib` que pkg-config no da
1186
1187`pkg-config` **omite los `-L` de directorios que considera estándar del sistema**, asumiendo que el
1188linker ya los busca. Cierto para un `cc` nativo; **falso para `zig cc` cruzando a
1189`x86_64-linux-musl`**, que trae sus propias rutas. Resultado: `--libs --static` devuelve `-ldbus-1`
1190pelado y zig responde `unable to find static system library 'dbus-1' … searched paths: none` — un
1191error que suena a «falta la librería» cuando lo que falta es la ruta.
1192
1193**Y de paso se corrigió un comentario desactualizado de `cosmic-start`** que decía que
1194`cosmic-settings-daemon` «todavía no se puede construir acá (arrastra pipewire)». Es falso hace
1195tiempo: ese daemon está sellado y corre. Un comentario viejo manda a buscar un bloqueo que ya no
1196existe, y eso cuesta igual que un bug.
1197
1198## 🔩 EL VIAJE A METAL: qué hacer con el USB en la mano (2026-08-05)
1199
1200`scripts/cosmic/metal-desktop-image.sh` → `work/hammer-cosmic-metal.img`, quemada a `/dev/sda`.
1201**No es «lo mismo pero en metal»**: es la única forma de saber si ScreenCast está bien, porque el
1202`mesa` del corpus se construye con `-Dgallium-drivers=iris` y en QEMU ese driver nunca se usaba.
1203
1204**Validado en OVMF ANTES de quemar, y con PANTALLA** —la regla de oro que costó un USB en el primer
1205viaje de KDE—, con `-serial null` para simular un metal sin puerto serie: `arje-zero` PID 1, las
1206cuatro particiones montadas, el motd VISIBLE y el prompt `/ #` en pantalla (o sea que el getty de
1207`tty1` hace su trabajo). El shell responde: `iris_dri.so` presente, `/dev/dri/card0`, y
1208`cosmic-start` / `portal-probe` / `wireplumber` en PATH.
1209Evidencia: `docs/evidencia/cosmic-metal-boot-pantalla-2026-08-05.png`.
1210
1211### La secuencia en la máquina física, en orden
1212
1213```sh
1214ls /dev/dri                       # card0 + renderD128 ⇐ renderD128 es la novedad: NO existe con swrast
1215dmesg | grep -iE 'i915|drm'       # que i915 haya hecho modeset, no simpledrm
1216cosmic-start                      # el MISMO script que corre en QEMU
1217# …y desde cosmic-term, dentro de la sesión:
1218portal-probe version              # ScreenCast v5
1219portal-probe screencast           # ⇐ LO QUE VINIMOS A VER
1220```
1221
1222**El veredicto es el `[4/4] OpenPipeWireRemote` con su fd**, no que el diálogo aparezca: eso ya
1223pasaba en QEMU. Si `Start` devuelve `Response: 0` y la sonda imprime `✓✓ HAY REMOTO DE PIPEWIRE`, el
1224frente cierra. Si vuelve a colgarse en `Start`, mirar `/tmp/cosmic-session.log`: si **ya no** aparece
1225`Erroneous EGL call`, entonces la causa era otra y el diagnóstico de hoy estaba incompleto.
1226
1227### 🩺 1er viaje físico: se congelaba en el USB, y NO estaba colgado (2026-08-05)
1228
1229Booteando en **otra máquina**, la pantalla llegaba hasta esto y se quedaba ahí:
1230
1231```
1232GPT: Use Parted to correct GPT errors      ← inofensivo: el backup GPT no está al final del USB
1233sdc: sdc1 sdc2 sdc3 sdc4
1234sd 6:0:0:0: [sdc] Attached SCSI removable disk
1235```
1236
1237**La prueba de qué pasaba es una AUSENCIA**: con `ignore_loglevel` en el cmdline, montar una ext4
1238imprime `EXT4-fs (sdc2): mounted filesystem` en pantalla sí o sí. No aparecía ⇒ el root nunca se
1239montó ⇒ el fallo estaba en el initramfs de pivote, no en el kernel ni en el escritorio.
1240
1241Dos causas encadenadas, y la segunda es la que hizo perder el viaje:
1242
12431. **CARRERA contra la enumeración USB.** El `rdinit` arranca apenas se desempaqueta el initramfs,
1244   pero el bus USB enumera **asíncrono** y tarda segundos (reset de hub, *settling* de 1 s por
1245   dispositivo, scan SCSI). El pivote reintentaba `findfs LABEL=takana-root` 5 veces con 1 s. En esa
1246   máquina el disco es `sd 6:0:0:0` —séptimo host SCSI— y no llegó a tiempo. **En QEMU el disco es
1247   virtio y está desde el instante cero, así que esta carrera NO SE PUEDE VER en validación**: hay
1248   que arrancar la imagen como `usb-storage` para reproducirla. Es el `rootwait` que no podemos usar,
1249   porque el descubrimiento es por LABEL y no hay `root=` en el cmdline. Ahora espera **60 s**.
1250
12512. **CEGUERA.** El cmdline horneado es `console=tty0 console=ttyS0,115200 …` y el kernel hace
1252   `/dev/console` = la **última** `console=` ⇒ el serial. El `exec sh` de rescate del pivote nacía en
1253   un puerto serie que esa máquina no tiene: pantalla muerta, indistinguible de un kernel colgado.
1254   **Es el MISMO bug que costó el 1er viaje de KDE, una capa más temprano**: allá se arregló para
1255   *después* del `switch_root` (el `tty1-getty`) y se dio el caso por cerrado, pero el initramfs
1256   seguía ciego. Ahora el log del pivote y el marcador `HAMMER-EFI-INIT-OK` van también a
1257   **`/dev/tty0`**, y el rescate abre un shell en **tty1** listando los bloques visibles.
1258
1259**La lección general** (vale para toda imagen booteable, no sólo COSMIC): *cada* capa del arranque
1260tiene que escribir a `/dev/tty0`, no al `console` heredado. Arreglar una sola capa deja el agujero
1261tapado y la siguiente vuelve a caer.
1262
1263Reproducir el escenario de metal sin salir del laptop:
1264
1265```sh
1266qemu-system-x86_64 -m 4096 -enable-kvm -cpu host -display none -vga std -serial null \
1267  -drive if=pflash,unit=0,format=raw,readonly=on,file=/usr/share/edk2/x64/OVMF_CODE.4m.fd \
1268  -drive if=pflash,unit=1,format=raw,file=work/.efi-vars-usbtest.fd \
1269  -drive if=none,id=usbstick,format=raw,file=work/hammer-cosmic-metal.img \
1270  -device qemu-xhci,id=xhci -device usb-storage,bus=xhci.0,drive=usbstick \
1271  -qmp unix:/tmp/qmp.sock,server,nowait     # el veredicto es el screendump, no el log
1272```
1273
1274De paso: el rebuild avisa `! sin takana musl` ⇒ **el CLI `takana` no está en la imagen** y
1275`takana boot menu` nunca corre. Igual lleva `timeout 15`, porque corre ANTES del `exec` de arje-zero
1276y colgarse ahí deja un arranque sin PID1 ni pantalla — el `|| true` protege del fallo, no del bloqueo.
1277
1278### 🖥 2º viaje: la imagen forzaba SOFTWARE — no podía ganar ni en el mejor caso (2026-08-05)
1279
1280Máquina: **Dell OptiPlex 3060, Coffee Lake, UHD 630**. Arrancó entero (arje-zero PID 1, hammerd,
1281sshd escuchando) y `cosmic-start` no levantó el escritorio.
1282
1283**La GPU estaba perfecta.** Del `dmesg` sacado del propio USB:
1284
1285```
1286[2.570] i915 0000:00:02.0: [drm] Found coffeelake (device ID 3e92) integrated display version 9.00
1287[3.179] [drm] Initialized i915 1.6.0 for 0000:00:02.0 on minor 0
1288[3.875] fbcon: i915drmfb (fb0) is primary device
1289[3.949] Console: switching to colour frame buffer device 240x67
1290```
1291
1292El `Failed to load DMC firmware i915/kbl_dmc_ver1_04.bin` es un *warning*: el propio kernel dice que
1293sólo desactiva gestión de energía. No era la causa (se arregló igual, es gratis).
1294
1295**La causa era el script de arranque.** `cosmic-start-qemu.sh` exportaba, sin condición:
1296
1297```sh
1298export LIBGL_ALWAYS_SOFTWARE=1
1299export MESA_LOADER_DRIVER_OVERRIDE=kms_swrast
1300```
1301
1302Correcto sobre virtio-gpu. Letal sobre Intel real: la mesa del corpus va con
1303`-Dgallium-drivers=iris -Dllvm=disabled` y **no existe ningún `kms_swrast_dri.so`** ⇒ el override no
1304degrada a software, hace fallar EGL. **Y en el mejor caso habría sido peor**: si hubiera arrancado,
1305habría compuesto por software y ScreenCast habría fallado por el mismo motivo que en QEMU — o sea que
1306**el viaje no podía contestar la pregunta que lo motivaba**.
1307
1308El origen fue un comentario en `metal-desktop-image.sh` que afirmaba que el script *«no tiene nada
1309específico del emulador»*. El nombre del fichero decía lo contrario y se ignoró.
1310**Regla: un artefacto que se comparte entre QEMU y metal no puede llevar el entorno hardcodeado —
1311se detecta.** Hoy el modo de GL se decide con dos patas (driver DRM del kernel **y** `.so` de mesa
1312presente) y la imagen falla el build si el script vuelve a forzar `kms_swrast`.
1313
1314**Y correr `cosmic-start` sobre la imagen de metal por PRIMERA VEZ destapó lo de fondo**: le faltaba
1315entera la preparación de sesión que sólo tenía `qemu-desktop-image.sh`. Sin grupo `video` no arranca
1316`seatd`; sin usuario `messagebus` no arranca el **bus de sistema**; y **sin bus de sistema no hay
1317portal**, o sea que `portal-probe screencast` no habría tenido con quién hablar ni con el GL perfecto.
1318La imagen de metal se armó copiando el *arranque* de la de QEMU pero no su *preparación*.
1319
1320**Lección de método**: validar que una imagen «llega al prompt» no valida nada del escritorio. Hay que
1321correr `cosmic-start` en la imagen que se va a quemar, aunque el GL no se pueda ejercer en QEMU: los
1322fallos de sesión (seatd, dbus, logind) sí se ven, y son los que cuestan los viajes.
1323
1324### Qué mirar si la pantalla queda negra
1325
1326Es el fallo que ya nos pasó una vez y tiene una causa aburrida. En orden de probabilidad:
1327
13281. **No es un cuelgue, es el prompt invisible.** Ya está arreglado (`tty1-getty` + el log del pivote
1329   a `tty0`), pero si vuelve: los printk van a las dos consolas y el userspace a una sola.
13302. **`ls /usr/lib/dri`** — si sólo hay `iris_dri.so` y la GPU no es Intel, no hay GL. Esta imagen es
1331   Intel-only a propósito; para otra máquina va la imagen dual de KDE.
13323. **`cosmic-comp` sin DRM master**: `dmesg | grep -i drm` y `ls -l /dev/dri/card0`. Sin `seatd` o
1333   sin el shim de logind el compositor no toma el master.
1334
1335### Gotchas del quemado, medidos
1336
1337- **NUNCA a un NVMe**, y **nunca por nombre**: los nombres cambian entre arranques — hoy mismo,
1338  después del reboot, `nvme0n1` y `nvme1n1` se intercambiaron respecto de lo anotado en julio.
1339  Identificar por `lsblk -dpo NAME,SIZE,TYPE,TRAN,MODEL` y confirmar `TRAN=usb`.
1340- **El progreso NO se mide con `kill -0`**: `sudo dd` corre como root, así que desde el usuario da
1341  EPERM y parece que terminó. Se mide con `awk '{print $7}' /sys/block/sda/stat` (sectores escritos).
1342- **No correr `os-prober` sobre el device que estás dd-eando**: monta la partición con `grub-probe`
1343  mientras dd la sobreescribe y el dd cae a KB/s.
1344- **`pkill -f <patrón>` mata su propio shell** si el patrón aparece en su cmdline (exit 144). Pasó
1345  otra vez hoy. Usar `pgrep -x` o un patrón que no se auto-matchee.
1346
1347## Lo que falta, en orden
1348
13491. **ScreenCast: en el USB, esperando el viaje físico.** pipewire ✅, cliente persistente ✅
1350   (`portal-probe`, `b3:af49d32d`), `wireplumber` ✅ (`b3:b8f3baf0`) — y **ninguno era el bloqueo**.
1351   La causa es **EGL por software en el compositor** (`kms_swrast`, sin dmabuf), medida por
1352   correlación de 8 ms entre el `Erroneous EGL call` de smithay y el `Paused` del backend. No es
1353   verificable en QEMU (sin virgl), así que **la imagen de metal con `iris` ya está construida,
1354   validada en OVMF y quemada** — ver la sección del viaje. Falta bootearla (acción física) y correr
1355   `portal-probe screencast`. Y `portal-probe filechooser`, que ya puede capturar la URI.
13562. **Decidir `gvfs`** (ver la corrección de arriba: las piezas ya están selladas, lo que falta es la
1357   decisión sobre la segunda glib). Destrabaría los montajes remotos de cosmic-files y su applet.
13583. **Enchufar la tienda a `.swm`**: servir `org.freedesktop.PackageKit` y encender la feature
1359   `packagekit`, que es Rust puro. Mismo patrón que `arje-logind-compat`.
13604. `cosmic-greeter` (hoy `cosmic-session` lo lanza y falla con ENOENT, sin consecuencias) y
1361   `cosmic-osd` con teclas de brillo/volumen reales.
13625. Metal: la imagen de QEMU y la de metal comparten todo menos el driver; el viaje físico es el
1363   mismo que el de `metal-dual-desktop-en-curso`.

cosmic-desktop.md

Runbook — COSMIC, el cuarto escritorio

Estado al 2026-08-03: LA SESIÓN COSMIC ARRANCA ENTERA. cosmic-session —el camino de producción, no el andamio— levanta la cadena completa sobre el kernel de takana y arje-zero como PID1:

cosmic-comp → cosmic-settings-daemon → cosmic-notifications → cosmic-panel
            → cosmic-osd → cosmic-bg

Los que todavía no existen (cosmic-app-library, cosmic-launcher, cosmic-workspaces, cosmic-greeter) sólo dejan una línea de error! y la sesión sigue viva — que es exactamente lo que la tabla de .expect de más abajo predijo, verificado en vez de supuesto.

sesión COSMIC en QEMU

Y el paso anterior: cosmic-comp arranca sobre el kernel de takana con arje-zero como PID1, toma el DRM, expone wayland-1 y presenta frames; y cosmic-bg se conecta, entrega su buffer y el fondo aparece con el color exacto que se le configuró — (0.13,0.29,0.53) → (33,74,135). O sea que la cadena cliente→compositor→KMS funciona de punta a punta. Este documento se escribe mientras se hace, no después.

cosmic-bg pintando en QEMU

Que el color sea el que se pidió y no uno cualquiera es lo que hace de esto una medición y no una impresión: descarta que se esté viendo un buffer sin inicializar o el borrado del compositor.

El paso anterior —el compositor solo, gris (39,41,42) y el cursor, 134 colores— quedó en cosmic-comp-qemu-2026-08-03.png, porque la diferencia entre las dos imágenes es exactamente la pregunta que el modo bare separa.

COSMIC es el escritorio de System76, en Rust sobre smithay (compositor) e iced/libcosmic (clientes). Es el cuarto de takana, tras mirada, KDE Plasma 6 y GNOME.

Por qué NO se parece a los dos anteriores

KDE y GNOME costaron lo que costaron por la misma razón: una torre de C con Qt o glib/GObject/introspección debajo, donde cada capa se paga entera antes de ver un píxel. COSMIC no tiene esa torre. La primera receta —cosmic-session— salió a la primera y sin una sola dep en C: ELF estático, cero NEEDED.

Lo que sí hereda es lo que ya está pago:

lo que COSMIC necesitade dónde salecosto
las deps en C de smithay (drm, gbm, seat, input, udev, xkb, pixman)mirada-compositor, que ya es un compositor smithay de este labcero — misma lista
libdisplay-info + hwdatacopiadas byte a byte de la cola GNOMEcero — mismo hash, artefacto ya sellado
bash, dbus-run-session, hicolor-icon-theme, mesa-llvmpipecorpuscero
el andamiaje de arranque en QEMU (run-qemu-desktop.sh, base rootfs, console-getty, screendump)campañas KDE/GNOMEcero

La deuda propia que sí trae, y que ninguna de las otras campañas tocó, es la del unwinder de la std de rustc cuando el enlace no es estático — la misma que ya está documentada para las recetas Cargo del corpus.

El pin es de la SUITE, no de cada paquete

COSMIC versiona todos sus repos en lockstep con el tag epoch-N. Comprobado repo por repo: los doce componentes están en epoch-1.5.0.

No mezclar epochs entre piezas. Comparten protocolos propios (cosmic-protocols) y el formato de configuración (cosmic-config); un desajuste ahí no da error de compilación, da un escritorio que arranca y no se entiende consigo mismo — que es mucho más caro de diagnosticar.

La cola

recipes/incoming-cosmic/, aislada (otro agente comparte el repo; nunca git add -A).

Las deps se resuelven hermano → padre: desde incoming-cosmic/ se ven las recetas de recipes/, pero no las de incoming-gnome/ ni incoming-kde/. Por eso libdisplay-info y hwdata entran copiadas.

Copiar es gratis sólo si TODA la clausura transitiva resuelve a lo mismo. El ArtifactHash no depende de la ruta de la receta, así que un fichero idéntico cuyas deps resuelven igual da el mismo hash ⇒ cache hit sobre el artefacto que ya existe. Se cumplió con libdisplay-info, hwdata y nasm, y se verificó con takana hash antes de copiar (b3:260f0519, b3:bdc36cf9, b3:33383070), no después. Dónde deja de cumplirse —y por qué importa— está en la sección de cosmic-settings-daemon, más abajo.

Qué levanta la sesión, y por lo tanto el orden de ataque

cosmic-session/src/main.rs es la lista autoritativa —el mismo método que cerró la capa JS de GNOME: leé el fichero donde el programa declara lo que arranca, no lo descubras de a uno:

cosmic-comp  →  cosmic-notifications · cosmic-panel · cosmic-app-library
             →  cosmic-launcher · cosmic-workspaces · cosmic-osd · cosmic-bg · cosmic-idle

Más cosmic-settings-daemon y xdg-desktop-portal-cosmic fuera de esa lista.

piezaestadohash
cosmic-session✅ selladob3:81f02351
cosmic-icons✅ sellado (671 SVGs)b3:94d06f77
dav1d 1.5.4✅ sellado (dep arrastrada, ver gotcha 8)b3:aab7a874
cosmic-comp✅ SELLADO — el gate (filtra ScreenReader, ver atajos)b3:e871eee4
cosmic-bg (dinámico, PINTA)✅ selladob3:b2cfc410
cosmic-idle (dinámico)✅ selladob3:e1806acc
pulseaudio · pipewire (cadena del settings-daemon)✅ selladosb3:e1ff4736 · b3:338d1d8c
cosmic-panel✅ selladob3:ac5e48c5
cosmic-osd✅ selladob3:f15cb9c1
cosmic-settings-daemon✅ sellado (filtra ScreenReader, ver atajos)b3:642919a5
cosmic-notifications✅ selladob3:3e2a5478
cosmic-launcher✅ selladob3:c17b4956
cosmic-app-library✅ selladob3:b2a163e7
cosmic-workspaces-epoch✅ sellado (necesitó mesa, ver gotcha 11)b3:a031249e
cosmic-applets (19 binarios en uno)✅ sellado (necesitó dbus-shared, ver gotcha 10)b3:3e53a325
pop-launcher 1.2.7 (otro repo, ver abajo)✅ sellado a la primerab3:64a5cbaa
gmp 6.3.0 · mpfr 4.2.2 (cadena de la calculadora)✅ sellados a la primerab3:d99a0a5d · b3:13982d20
libqalculate 5.12.0 (qalc)✅ sellado — la calculadora del lanzador ANDA (ver abajo)b3:edb8d14d
cosmic-term 1.5.0 — la 1ª APLICACIÓN✅ sellada a la primera (arrastra cosmic-files y wgpu)b3:d0ea1c1e
cosmic-files 1.5.0 — la 2ª aplicación✅ sellada, sin gvfs ni wgpu (ver abajo)b3:48d2b072
cosmic-settings 1.5.0 — la 3ª aplicación✅ sellada (necesitó el parche de la doble barra)b3:716dac53

La cola está COMPLETA: 33 recetas, escritorio-cosmic 71/71.

El mínimo viable de una sesión son cuatro: cosmic-comp + cosmic-settings-daemon + cosmic-notifications + cosmic-panel (los tres .expect de la tabla de arriba).

El orden no es el de la lista de arranque. cosmic-bg va antes que el panel a propósito: habla Wayland con smithay-client-toolkit y no toca libcosmic ni iced, mientras que panel, launcher, osd y workspaces son todos clientes de iced. Con el fondo sellado, «pinta el fondo pero no el panel» separa la capa de UI del transporte; sin él, las dos preguntas llegan juntas.

cosmic-settings-daemon: por qué queda al final y NO se resuelve copiando

Es el único con cadena propia. Su workspace incluye audio-server como dep de path no opcional, que arrastra cosmic-pipewire → pipewire-sys → libpipewire-0.3 por pkg-config. Y pipewire hoy vive sólo en las colas GNOME y KDE.

Acá el truco de copiar deja de ser gratis, y eso corrige lo que dice más arriba. La regla completa es: copiar una receta entre colas es gratis sólo si toda su clausura transitiva resuelve a lo mismo. Se cumplió con libdisplay-info, hwdata y nasm —sus deps están todas en el corpus—, y no se cumple con pipewire: de sus catorce deps, cinco viven sólo en colas ajenas (dbus-shared, alsa-lib, libsndfile, pulseaudio, zlib-shared) y —lo decisivo— glib resolvería distinto. Desde incoming-gnome, glib es la sombra de esa cola (b3:f6ccdf98); desde incoming-cosmic sería la del corpus (b3:93d2cad0). Son recetas distintas, no la misma en dos lugares.

O sea que copiar produciría un segundo pipewire construido contra otra glib, arrastrando su cadena entera. Y eso no es sólo caro: es exactamente el cuadro que la campaña GNOME ya midió y evitó —dos glib distintas con 370 rutas solapadas y dos registros de GType en un proceso—.

Antes de tocar nada se midió con yupana radio (la puerta única, nunca grep): pipewire tiene radio 1 en GNOME y 2 en KDE, pero glib del corpus tiene 21 dependientes transitivos y afecta la imagen escritorio-gnome.

Y después se midió el costo real, que resultó mucho menor que la estimación. Copiar pipewire a incoming-cosmic necesita cuatro recetas hermanas —alsa-lib, dbus-shared, libsndfile, pulseaudio—, no las seis contadas a ojo, y tres de ellas dan hash idéntico y ya están selladas. Sólo pulseaudio y pipewire hay que construir.

Eso refina la advertencia de arriba en el punto que importa: el peligro de las dos glib es mezclarlas en UNA IMAGEN, y la imagen COSMIC tiene una sola. Acá el costo es de builds, no de corrección. Lo que sigue vigente es no promover a ciegas entre colas.

Gotcha del copiado, que cuesta un minuto de confusión: [source] patches se resuelve relativo a la receta, así que copiar el .toml sin su .patch da «no pude leer patch» y el hash ni siquiera se calcula.

🚪 EL GATE PASADO: cosmic-comp compila (2026-08-03)

b3:b8abc52f, 51 min 39 s de cargo con CARGO_BUILD_JOBS=1 y lto = "fat". A la primera, salvo el --bin del gotcha 1. Sin un solo parche a la fuente.

Lo que dice el enlace es la parte que vale, porque es la medición y no la impresión:

NEEDED: libdisplay-info.so.2 · libgbm.so.1 · libseat.so.1 · libudev.so.1
        libinput.so.10 · libpixman-1.so.0 · libxkbcommon.so.0 · libc.so

Ocho, y las siete primeras son exactamente los backends de smithay que la receta declara. La octava es musl. No hay libgcc_s — o sea que el compositor no arrastra la deuda del unwinder que sí tienen varias recetas Cargo del corpus, porque acá el enlace es dinámico contra musl y no hay +crt-static que forzar. Y no aparece nada que no se haya declarado: la clausura de build y la de runtime coinciden, que es la propiedad que uno quiere y casi nunca se cumple gratis.

Que un compositor Wayland completo —DRM, GBM, EGL, libinput, seat, Vulkan, XWayland, un renderer multi-GPU y una capa de UI en iced— cierre con ocho librerías compartidas y ningún parche es el resultado más limpio de las cuatro campañas de escritorio.

🖥 EL PRIMER ARRANQUE: qué falló y qué no (2026-08-03)

Tres imágenes y tres causas distintas, todas medidas. Vale dejarlas porque ninguna estaba en el camino que uno anticipa, y las tres se manifestaron a varias capas de distancia de su causa.

1. cosmic-session no es opcional en el sentido que yo asumí — y cosmic-settings-daemon tampoco. El compositor arrancó perfecto y le devolvió a la sesión su WAYLAND_DISPLAY=wayland-1; el gestor entonces intentó lanzar cosmic-settings-daemon, no lo encontró, y panickeó:

INFO  cosmic_session: got environmental variables from cosmic-comp: [("WAYLAND_DISPLAY", "wayland-1")]
ERROR panic: 'failed to start settings daemon: NotFound' src/main.rs:255

Es un .expect(), sin feature que lo apague. Y NO es el único — son TRES, y esto corrige lo que esta misma sección afirmaba hace unas horas:

líneacomponentequé pasa si falta
main.rs:255cosmic-settings-daemonpanic
main.rs:306cosmic-notificationspanic
main.rs:325cosmic-panelpanic

El resto (app-library, launcher, workspaces, osd, bg, idle) va por start_component(), que ante un fallo sólo hace error!("failed to start …") y sigue (main.rs:558). O sea que el mínimo viable de una sesión COSMIC son cuatro binarios: compositor + esos tres.

Cómo se equivocó el diagnóstico, que es la parte reusable: leí start_component(), vi que sólo logueaba, y generalicé a «todos menos el settings-daemon». Pero notifications y panel no pasan por esa función — tienen su propio .expect() unas líneas antes. Buscar la función que lanza no es lo mismo que buscar los \.expect( del fichero; grepear la construcción que mata cuesta lo mismo y no generaliza de más. Esto corrige lo que decía la sección de cosmic-settings-daemon: no es que sin él se pierdan el brillo y el tema, es que no hay sesión. Queda por eso el modo bare en cosmic-start —el camino que upstream ya soporta como install-bare-session—: se lanza el compositor solo y los clientes a mano contra su WAYLAND_DISPLAY. Separa «¿compone?» de «¿arranca la sesión?», que son dos preguntas y estaban viniendo juntas.

2. La pantalla negra era libz.so.1. El corpus trae zlib estática, que le alcanza a todo el cierre de COSMIC. Pero kms_swrast_dri.so es un .so que mesa abre con dlopen y NEEDea libz.so.1:

MESA-LOADER: failed to open kms_swrast: Error loading shared library libz.so.1
WARN  cosmic_comp::backend::kms: Failed to add device /dev/dri/card0: Failed to initialize GBM device

Sin driver de software no hay GBM, y el compositor arranca igual y presenta una pantalla negra. La causa está a tres capas del síntoma. Se arregla agregando zlib-shared como raíz del hidratador.

3. Los clientes NO pueden ser estáticos. cosmic-bg moría con «The wayland library could not be loaded» teniendo libwayland-client.so.0 en el rootfs. wayland-client entra con la feature dlopen: carga libwayland en runtime en vez de enlazarla, y un binario musl estático no tiene dlopen funcional. No faltaba la librería: faltaba poder abrirla. Todos los clientes pasan a link = "dynamic".

Lo que NO falló, y conviene decirlo: el compositor no necesitó un solo parche, ni una perilla de KMS, ni forzar modeset legacy. Las cuatro perillas de render por software heredadas de KDE y GNOME (GBM_ALWAYS_SOFTWARE, kms_swrast, LP_NUM_THREADS=1) alcanzaron. GNOME costó cinco muros para llegar a este punto; COSMIC costó tres, y ninguno era del compositor.

Dos carreras con /run, la misma causa

dbus-daemon --system falló con «Failed to bind socket /run/dbus/system_bus_socket: No such file or directory» aunque el directorio se creaba al principio del script, y dbus-run-session se quejó por su lado de que XDG_RUNTIME_DIR no existía. Entre el arranque del getty y el uso pasan varios segundos en los que arje-zero sigue montando, y un tmpfs sobre /run se lleva puesto lo que hubiera. Crear justo antes de usar es la única forma de no depender de ese orden; comprobar al principio y usar al final es exactamente cómo se cuela una carrera — la misma forma del bug de PolicyKit1 que se cerró hoy en GNOME.

Gotchas medidos (no previstos)

  1. cargo rustc -- exige UN target. La fase compile del lab pasa flags tras el --, y cualquier paquete con binario + lib + ejemplos corta con «extra arguments to rustc can only be passed to one target». Se arregla con flags = ["--bin", "<nombre>"]. Le pasa a cosmic-comp y a cosmic-bg (workspace con config/), y le va a pasar a casi todos: ponelo desde el principio.

  2. start-cosmic es bash de verdad, no sh. Usa mapfile, [[ ]] y expansión indirecta ${!var}; con el busybox del rootfs no arranca. bash ya está en el corpus, pero tiene que entrar en la imagen — y no lo pide ningún [deps], así que no lo va a traer nadie por accidente. Lo mismo con dbus-run-session (de dbus), que es lo que el script exec-uta al final.

  3. rust-toolchain.toml con channel = "1.93" es inerte acá — es una perilla de rustup, y el sandbox tiene el rustc de Alpine (1.96) pelado. Conviene saberlo antes de «arreglarlo».

  4. cosmic.desktop existe DOS veces (en cosmic-session y en cosmic-comp) con contenido distinto y en la misma ruta. El de cosmic-comp es el de la «bare session» y apunta a /usr/bin/cosmic-service, que vive en la rama de systemd que acá no va. Se instala sólo el de cosmic-session; si entraran los dos, quién gana depende del orden de hidratación y el que pierde es el bueno.

  5. libsystemd no es systemd. La feature default = ["systemd"] de cosmic-comp trae el crate libsystemd, que es una reimplementación en Rust del protocolo, no un binding a la C. Y logind habla org.freedesktop.login1, que acá lo sirve arje-logind-compat. No hay que apagar nada: el nombre de la feature es el del protocolo, no el del programa que lo contesta.

  6. Las features de smithay que asustan no cuestan deps de build: backend_vulkan (crate ash), backend_x11 (x11rb con dl-libxcb) y xwayland cargan todo por dlopen. Son deuda de RUNTIME —libxcb y vulkan-loader viven hoy sólo en la cola KDE—, no un muro de compilación.

  7. Todo cliente de la suite lleva las MISMAS CUATRO deps: libxkbcommon, pkgconf, libudev-zero, libinput — incluso las piezas sin interfaz. Las tres primeras se descubrieron de a una, un build por dep, hasta que la línea de enlace las mostró juntas:

    -Wl,-Bstatic  -ludev  -linput  -lxkbcommon …
    

    El error dice la que falta primero; el COMANDO dice todas. Leerlo entero la primera vez habría ahorrado tres builds. Los tres llegan por la cadena de libcosmic —enumeración de monitores y de dispositivos de entrada—, no por nada que el cliente haga a la vista.

    Y el camino hasta ahí fue de razonamientos buenos y falsos, que es lo que vale dejar escrito:

    • cosmic-bg fue sin [deps] porque «habla el protocolo en Rust y no toca C». Reventó en el build.rs de smithay-client-toolkit pidiendo xkbcommon.pc: sctk ENLAZA libxkbcommon; el que la dlopea es winit, que es otra capa. Hablar Wayland en Rust dice cómo viaja el byte, no con qué se interpreta un teclado.
    • cosmic-idle fue sin [deps] porque ese argumento ya no aplicaba —no usa sctk— y un demonio de inactividad no interpreta teclas. Reventó en el ENLACE con -lxkbcommon, arrastrado por cosmic-settings-config: de ahí sale la tabla de atajos y la usa todo componente de la suite. La dep no viene de lo que el paquete hace, sino de la librería de configuración común.

    La lección de método vale más que la dep: la dep no se deduce de la función del paquete. Está escrita en el build.rs que panickea o en la línea de enlace, y las dos veces el error dijo exactamente quién y por qué. El link = "static" sobrevive porque el artefacto de libxkbcommon trae .a además de .so.

  8. Una feature apagada en el paquete puede estar prendida por otro. cosmic-bg declara image con default-features = false y sin AVIF, y aun así compila dav1d-sys: las features de cargo se UNIFICAN en el grafo. Se resolvió trayendo dav1d (b3:aab7a874) en vez de parchear el Cargo.toml de upstream — si el grafo dice que sabe decodificar AVIF, que lo sepa de verdad. Mismo modo de falla que un .pc Requires arrastrando una dep que nadie declaró.

  9. cosmic-wallpapers no se puede empaquetar del tarball: está en git-lfs. El default de cosmic-bg apunta a /usr/share/backgrounds/cosmic/orion_nebula_nasa_heic0601a.jpg, y el archivo de GitHub de ese repo pesa 20 KB — son punteros LFS, no imágenes. Una receta escrita del modo normal produciría un paquete de ficheros de texto que además parecería correcto: se instalarían con el nombre bueno en la ruta buena. Mientras tanto cosmic-start escribe la config de USUARIO con una fuente Color, que cosmic-bg soporta de fábrica — y va en el lanzador, no en el artefacto, porque es política de la imagen de prueba y no algo que el paquete prometa.

  10. build.rs contento ≠ linker contento: cosmic-applets necesita la libdbus COMPARTIDA. Los applets de red, bluetooth y status-area hablan D-Bus por libdbus-sys (FFI a la C, no zbus como el resto de la suite). Declarar dbus a secas alcanza para que su build.rs pase —le basta el .pc, que el paquete del daemon trae— y el fallo aparece una hora después, en el enlace final del multiplexor: unable to find dynamic system library 'dbus-1'. El corpus sólo empaqueta libdbus-1.a; el .so vive en dbus-shared. Son dos preguntas distintas y las contestan dos herramientas distintas: pkg-config dice si la dep existe, el linker si hay algo que enlazar. Era la única receta de la cola que pedía la estática.

  11. cosmic-workspaces necesita mesa, y la dep sale de lo que el programa MUESTRA. La vista de espacios de trabajo dibuja miniaturas vivas de las ventanas; el compositor se las pasa como DMA-BUF por zcosmic-toplevel-info y el cliente importa el fd con GBM ⇒ -lgbm. Es el único cliente de la suite que toca el pipeline gráfico además del compositor, así que el juego fijo de cuatro (libxkbcommon, pkgconf, libudev-zero, libinput) no alcanza. Corolario del gotcha de siempre: la dep no se deduce de la familia del paquete, se lee de lo que hace.

  12. DOS GPUs en QEMU = el panel dibuja en la pantalla que no mirás. -device virtio-gpu-pci no reemplaza a la VGA por defecto de QEMU: la SUMA. El invitado ve card0 y card1, cosmic-comp crea dos outputs (Virtual-1 y Unknown-1), cosmic-bg pinta el fondo en ambos y el panel se ancla en uno solo. El screendump captura el otro ⇒ fondo perfecto, panel invisible, y ni un solo error en el log. Con -vga none el escritorio aparece entero a la primera. Y esto corrige un diagnóstico anterior de este mismo runbook: cuando el panel selló y no dibujó, lo atribuí a que le faltaban los applets. Los applets hacían falta —sin ellos el panel está vacío— pero no eran la causa de la pantalla lisa; la causa era la segunda GPU, que ya estaba ahí. Un síntoma puede tener dos causas suficientes y arreglar la primera no prueba nada: la prueba es la pantalla, no el razonamiento.

  13. mkdir: not found en cosmic-start, y la sesión arrancaba igual. El getty entrega un PATH sin /usr/bin, y en la imagen fundida mkdir sólo vive ahí (/usr/bin/mkdir → coreutils; la base metal no tendió el symlink /bin/mkdir → busybox que sí tienen chmod, mount, sed y tee). Los cuatro mkdir -p de la preparación fallaban y no se notaba, porque los directorios venían horneados en la imagen. Es la forma más traicionera de bug: gratis hoy, fatal el día que alguien cambie cómo se arma la imagen. Fix: exportar PATH como primera línea del script.

Cómo se construye

cd ~/hammer
./target/release/takana --store store hash recipes/incoming-cosmic/<pieza>.toml   # dry-run, ~2ms
./target/release/takana build recipes/incoming-cosmic/<pieza>.toml --store store

🏔 EL ESCRITORIO ENTERO, DIBUJADO (2026-08-04)

docs/evidencia/cosmic-panel-applets-qemu-2026-08-04.png — 1280×800, 895 colores distintos:

48 procesos cosmic-* estables a los 30 s, sobre el kernel de takana con arje-zero como PID1. Se reproduce así (headless y automatizable; el -vga none no es opcional, ver gotcha 12):

COSMIC_MODE=session bash scripts/cosmic/qemu-desktop-image.sh
qemu-system-x86_64 -machine q35,accel=kvm -cpu host -smp 8 -m 8192 \   # 8 vCPU: ver el deadline del panel
  -drive if=pflash,format=raw,readonly=on,file=/usr/share/edk2/x64/OVMF_CODE.4m.fd \
  -drive if=pflash,format=raw,file=/tmp/OVMF_VARS.fd \
  -drive file=work/hammer-cosmic-qemu.img,format=raw,if=virtio \
  -vga none -device virtio-gpu-pci,xres=1280,yres=800 \
  -device qemu-xhci,id=xhci -device usb-kbd,bus=xhci.0 -device usb-tablet,bus=xhci.0 \
  -display none -serial file:/tmp/cosmic.serial -qmp unix:/tmp/qmp.sock,server,nowait
# y después, por QMP: {"execute":"screendump","arguments":{"filename":"/tmp/shot.ppm"}}

El veredicto es contar colores distintos del PPM, no leer el log: con dos GPUs el log salía impecable y la pantalla estaba lisa.

🖱 LA ENTRADA: se usa, no sólo se ve (2026-08-04)

Hay que usar USB HID, no virtio-input. El kernel de takana no trae VIRTIO_INPUT (mirá el -e … de recipes/linux-generic.toml), así que -device virtio-keyboard no produce ni un /dev/input/event*. Sí trae USB_XHCI_HCD + USB_HID + INPUT_EVDEV, y además es lo que se va a usar en metal, así que la prueba es más fiel:

-device qemu-xhci,id=xhci -device usb-kbd,bus=xhci.0 -device usb-tablet,bus=xhci.0

usb-tablet y no usb-mouse: reporta coordenadas ABSOLUTAS, y QMP sólo sabe mandar eventos abs (el eje va de 0 a 32767 sobre el ancho/alto de la pantalla, no en píxeles).

Se maneja entero por QMP, sin ver la pantalla y sin humano:

// mover el puntero a (155,16) — el botón «Applications»
{"execute":"input-send-event","arguments":{"events":[
  {"type":"abs","data":{"axis":"x","value":3968}},
  {"type":"abs","data":{"axis":"y","value":655}}]}}
// click
{"execute":"input-send-event","arguments":{"events":[{"type":"btn","data":{"down":true,"button":"left"}}]}}
{"execute":"input-send-event","arguments":{"events":[{"type":"btn","data":{"down":false,"button":"left"}}]}}
// una tecla
{"execute":"input-send-event","arguments":{"events":[
  {"type":"key","data":{"down":true,"key":{"type":"qcode","data":"c"}}},
  {"type":"key","data":{"down":false,"key":{"type":"qcode","data":"c"}}}]}}

El veredicto también es de píxeles, y conviene medirlo por REGIÓN. El click en Applications llevó el color dominante de (33,74,135) (fondo, 91 %) a (27,27,27) (84 %): la biblioteca de aplicaciones se abrió a pantalla casi completa. Y para el teclado, contar colores del rectángulo del buscador (440..840 × 80..112) bastó: 155 → 118 colores al reemplazarse el placeholder por lo tecleado. Evidencia: cosmic-app-library-click-qemu-2026-08-04.png y cosmic-teclado-busqueda-qemu-2026-08-04.png.

La biblioteca sale vacía y está BIEN. Los 20 .desktop de la imagen son NoDisplay=true —son applets y componentes de sesión, no aplicaciones—, así que no hay nada que listar. Que la búsqueda no devuelva resultados no es un fallo del escritorio: es que la distro todavía no tiene ni una sola aplicación gráfica. Se sabrá arreglado cuando entre cosmic-files o un terminal.

🔑 Los atajos: dos ficheros envenenados por la misma línea

Super+A abre la biblioteca y Super+W la vista de espacios de trabajo (cosmic-super-w-workspaces-qemu-2026-08-04.png, con miniatura viva del escritorio dentro — o sea que el camino DMA-BUF→GBM del gotcha 11 anda de verdad). Para llegar ahí hubo que sanear DOS ficheros de datos, no uno:

ficherolo instalaqué liga
…Shortcuts/v1/defaultscosmic-comptecla → acción
…Shortcuts/v1/system_actionscosmic-settings-daemonacción → comando

Los dos traen una entrada ScreenReader, una variante que cosmic-settings-config —el crate que los parsea, pineado por cargo en la rev 8c54bbbc— todavía no tiene en su enum. RON no ignora lo que no conoce: falla la deserialización del MAPA COMPLETO. Con defaults roto no hay ligaduras; con system_actions roto hay ligadura pero ningún comando que ejecutar. Hacen falta los dos grep -v.

Y acá me equivoqué de fichero teniendo el dato exacto delante. El error decía 3:5-3:17: Unexpected variant named 'ScreenReader' — con línea y columna. Busqué el token en vez de preguntarme qué fichero lo tiene en esa posición, di con keybindings.ron (que también lo traía, en la línea 94), lo arreglé, reconstruí cosmic-comp cuarenta minutos y el error salió igualito. Era otro fichero, de otro paquete. Cuando el error trae coordenadas, la primera pregunta es de qué fichero son, no dónde más aparece esa palabra.

Es el riesgo del pin lockstep asomando desde adentro de upstream: el dato y el parser del mismo epoch no coinciden.

Super abre el lanzador — pero hizo falta un paquete más, y de otro repo: ver abajo.

🔎 pop-launcher: el lanzador es DOS programas

Super no abría nada y el único rastro era una línea que no menciona ninguna tecla: ERROR pop-launcher failed to start: No such file or directory. cosmic-launcher es sólo la interfaz: dibuja la lista y manda lo tecleado por stdin a un proceso hijo, pop-launcher, que es quien busca. Sin ese binario la ventana no tiene qué mostrar, así que no se muestra — el mismo modo de falla que el panel sin applets: lo que falta es un HIJO, no una librería, y el log del padre se lee sano.

Vive en otro repo (pop-os/launcher), no versiona por epoch-N y no entra en el pin lockstep. La versión la fija el Cargo.lock de cosmic-launcher —rev a332a3a7 de la 1.2.7—, no el último tag del repo: el lock es lo que garantiza que hablen el mismo protocolo. El tarball se pide por commit (/archive/<sha>.tar.gz), porque no hay tag que nombre esa rev. Selló a la primera (b3:64a5cbaa) con un solo NEEDED: libc.so.

Es otro multiplexor por symlink, como cosmic-applets: un binario, y cada plugin es un enlace con su nombre dentro de su propio directorio junto al plugin.ron que lo describe. El servicio enumera plugins/*/; si está el .ron y falta el enlace —o al revés— el plugin no existe para él, sin error. Ojo con el sed 's/_/-/' de upstream: cambia sólo el primer guión bajo, así que el directorio es desktop_entries y el ejecutable de adentro desktop-entries. Son dos nombres distintos a propósito.

Verificado de punta a punta (cosmic-super-lanzador-, cosmic-pop-launcher-plugins-, cosmic-pop-launcher-calc-qemu-2026-08-04.png): Super abre el lanzador, ? lista los seis plugins con su sintaxis —o sea que la enumeración y el IPC JSON-sobre-stdio andan— y =2+2 lanza el plugin, que contesta «qalc command is not installed». Esa respuesta es la mejor prueba que había disponible: el proceso hijo corrió, evaluó la consulta y devolvió una fila. Lo que falta ahora es libqalculate (el binario qalc), que es un paquete, no un problema.

🧮 libqalculate: la calculadora del lanzador, y un stdin que era stdout

=123*456 → 56088 en el lanzador (cosmic-calculadora-qemu-2026-08-04.png). La cadena entera desde una tecla está en el catálogo: Super → cosmic-launcher → pop-launcher → plugin calc → qalc → mpfr → gmp.

Pero el camino hasta ahí tuvo dos muros que vale la pena dejar escritos, porque ninguno se parecía a lo que era.

1) stdin apuntaba al descriptor 1. El plugin no le pasa la cuenta por argv: arranca qalc SIN expresión, con stdin en tubería, le escribe la expresión y cierra. En ese modo el binario no leía nada — ni error ni resultado, la fila del lanzador salía en blanco. El síntoma acusaba a readline, al toolchain y a la versión, y no era ninguno:

DIAG-main isatty=0 feof=0 ferror=0 fd=1        ← fileno(stdin) == 1
nm -S qalc | grep -E ' (stdin|stdout|stderr)$'
  1847148 8 B stderr
  1847148 8 B stdin                             ← las TRES en la misma dirección
  1847148 8 B stdout

La causa: el binario salía DINÁMICO aunque la receta dice link = "static", porque libtool se come el -static que inyecta el harness ([[link-static-mentira-libtool]] otra vez, cuarto caso). Y en un musl dinámico las tres variables de stdio se resolvieron por copy relocation a una sola ranura de BSS compartida: la última que escribe el loader gana, así que el stdin del programa era el stdout. De ahí la asimetría que parecía absurda: qalc -f /dev/stdin funcionaba —abre el descriptor por RUTA, sin tocar el FILE* roto— y qalc leyendo stdin no.

El fix es el par que ya usa libxml2: make LDFLAGS="-all-static -no-pie". Con él el binario queda estático de verdad y los tres símbolos vuelven a tener direcciones distintas, en .rodata:

  11e71c8 8 R stderr
  11e71d0 8 R stdin
  11e71d8 8 R stdout

La regla, y es de las caras: link = "static" en la receta no garantiza un binario estático. Cuando algo se comporta como si su stdin/stdout estuvieran cruzados, readelf -d y nm -S | grep ' std' contestan en dos segundos lo que el log no dice nunca.

Barrido de la cola entera con ese criterio, después del fix: de las 7 recetas que declaran link = "static" (cosmic-session, gmp, mpfr, nasm, libqalculate y las dos que en realidad son dynamic), ninguna tiene NEEDED indebido y ninguna tiene los símbolos de stdio aliasados. Era un caso, no un patrón — pero el patrón que lo causa (libtool comiéndose el -static) ya tiene cuatro víctimas anteriores en el corpus.

2) La pregunta de primer arranque se come la cuenta, SIEMPRE. Antes de leer nada, qalc pregunta si activar autocalc. El plugin le manda la expresión y cierra: la expresión se consume como respuesta, no es un sí/no válido, y qalc no llega a guardar config ⇒ la próxima vez vuelve a preguntar. No es «falla la primera vez»: no anda nunca. Se siembra ~/.config/qalculate/qalc.cfg con calculate_as_you_type=0 desde cosmic-start — política de la imagen, como el color de fondo; una distro de verdad lo pondría en /etc/skel.

Gotchas de empaquetado de la cadena:

🖥 cosmic-term: la primera APLICACIÓN, y la biblioteca deja de estar vacía

docs/evidencia/cosmic-terminal-qemu-2026-08-04.png:

bash-5.3# uname -srm; qalc -t '6*7'
Linux 6.16.12 x86_64
42

Una ventana de verdad —con decoración, File/Edit/View, minimizar/maximizar/cerrar—, bash corriendo adentro, el kernel propio de takana contestando, y el qalc que empaquetamos una hora antes devolviendo 42. Se abre haciendo click en su icono en la biblioteca de aplicaciones (cosmic-biblioteca-con-app-qemu-2026-08-04.png), que hasta hoy salía vacía con razón: los 20 .desktop de la imagen eran NoDisplay=true. Éste no lo es, y es el primero.

Selló a la primera, y eso es noticia por lo que arrastra: su Cargo.toml depende de cosmic-files —el gestor de ficheros entra como LIBRERÍA, por el selector y el arrastrar-y-soltar—, así que compilar la terminal compila medio gestor de ficheros. Mismo patrón que cosmic-settings-config en los applets: en esta suite las «aplicaciones» se usan unas a otras como crates, y el grafo de deps de Cargo no se parece al mapa de componentes.

wgpu viene en default, al revés que el resto de los clientes que pintan con tiny-skia. Se dejó como está: wgpu y naga ya se habían compilado enteros para cosmic-workspaces, así que el costo estaba pago, y apartarse de las features por defecto es apartarse de la única combinación que upstream prueba. Cierra con dos NEEDED: libxkbcommon.so.0 y libc.so.

Terminfo, para la próxima: la ncurses del corpus es libs-only y no instala la base de terminfo, pero trae --with-fallbacks=…,xterm-256color,… compilados. A la terminal no le hace falta —el que necesita terminfo es lo que corre ADENTRO— y el hijo acá es el sh de busybox, que no lo usa. El día que entren vim o htop en la imagen, hará falta un paquete de datos de terminfo.

📁 cosmic-files: «ya está compilado» era falso por una línea del vecino

docs/evidencia/cosmic-files-qemu-2026-08-04.png: el gestor abre desde la biblioteca y lista el sistema de ficheros real de takana — bin (72 ítems), dev (129), ente, etc, lib, lost+found—, con su barra lateral de Recientes/Carpeta personal/Papelera.

Entró creyendo que era casi gratis, porque cosmic-term ya compila cosmic-files como crate. No lo era, y el motivo cabe en una línea del Cargo.toml del vecino: cosmic-term lo declara con default-features = false. Lo que un paquete cuesta depende de con qué features lo pide quien lo usa, así que «ya se compiló» puede ser falso.

Se apagaron dos features por defecto, y las dos por una razón medida:

El intento con glib declarado dejó una regla que vale para toda la cola: el error fue Package 'libpcre2-8', required by 'glib-2.0', not found — o sea que el que falla no es la dep sino lo que su .pc declara en Requires. Declarar una dep trae su artefacto, no su clausura de pkg-config. Se lee en dos segundos con grep ^Requires sobre los .pc del artefacto, ANTES de gastar el build: glib-2.0.pc → libpcre2-8, gobject-2.0.pc → libffi, gio-2.0.pc → zlib.

⚙ cosmic-settings: un panel de control que NO arrastra medio sistema

docs/evidencia/cosmic-settings-qemu-2026-08-04.png: abre desde el lanzador (Super, teclear «settings») con la barra lateral entera —Red, Bluetooth, Accesibilidad, Escritorio, Pantallas, Sonido, Energía, Dispositivos de entrada, Aplicaciones, Fecha y hora, Sistema y cuentas— y la página de Escritorio poblada. Tarda ~20 s en aparecer con render por software; no está colgado.

La sorpresa buena: no arrastra NetworkManager, ni libpulse, ni udisks, ni accountsservice. Habla con los daemons por D-Bus vía zbus, que es Rust puro. Se verificó ANTES de escribir la receta listando los crates -sys del Cargo.lock —que es donde vive la verdad sobre qué C hace falta—, y salieron sólo: drm-sys, input-sys, libudev-sys, dav1d-sys, wayland-sys (dlopen) y gettext-sys (trae su propio C). O sea el juego fijo de todo cliente + libdrm + dav1d. Regla barata que ahorra builds: grep '^name = ".*-sys"' Cargo.lock | sort -u antes de adivinar deps por lo que el programa hace.

Volvió a morder la doble barra de cosmic-protocols//, igual que en cosmic-applets: el manifiesto usa esa forma para que cargo trate la fuente como distinta y el [patch] no se apunte a sí mismo, GitHub le contesta 404, y el build muere en el fetch con tres reintentos de red y failed to load source for dependency 'cosmic-client-toolkit'. Mismo fix: mover la barra al medio (pop-os//cosmic-protocols) en Cargo.toml y Cargo.lock. Segunda víctima ⇒ es un patrón de la suite, no una rareza de un repo: conviene mirarlo apenas un cargo vendor da 404.

Instala 32 .desktop, uno por página, así que cada sección del panel aparece como entrada propia en la biblioteca. Y resources/default_schema/ va con find, no con cp plano: es el árbol <Componente>/v1/<clave> de cosmic-config, donde cada clave es un fichero cuyo nombre es la clave — aplanarlo deja el escritorio sin esa configuración, callado.

Deuda que deja anotada su propio log: org.freedesktop.locale1 was not provided by any .service files — nadie sirve localed en la imagen, así que la página de idioma no va a poder cambiar nada. Es del mismo tipo que login1 (que sí resuelve arje-logind-compat), y se arregla del mismo modo.

⏱ El panel tiene un DEADLINE de arranque: con 4 vCPU sale mutilado

Medido: con -smp 4 el panel arranca sin ala izquierda, sin reloj y sin dock —sólo la bandeja derecha— y el único rastro es Failed to setup panel dbus server deadline has elapsed. Con -smp 8 -m 8192: 4 arranques de 4 con el escritorio completo y cero deadlines vencidos. La causa es la estampida del arranque: el panel registra su servidor D-Bus mientras catorce clientes de iced escanean fuentes al mismo tiempo.

CORRECCIÓN medida el 2026-08-04 validando cosmic-edit: la carga del anfitrión TAMBIÉN cuenta, y -smp 8 no la compensa. Acá decía «no es carga del anfitrión (falla con el anfitrión ocioso)» — cierto para el caso de 4 vCPU, pero se leía como que con 8 el anfitrión daba igual, y no. Dos arranques de la MISMA imagen con la MISMA línea de QEMU (-smp 8 -m 8192):

anfitrióncoloresveredicto
load average ≈ 6 (otro agente compilando)130mutilado, y deadline has elapsed en el log
ocioso (load ≈ 1,8)1671completo

Tiene sentido y conviene decirlo entero: este anfitrión tiene 8 núcleos (nproc), así que -smp 8 le entrega la máquina entera a la VM y cualquier otra cosa que compile compite por los mismos hilos. La receta práctica: antes de creerle a un arranque mutilado, mirá /proc/loadavg y repetí con el anfitrión quieto — es un minuto y evita perseguir una regresión que no existe, que es exactamente el error que ya está documentado dos párrafos más abajo.

Y una lección de medición que costó horas: el conteo de procesos NO sirve como veredicto. Perseguí una regresión inexistente comparando «48 procesos» contra «31», revirtiendo uno por uno tres cambios míos que no tenían nada que ver — cuando un escritorio completo y sano muestra 32. El número de procesos no distingue «arrancó» de «dibujó». El framebuffer sí, y sin ambigüedad: 894 colores = completo, 130 = mutilado. (Con las cuatro aplicaciones en el dock el escritorio completo da ~1670: el umbral es «cientos vs. miles», no un número exacto.)

✎ LA CUARTA APLICACIÓN: cosmic-edit (2026-08-04)

docs/evidencia/cosmic-edit-qemu-2026-08-04.png y …-escribiendo-…png — 1280×800, 2105 colores: ventana con menús File/Edit/View, pestaña New document, numeración de líneas, y el icono del editor en el dock con el punto de «en ejecución». La segunda captura tiene takana edita escrito por teclado sobre la línea 1 y el punto de modificado en la pestaña: se escribe, no sólo se ve.

Se llegó por el lanzador, sin tocar el ratón para elegir: Super → teclear text → el primer resultado es «Text editor for the COSMIC desktop» (Ctrl+1) → Enter. Que el editor sea lo que responde a «text» es efecto del .desktop que genera xdgen, con sus Keywords y su MimeType=text/plain — el mismo fichero que lo hace manejador por defecto de los .txt en el gestor.

Lo que la receta enseñó, y que corrige un borde de la regla del Cargo.lock: leer los crates -sys del lock sigue siendo lo primero que hay que hacer, pero el lock lista lo POSIBLE, no lo ENCENDIDO. En cosmic-edit aparecen gio-sys, glib-sys y gobject-sys —que en cualquier otro paquete mandarían a declarar glib, y de ahí a la campaña de la cadena compartida— y entran sólo por la feature gvfs, que se apaga. El lock dice el universo; las features dicen el recorte. La receta no declara ninguna glib y su binario enlaza sólo libxkbcommon.so.0 y libc.so: dos NEEDED, el mínimo de la suite.

🛍 LA QUINTA: cosmic-store, y el catálogo que hubo que fabricar (2026-08-05)

docs/evidencia/cosmic-store-qemu-2026-08-05.png — la tienda abierta con Popular apps poblada (App Library, Files, Launcher, Settings) y Recently updated; y …-busqueda-…png con la búsqueda text devolviendo COSMIC Text Editor. Sellada en b3:a06437f7, NEEDED = libxkbcommon + libc.

Es la primera aplicación de la suite que choca con que takana es OTRA distro. Una tienda es la cara de un gestor de paquetes. De sus cuatro backends (src/backend/, todos tras #[cfg(feature)]): flatpak pide libflatpak (GObject ⇒ glib compartida + ostree/libsoup/gpgme: la torre de C que esta campaña evita); packagekit es Rust puro pero necesita el DEMONIO en el bus; rpm-ostree es irrelevante; pkgar es el único que arranca — ni demonio ni enlace.

ALCANCE: navega, no instala. Nada está cableado al .swm (Etapa F). El camino barato para arreglarlo quedó identificado: servir org.freedesktop.PackageKit sobre .swm y encender la feature packagekit. Es un shim de D-Bus, mismo patrón que arje-logind-compat, y no cuesta un solo .so de C.

El catálogo AppStream: dos trampas encadenadas

1. pkgar NO lee /usr/share/metainfo/. Lee el CATÁLOGO — {/usr/share,/var/lib,/var/cache}/{swcatalog,app-info}/{xml,yaml}/ — que en una distro normal genera appstreamcli compose agregando los metainfo en un <components version=… origin=…>. Sin ese fichero la tienda abre perfecta y no lista NADA. Lo genera ahora qemu-desktop-image.sh con doce líneas de python; no hace falta AppStream ni glib.

2. Hay que SACAR el <provides>. Los metainfo que genera xdgen lo escriben con los envoltorios <mimetypes>/<binaries>, y el crate appstream espera los hijos planos del esquema de catálogo. Con el envoltorio muere con The tag provide doesn't have a value y descarta el componente entero, callado: listaba 2 de 7, justo los dos sin <provides> (App Library y Launcher).

Cómo se diagnosticó, que es lo reutilizable: desde el lanzador la salida del proceso NO llega al log de la sesión. Hay que abrir cosmic-term DENTRO de la VM y correr rm -rf ~/.cache/cosmic-store; RUST_LOG=info cosmic-store 2>/tmp/s.log — el rm no es opcional, la tienda cachea el parseo en ~/.cache/cosmic-store/*.bitcode y si no se borra vuelve a mostrar el resultado viejo. Que la imagen tenga terminal deja de ser una comodidad y pasa a ser instrumental.

Y el deadline del panel es una CARRERA, no un umbral

Con el anfitrión ocioso y -smp 8, un arranque de esta tanda salió igual mutilado (130 colores) con 33 procesos vivos y el deadline has elapsed en el log. O sea: 8 vCPU + anfitrión quieto sube mucho la probabilidad pero no garantiza. Si sale mutilado y la carga estaba baja, rearrancá antes de buscar la causa en tu cambio.

📷 LA SEXTA: cosmic-screenshot, sellada y ROTA A PROPÓSITO (2026-08-05)

b3:8b57ec9b, 9 minutos de build, NEEDED = libc.so y nada más — un solo NEEDED, y el único [deps] build = [] de la campaña: 130 líneas de Rust, ni libxkbcommon ni libinput, porque no dibuja ni habla wayland. Todo su trabajo es Screenshot::request() de ashpd.

Se incluye en la imagen sabiendo que falla, para que el hueco sea medible. El error exacto está en docs/evidencia/cosmic-screenshot-sin-portal-2026-08-05.png, corrido desde cosmic-term dentro de la VM:

panicked at src/main.rs:76:10: failed to send screenshot request: Portal(ZBus(MethodError(
  OwnedErrorName("org.freedesktop.DBus.Error.ServiceUnknown"),
  Some("The name org.freedesktop.portal.Desktop was not provided by any .service files"))))

Misma forma que la deuda de org.freedesktop.locale1 que anota el log de settings: un nombre de D-Bus que nadie sirve.

La cadena del portal son TRES eslabones, no dos

Medido en el fuente de xdg-desktop-portal-cosmic: static DBUS_NAME = "org.freedesktop.impl.portal.desktop.cosmic" (src/main.rs:27). Es un backend; no toma el nombre que los clientes buscan. La cadena es:

cosmic-screenshot → org.freedesktop.portal.Desktop → xdg-desktop-portal (frontend, C sobre GIO) → org.freedesktop.impl.portal.desktop.cosmic → xdp-cosmic (backend, Rust)

Faltan los dos, y ninguno está en ninguna cola (xdg-desktop-portal-gnome sí está, pero es otro backend). Lo que sí hay para el backend: pipewire, gbm (mesa) y wayland ya en el corpus. Su único bloqueo real es que declara cosmic-files con la feature gvfs fijada a mano — el mismo patrón que ya bloqueó cosmic-files-applet, y acá es un parche de una línea.

⚠ CORRECCIÓN: la cadena glib COMPARTIDA YA EXISTE Y ESTÁ SELLADA

Este documento decía que gvfs exigía «una campaña propia». Es falso desde que la campaña KDE selló las piezas. Medido con takana hash desde las dos colas, que es el método correcto para saber si una receta se puede compartir:

recetacola¿resuelve igual desde incoming-cosmic?
glib-shared 2.88.1incoming-kdesí — b3:0584ce1f, ya sellada
pcre2-sharedincoming-kdesí — b3:f254abaa, ya sellada
zlib-sharedya en incoming-cosmic—

Hash idéntico ⇒ cero rebuild: los artefactos sirven tal cual. libffi entra estático dentro de la propia .so, así que no hace falta variante compartida (su libffi aparece en Requires.private, que pkg-config ignora fuera del modo estático).

Pero la decisión sigue teniendo costo y no es técnica: encender gvfs mete una SEGUNDA glib en la imagen —justo lo que la campaña GNOME midió y evitó— y re-hashea cosmic-files, que arrastra a cosmic-term y cosmic-edit porque lo usan como crate.

🚪 LA CADENA DEL PORTAL, CERRADA (2026-08-05)

docs/evidencia/cosmic-portal-captura-2026-08-05.png — cosmic-screenshot corrido desde cosmic-term dentro de la VM devuelve una ruta en vez de un panic, y el fichero existe:

/root/Pictures/Screenshots/Screenshot_2026-08-05_10-18-14.png
-rw-r--r-- 1 root root 42256 Aug  5 10:18 Screenshot_2026-08-05_10-18-14.png
0000000 211   P   N   G  \r  \n 032  \n          ← la firma PNG, no un fichero vacío

El veredicto es el fichero, no el código de salida: que el binario no panickee sólo prueba que encontró el servicio; que haya 42 KB con la firma \x89PNG prueba que el compositor entregó píxeles por la cadena entera.

Cinco recetas nuevas, en el orden en que hicieron falta:

recetahashpor qué
fuse3 3.18.2b3:3a84af86dep dura del frontend (document portal)
json-glib 1.10.8b3:dc8c8e78dep dura; variante SIN introspección (ver abajo)
xdg-desktop-portal 1.18.4b3:f4adb8c9el frontend: registra org.freedesktop.portal.Desktop
clang18b3:62c6bfb5sólo libclang.so, para el bindgen de libspa-sys
xdg-desktop-portal-cosmicb3:7ad0f124el backend: las 5 interfaces impl.portal.*

Son dos raíces del perfil y de la hidratación porque son dos procesos que se encuentran por D-Bus en runtime: ningún [deps] los relaciona, igual que los componentes de la sesión.

Lo que costó cada decisión

Pin a 1.18.4 por gstreamer. Desde 1.20 el meson del frontend pide gstreamer-pbutils-1.0 sin required: —valida sonidos de notificación— y eso arrastra gstreamer + gst-plugins-base. El día que se quiera 1.22, ése es el precio.

El choque de gvdb. El portal vendoriza gvdb y glib también lo lleva adentro (GSettings): con glib estática el .a expone los símbolos y hay doble definición. Los dos caminos obvios se descartaron POR MEDICIÓN: --allow-multiple-definition zig-cc no lo acepta (unsupported linker arg, que meson reporta como Compiler cannot compile programs y manda a buscar donde no está), y glib-shared habría abierto la cadena entera (json-glib y gdk-pixbuf también enlazan glib). Se resolvió renombrando los diez símbolos del lector con -D: el renombrado es total porque todos los usos pasan por gvdb-reader.h, y el binario queda con lector+constructor de la MISMA copia — que importa, porque glib empaqueta sólo el lector.

libclang no salió de COSMIC. El backend declara pipewire/libspa-sys como deps duras y libspa-sys/build.rs corre bindgen incondicional, sin bindings pregenerados de reserva (a diferencia de aws-lc-sys, que sí cae a cargo:rustc-cfg=universal y por eso cruzó a musl sin cmake ni perl). clang18 NO recompila LLVM: el artefacto llvm18 ya publica headers y lib/cmake/llvm. Instala sólo libclang.so y clang-c/ — el compilador de esta distro sigue siendo zig-cc.

El parche de una línea: el backend fija cosmic-files con features = ["gvfs", "wayland"] a mano. El sed saca gvfs y queda la misma combinación que ya compilaron term y edit. Se pierden los montajes remotos dentro del selector del portal, no el selector.

⚠ Gotchas de andamiaje que costaron tiempo real

--offline SIN --locked cuando la receta parchea el manifiesto: el sed corre en la fase compile, o sea DESPUÉS del cargo vendor, así que cargo ve el Cargo.toml cambiado y con --locked se niega. Quitar una feature sólo ENCOGE el conjunto de crates; la hermeticidad la da --offline + el árbol vendorizado.

El worker NO puede construir la cola COSMIC. El snapshot golden es del 2026-07-15 y toda esta cola es posterior, así que allá nada es cache-hit: intentó reconstruir pipewire y murió buscando glib. Es la regla de «rootfs laptop ≠ worker» pero con el STORE. Corolario: mandar a la granja sólo lo que no dependa de artefactos recientes — clang18 sí (sus deps son viejas), el backend no.

farm-down.sh cosecha /opt/hammer/store, pero farm-up.sh ancla el store del worker en /mnt/cosecha/store. Son directorios distintos, NO el mismo por symlink: una cosecha normal no trae lo que el worker construyó ahí. Bajar el artefacto a mano ANTES de destruir nada.

Y no pedir un farm-down completo por un artefacto de 57 MB: el pull de los 616 artefactos del store horneado dejó 67 G en store/.dmerge y llenó /home al 100 %. (La protección sí funcionó: al fallar el pull, no destruyó el worker.)

pipewire no lleva --wrap-mode=nodownload ⇒ ante una dep faltante intenta bajarse un subproyecto de internet y, sin red, el error no dice «te falta glib» sino Unhandled python exception / This is a Meson bug. Deuda anotada.

Las cinco interfaces, sondeadas por D-Bus

docs/evidencia/cosmic-portal-interfaces-2026-08-05.png. Del lado del CLIENTE, contra org.freedesktop.portal.Desktop:

interfazversión
Screenshot2
FileChooser3
ScreenCast5
Settings2
Accessno existe, y está bien

org.freedesktop.portal.Access no es una interfaz de cliente: Access vive sólo como org.freedesktop.impl.portal.Access, o sea del lado del BACKEND — es como el frontend le pide al escritorio que muestre el diálogo de permiso. Pedirla al frontend devuelve No such interface, que es el comportamiento correcto y no un hueco. (El sondeo inicial la pidió mal; el error era del sondeo, no del portal.)

Del lado del BACKEND, contra org.freedesktop.impl.portal.desktop.cosmic: Screenshot 2, ScreenCast 4, Settings 2, y Access + FileChooser responden UnknownProperty 'version'. Ese error prueba que la interfaz ESTÁ: si faltara, D-Bus contestaría No such interface; UnknownProperty significa que la encontró y que esa implementación no publica la propiedad version.

Y el ListNames del bus muestra org.freedesktop.impl.portal.desktop.cosmic activado junto a org.freedesktop.impl.portal.PermissionStore: el frontend levantó al backend por D-Bus activation, que es justamente el eslabón que el .service con @libexecdir@ sustituido hace posible.

⚠ Alcance honesto de esta medición: prueba DISPONIBILIDAD y ENRUTADO, no los flujos completos. La única interfaz ejercida de punta a punta —con fichero en disco— es Screenshot.

📂 FileChooser EJERCIDO — y por qué NO se puede desde cosmic-store

docs/evidencia/cosmic-portal-filechooser-2026-08-05.png y …-nav-…png.

Primero, el camino que NO existe. cosmic-store se compila con la feature xdg-portal, pero su ÚNICO uso del selector está en src/update.rs:505, dentro de Message::RepositoryAddDialog, y arranca con:

if backend_name == BackendName::FlatpakUser {
    #[cfg(feature = "xdg-portal")]
    ...file_chooser::open::Dialog::new()...

O sea que el diálogo sólo se abre para añadir un repositorio flatpak, y esta imagen no construye el backend flatpak (ver arriba: sólo pkgar). La feature está compilada y el punto de llamada es inalcanzable. Tener la feature encendida no es tener el flujo disponible.

El camino que sí ejerce la interfaz es llamarla directo, que además prueba los tres eslabones sin depender de qué aplicación la use:

dbus-send --session --print-reply --dest=org.freedesktop.portal.Desktop \
  /org/freedesktop/portal/desktop org.freedesktop.portal.FileChooser.OpenFile \
  string:"" string:Prueba dict:string:variant:handle_token,string:hammer1 &

Resultado medido: el frontend devuelve el objeto Request con el token que le pasamos (/org/freedesktop/portal/desktop/request/1_113/hammer3) y el backend dibuja el diálogo real — título Prueba, el FS de verdad (run, tmp, dev, proc, sys), panel de detalles (inode/directory, 963 items) y botones Cancel/Open. La navegación anda: doble clic en dev cambia la miga de pan a Filesystem › dev y lista sus 129 entradas; seleccionar y pulsar Open lo cierra.

⚠ Lo que NO quedó capturado, y por qué: la URI devuelta. El Response de la especificación de portales es una señal unicast al llamador, y dbus-send termina apenas recibe el method return, así que ya no está para recibirla; dbus-monitor con un match rule normal tampoco la ve. Capturarla pide un cliente que implemente el ciclo Request/Response — que es justo lo que hace ashpd dentro de las aplicaciones. El diálogo está probado; la entrega del fichero a la aplicación, no.

🎥 ScreenCast: dos bloqueos distintos, y el segundo es el que importa

Bloqueo 1 — dbus-send no puede manejarlo, al revés que FileChooser. El selector se abre con UNA llamada; ScreenCast necesita un apretón de manos de tres pasos con estado (CreateSession → SelectSources → Start), y la sesión queda atada a la conexión que la creó. Cada dbus-send es una conexión distinta que muere al terminar, así que el paso 2 nunca encuentra la sesión del paso

  1. No es un límite del portal sino de la herramienta: hace falta un cliente ashpd de verdad.

Lo que sí se midió con dbus-send: CreateSession funciona — el frontend acepta y devuelve /org/freedesktop/portal/desktop/request/1_106/sc1, con el handle_token que le pasamos.

Bloqueo 2 — NADIE ARRANCA EL DEMONIO DE PIPEWIRE, y éste no lo arregla ningún cliente. docs/evidencia/cosmic-screencast-sin-pipewire-2026-08-05.png:

$ ls /usr/bin/ | grep -i pipewire      →  pipewire, pipewire-aes67, pipewire-avb, pipewire-pulse
$ ps ax | grep -c pipewire             →  1     (el propio grep: NO hay demonio)

Los binarios están en la imagen —el backend del portal enlaza libpipewire-0.3.so— pero el demonio no corre: cosmic-start no lo lanza y no hay systemd de usuario que lo active. O sea que aunque se escribiera el cliente ashpd, Start no tendría con quién negociar el stream.

Arreglo: lanzarlo desde cosmic-start. (Para AUDIO además falta wireplumber, que ni siquiera está construido — ver la decisión de audio del proyecto.)

✅ HECHO: el demonio arranca y el portal se conecta a él (2026-08-05)

cosmic-start lanza /usr/bin/pipewire justo después de asegurar XDG_RUNTIME_DIR, y espera el socket, no el pid — la misma disciplina que el compositor, y por el mismo motivo: un demonio que muere al instante deja el pid «existiendo» un rato y esperar por él da un OK falso.

Va en ese punto del script a propósito: pipewire no necesita D-Bus para arrancar, sólo el runtime dir donde abre su socket. Ponerlo ahí lo hace válido para los dos modos —bare y session— sin duplicar el bloque.

Lo medido dentro de la VM, en este orden, porque cada paso responde una pregunta distinta:

== cosmic-qemu :: pipewire OK (socket pipewire-0, pid 189)   ← ¿abre socket?

$ pw-cli info 0
  type: PipeWire:Interface:Core/4      version: "1.2.7"      ← ¿SIRVE a un cliente?
  core.daemon = "true"    core.name = "pipewire-0"

$ pw-cli ls Factory | grep factory.name
  … "client-node" …                                          ← ¿tiene lo que ScreenCast usa?

$ pw-cli ls Client | grep application.name
  application.name = "pw-cli"
  application.name = "xdg-desktop-portal"                     ← ¡EL PORTAL YA ESTÁ CONECTADO!

La última línea es el veredicto. El frontend del portal aparece como cliente del demonio de pipewire — una conexión que antes no podía existir, y que es exactamente la que Start necesita para negociar el nodo del stream. client-node es la factory con la que el backend lo crea.

Y sin regresión, medido con la cadena entera: cosmic-screenshot desde cosmic-term levanta la UI interactiva del portal —un toolbar con los tres modos (región, ventana, pantalla), Capture y Save to Pictures—, y el click en Capture deja un PNG de 53.590 bytes en /root/Pictures/Screenshots/. Evidencia: docs/evidencia/cosmic-portal-ui-captura-2026-08-05.png. Esa UI no estaba documentada: el registro anterior sólo probaba la ruta no-interactiva.

El escritorio completo con pipewire corriendo: docs/evidencia/cosmic-escritorio-con-pipewire-2026-08-05.png (2414 colores distintos — el panel mutilado da 130).

Lo que sigue faltando, y son dos cosas separadas:

🔌 portal-probe: la SONDA, y hasta dónde llega ScreenCast de verdad (2026-08-05)

recipes/incoming-cosmic/portal-probe.toml → b3:af49d32d. La única receta de la campaña cuyo producto no es una pieza del escritorio sino un instrumento para medirlo.

Por qué no alcanzaba dbus-send, y por qué tampoco hacía falta ashpd

Los portales no son RPC: son asíncronos en dos tiempos. Cada método devuelve al instante un object path de Request, y el resultado llega DESPUÉS como señal Request.Response sobre ese path. dbus-send ya salió para entonces. Y hay un segundo muro, más duro: el portal ata la sesión al SENDER, así que CreateSession con un dbus-send y SelectSources con otro se rechazan por venir de nombres únicos distintos. Hace falta UNA conexión viva durante todo el intercambio.

ashpd era el camino obvio, pero el árbol fuente de una receta de código propio es el repo del propio takana (no hay modo path en [source]: sólo git y tarball) ⇒ habría sumado ~150 crates al Cargo.lock de la herramienta de build por una sonda de diagnóstico. En C sobre libdbus cuesta cero deps nuevas, sale estática (0 NEEDED) —un instrumento no debe depender de lo que mide— y sobre todo: el protocolo se ve. Una sonda cuyo valor es documentar un handshake no debería esconderlo tras una biblioteca que lo abstrae.

Detalle que la especificación regala y hay que usar: el path del Request se PREDICE (/…/request/<sender>/<token>, con sender = nombre único sin : y con .→_). Existe justo para poder suscribirse ANTES de llamar; esperar al path devuelto para recién ahí hacer AddMatch es una carrera real, no teórica.

Lo medido: el stream SE CREA, y el Response es lo único que falta

portal-probe version        ScreenCast v5 · Screenshot v2 · FileChooser v3 · Settings v2
                            Access y RemoteDesktop AUSENTES
portal-probe screencast
  [1/4] CreateSession   → Response 0, session_handle = /…/session/1_108/hammersess   ✓
  [2/4] SelectSources   → Response 0                                                 ✓
  [3/4] Start           → el backend ABRE su diálogo «Share your screen», con
                          miniatura EN VIVO del framebuffer y el output `Virtual-1`;
                          se elige, se pulsa Share… y NO llega Response en 60s        ✗

El log del backend (RUST_LOG=debug) da la línea exacta:

xdg_desktop_portal_cosmic::screencast_thread: state-changed 'Connecting' -> 'Paused'

Y pw-cli ls Node durante la espera da el veredicto independiente:

node.name   = "cosmic-screencast"
media.class = "Video/Source"

El nodo de video EXISTE en el grafo de pipewire. O sea: la cadena entera —cliente → frontend → backend → compositor → demonio de pipewire— funciona hasta crear y negociar el stream. Lo único que no ocurre es que el backend emita el Response de vuelta al cliente tras quedar en Paused.

Ese es un lugar muy distinto del que ocupaba el frente esta mañana («no hay demonio con quien negociar»). El siguiente paso es el Paused → Streaming, y la sospecha razonable es wireplumber: sin gestor de sesión nadie mueve el nodo de Paused, y el backend parece esperarlo. Es una hipótesis, no una medición — construir wireplumber la confirma o la mata.

Dos gotchas que costaron corridas

🔇 wireplumber: construido, corriendo… y NO era el bloqueo (2026-08-05)

recipes/incoming-cosmic/wireplumber.toml 0.5.15 → b3:b8f3baf0, selló a la primera. Arranca desde cosmic-start (wireplumber OK (pid 192) — hay gestor de sesión), se conecta al grafo (dos clientes en wpctl status) y wpctl pasa a mostrar una sección Video que antes no existía.

Y el Start de ScreenCast sigue sin Response a los 60s. Exactamente el mismo resultado.

La hipótesis de la sección anterior —«falta el gestor de sesión que mueva el nodo de Paused a Streaming»— queda MATADA por medición. Era mía y era razonable; no era cierta. Se deja escrita junto con su refutación porque una hipótesis descartada con evidencia vale más que una lista de sospechosos: acota el próximo paso a lo que queda, que es el propio backend.

El dato nuevo, que es por dónde seguir: con wireplumber corriendo, wpctl status lista Video → Streams vacío durante el handshake. O sea que el nodo cosmic-screencast que sí existe en pw-cli ls Node no llega a wireplumber como stream gestionado. El próximo movimiento es el log del backend con RUST_LOG=trace alrededor de screencast_thread, no otra dep.

🧊 LA CAUSA REAL DE ScreenCast: EGL POR SOFTWARE, no el portal (2026-08-05)

Con RUST_LOG=xdg_desktop_portal_cosmic=trace el log del backend tiene 5 líneas en total y la última es la de siempre:

INFO xdg_desktop_portal_cosmic::screencast_thread: state-changed 'Connecting' -> 'Paused'

Después del Paused el backend no escribe NADA MÁS — ni error, ni panic, ni progreso. O sea que el trace no aportó: screencast_thread no tiene más instrumentación. Buscar ahí estaba agotado.

La pieza que faltaba mirar era el compositor, que es quien tiene que entregar los frames. /tmp/cosmic-session.log:

15:19:02.301568Z WARN smithay::backend::egl::error: Erroneous EGL call didn't set EGLError
15:19:02.310127Z INFO …screencast_thread: state-changed 'Connecting' -> 'Paused'
                 ↑ 8 MILISEGUNDOS de diferencia

Y la correlación no es casual: el contador de Erroneous EGL sube de a dos por cada intento de screencast (uno al abrir el diálogo con su miniatura en vivo, otro al pulsar Share) y no se mueve en ningún otro momento. Seis acumulados tras tres intentos.

ls /usr/lib/dri/ da iris_dri.so, kms_swrast_dri.so, swrast_dri.so. En QEMU con virtio-gpu-pci (sin virgl) el que carga es kms_swrast: mesa por software, sin exportación dmabuf real. Y eso es exactamente lo que un stream de vídeo continuo necesita y una captura de una sola toma no — por eso cosmic-screenshot SÍ funciona (va por shm) y ScreenCast no.

El frente no está bloqueado por una receta, está bloqueado por el ENTORNO

Esto reencuadra todo el día: pipewire no era, wireplumber no era, el backend del portal no era. Las tres piezas están construidas, corriendo y haciendo su trabajo —el nodo cosmic-screencast existe en el grafo—, y el que no puede cumplir es el compositor sobre una GPU de software.

Es el MISMO muro que documenta el frente KDE («GBM/EGL de software; mesa es softpipe, falta llvmpipe o virgl»), llegando por otro camino.

No se puede verificar en este QEMU: qemu-system-x86_64 -display help da sólo none, gtk y sdl, y no existe virtio-gpu-gl-pci — este binario no se compiló con virgl, aunque libvirglrenderer.so.1 esté instalada en el host. Las dos salidas reales son:

  1. Metal, donde iris_dri.so (que YA está en la imagen) habla con la Intel de verdad. Es el viaje de [[metal-dual-desktop-en-curso]] y probaría ScreenCast de paso.
  2. Un QEMU con virgl (-device virtio-gpu-gl-pci -display egl-headless), que hoy hay que construir o instalar aparte.

Hasta entonces, lo honesto es que ScreenCast está verificado hasta donde el entorno permite: la cadena completa de D-Bus funciona, el stream se crea y se negocia, y el último tramo —los píxeles— depende de una GPU que esta VM no tiene.

Por qué es una receta propia y no la de GNOME

La de incoming-gnome está sellada y funciona, pero su binario NEEDea libglib-2.0.so.0, libgobject-2.0.so.0 y libpipewire-0.3.so.0 de la isla dinámica de GNOME: traerla tal cual metería una segunda glib y una segunda pipewire en la imagen. De las 19 deps, 14 dan hash idéntico y las que divergen divergen a propósito — sobre todo pipewire, que acá apunta a la de COSMIC (338d1d8c), la que el escritorio realmente arranca. Un gestor de sesión contra otra pipewire que la que corre no gestiona nada.

El cambio de fondo es glib → glib-shared. COSMIC usa la glib ESTÁTICA del corpus, que le alcanza a xdg-desktop-portal porque es un ejecutable. Acá no: wireplumber produce libwireplumber-0.5.so y 17 módulos, y libglib-2.0.a del corpus tiene 44.275 reubicaciones R_X86_64_32/32S ⇒ no es PIC y no entra en un .so. (Regla del frente GNOME: readelf -r ANTES de gastar el build — acá ahorró uno.)

La salida no fue una campaña nueva sino una medición: incoming-kde/glib-shared.toml copiada a esta cola resuelve al mismo hash (0584ce1f) y ya estaba sellada ⇒ cero rebuild. Idem pcre2-shared (f254abaa), lua (e6c35f98) y libelogind (f1a690b2). Las dos glib coexisten sin pisarse en el rootfs: .a y .so son ficheros distintos, y el portal conserva sus 3 NEEDED.

Cierre: escritorio-cosmic pasa de 84 a 89 recetas, 0 faltantes.

El -L/usr/lib que pkg-config no da

pkg-config omite los -L de directorios que considera estándar del sistema, asumiendo que el linker ya los busca. Cierto para un cc nativo; falso para zig cc cruzando a x86_64-linux-musl, que trae sus propias rutas. Resultado: --libs --static devuelve -ldbus-1 pelado y zig responde unable to find static system library 'dbus-1' … searched paths: none — un error que suena a «falta la librería» cuando lo que falta es la ruta.

Y de paso se corrigió un comentario desactualizado de cosmic-start que decía que cosmic-settings-daemon «todavía no se puede construir acá (arrastra pipewire)». Es falso hace tiempo: ese daemon está sellado y corre. Un comentario viejo manda a buscar un bloqueo que ya no existe, y eso cuesta igual que un bug.

🔩 EL VIAJE A METAL: qué hacer con el USB en la mano (2026-08-05)

scripts/cosmic/metal-desktop-image.sh → work/hammer-cosmic-metal.img, quemada a /dev/sda. No es «lo mismo pero en metal»: es la única forma de saber si ScreenCast está bien, porque el mesa del corpus se construye con -Dgallium-drivers=iris y en QEMU ese driver nunca se usaba.

Validado en OVMF ANTES de quemar, y con PANTALLA —la regla de oro que costó un USB en el primer viaje de KDE—, con -serial null para simular un metal sin puerto serie: arje-zero PID 1, las cuatro particiones montadas, el motd VISIBLE y el prompt / # en pantalla (o sea que el getty de tty1 hace su trabajo). El shell responde: iris_dri.so presente, /dev/dri/card0, y cosmic-start / portal-probe / wireplumber en PATH. Evidencia: docs/evidencia/cosmic-metal-boot-pantalla-2026-08-05.png.

La secuencia en la máquina física, en orden

ls /dev/dri                       # card0 + renderD128 ⇐ renderD128 es la novedad: NO existe con swrast
dmesg | grep -iE 'i915|drm'       # que i915 haya hecho modeset, no simpledrm
cosmic-start                      # el MISMO script que corre en QEMU
# …y desde cosmic-term, dentro de la sesión:
portal-probe version              # ScreenCast v5
portal-probe screencast           # ⇐ LO QUE VINIMOS A VER

El veredicto es el [4/4] OpenPipeWireRemote con su fd, no que el diálogo aparezca: eso ya pasaba en QEMU. Si Start devuelve Response: 0 y la sonda imprime ✓✓ HAY REMOTO DE PIPEWIRE, el frente cierra. Si vuelve a colgarse en Start, mirar /tmp/cosmic-session.log: si ya no aparece Erroneous EGL call, entonces la causa era otra y el diagnóstico de hoy estaba incompleto.

🩺 1er viaje físico: se congelaba en el USB, y NO estaba colgado (2026-08-05)

Booteando en otra máquina, la pantalla llegaba hasta esto y se quedaba ahí:

GPT: Use Parted to correct GPT errors      ← inofensivo: el backup GPT no está al final del USB
sdc: sdc1 sdc2 sdc3 sdc4
sd 6:0:0:0: [sdc] Attached SCSI removable disk

La prueba de qué pasaba es una AUSENCIA: con ignore_loglevel en el cmdline, montar una ext4 imprime EXT4-fs (sdc2): mounted filesystem en pantalla sí o sí. No aparecía ⇒ el root nunca se montó ⇒ el fallo estaba en el initramfs de pivote, no en el kernel ni en el escritorio.

Dos causas encadenadas, y la segunda es la que hizo perder el viaje:

  1. CARRERA contra la enumeración USB. El rdinit arranca apenas se desempaqueta el initramfs, pero el bus USB enumera asíncrono y tarda segundos (reset de hub, settling de 1 s por dispositivo, scan SCSI). El pivote reintentaba findfs LABEL=takana-root 5 veces con 1 s. En esa máquina el disco es sd 6:0:0:0 —séptimo host SCSI— y no llegó a tiempo. En QEMU el disco es virtio y está desde el instante cero, así que esta carrera NO SE PUEDE VER en validación: hay que arrancar la imagen como usb-storage para reproducirla. Es el rootwait que no podemos usar, porque el descubrimiento es por LABEL y no hay root= en el cmdline. Ahora espera 60 s.

  2. CEGUERA. El cmdline horneado es console=tty0 console=ttyS0,115200 … y el kernel hace /dev/console = la última console= ⇒ el serial. El exec sh de rescate del pivote nacía en un puerto serie que esa máquina no tiene: pantalla muerta, indistinguible de un kernel colgado. Es el MISMO bug que costó el 1er viaje de KDE, una capa más temprano: allá se arregló para después del switch_root (el tty1-getty) y se dio el caso por cerrado, pero el initramfs seguía ciego. Ahora el log del pivote y el marcador HAMMER-EFI-INIT-OK van también a /dev/tty0, y el rescate abre un shell en tty1 listando los bloques visibles.

La lección general (vale para toda imagen booteable, no sólo COSMIC): cada capa del arranque tiene que escribir a /dev/tty0, no al console heredado. Arreglar una sola capa deja el agujero tapado y la siguiente vuelve a caer.

Reproducir el escenario de metal sin salir del laptop:

qemu-system-x86_64 -m 4096 -enable-kvm -cpu host -display none -vga std -serial null \
  -drive if=pflash,unit=0,format=raw,readonly=on,file=/usr/share/edk2/x64/OVMF_CODE.4m.fd \
  -drive if=pflash,unit=1,format=raw,file=work/.efi-vars-usbtest.fd \
  -drive if=none,id=usbstick,format=raw,file=work/hammer-cosmic-metal.img \
  -device qemu-xhci,id=xhci -device usb-storage,bus=xhci.0,drive=usbstick \
  -qmp unix:/tmp/qmp.sock,server,nowait     # el veredicto es el screendump, no el log

De paso: el rebuild avisa ! sin takana musl ⇒ el CLI takana no está en la imagen y takana boot menu nunca corre. Igual lleva timeout 15, porque corre ANTES del exec de arje-zero y colgarse ahí deja un arranque sin PID1 ni pantalla — el || true protege del fallo, no del bloqueo.

🖥 2º viaje: la imagen forzaba SOFTWARE — no podía ganar ni en el mejor caso (2026-08-05)

Máquina: Dell OptiPlex 3060, Coffee Lake, UHD 630. Arrancó entero (arje-zero PID 1, hammerd, sshd escuchando) y cosmic-start no levantó el escritorio.

La GPU estaba perfecta. Del dmesg sacado del propio USB:

[2.570] i915 0000:00:02.0: [drm] Found coffeelake (device ID 3e92) integrated display version 9.00
[3.179] [drm] Initialized i915 1.6.0 for 0000:00:02.0 on minor 0
[3.875] fbcon: i915drmfb (fb0) is primary device
[3.949] Console: switching to colour frame buffer device 240x67

El Failed to load DMC firmware i915/kbl_dmc_ver1_04.bin es un warning: el propio kernel dice que sólo desactiva gestión de energía. No era la causa (se arregló igual, es gratis).

La causa era el script de arranque. cosmic-start-qemu.sh exportaba, sin condición:

export LIBGL_ALWAYS_SOFTWARE=1
export MESA_LOADER_DRIVER_OVERRIDE=kms_swrast

Correcto sobre virtio-gpu. Letal sobre Intel real: la mesa del corpus va con -Dgallium-drivers=iris -Dllvm=disabled y no existe ningún kms_swrast_dri.so ⇒ el override no degrada a software, hace fallar EGL. Y en el mejor caso habría sido peor: si hubiera arrancado, habría compuesto por software y ScreenCast habría fallado por el mismo motivo que en QEMU — o sea que el viaje no podía contestar la pregunta que lo motivaba.

El origen fue un comentario en metal-desktop-image.sh que afirmaba que el script «no tiene nada específico del emulador». El nombre del fichero decía lo contrario y se ignoró. Regla: un artefacto que se comparte entre QEMU y metal no puede llevar el entorno hardcodeado — se detecta. Hoy el modo de GL se decide con dos patas (driver DRM del kernel y .so de mesa presente) y la imagen falla el build si el script vuelve a forzar kms_swrast.

Y correr cosmic-start sobre la imagen de metal por PRIMERA VEZ destapó lo de fondo: le faltaba entera la preparación de sesión que sólo tenía qemu-desktop-image.sh. Sin grupo video no arranca seatd; sin usuario messagebus no arranca el bus de sistema; y sin bus de sistema no hay portal, o sea que portal-probe screencast no habría tenido con quién hablar ni con el GL perfecto. La imagen de metal se armó copiando el arranque de la de QEMU pero no su preparación.

Lección de método: validar que una imagen «llega al prompt» no valida nada del escritorio. Hay que correr cosmic-start en la imagen que se va a quemar, aunque el GL no se pueda ejercer en QEMU: los fallos de sesión (seatd, dbus, logind) sí se ven, y son los que cuestan los viajes.

Qué mirar si la pantalla queda negra

Es el fallo que ya nos pasó una vez y tiene una causa aburrida. En orden de probabilidad:

  1. No es un cuelgue, es el prompt invisible. Ya está arreglado (tty1-getty + el log del pivote a tty0), pero si vuelve: los printk van a las dos consolas y el userspace a una sola.
  2. ls /usr/lib/dri — si sólo hay iris_dri.so y la GPU no es Intel, no hay GL. Esta imagen es Intel-only a propósito; para otra máquina va la imagen dual de KDE.
  3. cosmic-comp sin DRM master: dmesg | grep -i drm y ls -l /dev/dri/card0. Sin seatd o sin el shim de logind el compositor no toma el master.

Gotchas del quemado, medidos

Lo que falta, en orden

  1. ScreenCast: en el USB, esperando el viaje físico. pipewire ✅, cliente persistente ✅ (portal-probe, b3:af49d32d), wireplumber ✅ (b3:b8f3baf0) — y ninguno era el bloqueo. La causa es EGL por software en el compositor (kms_swrast, sin dmabuf), medida por correlación de 8 ms entre el Erroneous EGL call de smithay y el Paused del backend. No es verificable en QEMU (sin virgl), así que la imagen de metal con iris ya está construida, validada en OVMF y quemada — ver la sección del viaje. Falta bootearla (acción física) y correr portal-probe screencast. Y portal-probe filechooser, que ya puede capturar la URI.
  2. Decidir gvfs (ver la corrección de arriba: las piezas ya están selladas, lo que falta es la decisión sobre la segunda glib). Destrabaría los montajes remotos de cosmic-files y su applet.
  3. Enchufar la tienda a .swm: servir org.freedesktop.PackageKit y encender la feature packagekit, que es Rust puro. Mismo patrón que arje-logind-compat.
  4. cosmic-greeter (hoy cosmic-session lo lanza y falla con ENOENT, sin consecuencias) y cosmic-osd con teclas de brillo/volumen reales.
  5. Metal: la imagen de QEMU y la de metal comparten todo menos el driver; el viaje físico es el mismo que el de metal-dual-desktop-en-curso.