tawasuyu

sergio/takana

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

TOML 1572Shell 203Rust 109Markdown 993728 Definiciones

⎇ cd1497d740b1b6e2d7be231b005831cea02525d6 docs/runbooks/cosmic-desktop.md Markdown · 391 líneas · 24.5 KB Historia crudo

🏔 cosmic: EL ESCRITORIO ENTERO DIBUJADO — panel, dock y applets en QEMU cd1497d74 · 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 hammer 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 hammer 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 hammer, 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 `hammer 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** | `b3:b8abc52f` |
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 | `b3:0263288e` |
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
110**La cola está COMPLETA: 26 recetas, `escritorio-cosmic` 62/62.**
111
112**El mínimo viable de una sesión son cuatro**: `cosmic-comp` + `cosmic-settings-daemon` +
113`cosmic-notifications` + `cosmic-panel` (los tres `.expect` de la tabla de arriba).
114
115**El orden no es el de la lista de arranque.** `cosmic-bg` va antes que el panel a propósito: habla
116Wayland con `smithay-client-toolkit` y **no toca libcosmic ni iced**, mientras que panel, launcher,
117osd y workspaces son todos clientes de iced. Con el fondo sellado, «pinta el fondo pero no el panel»
118separa la capa de UI del transporte; sin él, las dos preguntas llegan juntas.
119
120### `cosmic-settings-daemon`: por qué queda al final y NO se resuelve copiando
121
122Es el único con cadena propia. Su workspace incluye `audio-server` como dep de path **no opcional**,
123que arrastra `cosmic-pipewire` → `pipewire-sys` → `libpipewire-0.3` por pkg-config. Y `pipewire` hoy
124vive sólo en las colas GNOME y KDE.
125
126**Acá el truco de copiar deja de ser gratis, y eso corrige lo que dice más arriba.** La regla completa
127es: *copiar una receta entre colas es gratis sólo si **toda su clausura transitiva** resuelve a lo
128mismo.* Se cumplió con `libdisplay-info`, `hwdata` y `nasm` —sus deps están todas en el corpus—, y
129**no** se cumple con pipewire: de sus catorce deps, cinco viven sólo en colas ajenas
130(`dbus-shared`, `alsa-lib`, `libsndfile`, `pulseaudio`, `zlib-shared`) y —lo decisivo— **`glib`
131resolvería distinto**. Desde `incoming-gnome`, `glib` es la sombra de esa cola (`b3:f6ccdf98`); desde
132`incoming-cosmic` sería la del corpus (`b3:93d2cad0`). Son recetas distintas, no la misma en dos
133lugares.
134
135O sea que copiar produciría un **segundo pipewire** construido contra otra glib, arrastrando su cadena
136entera. Y eso no es sólo caro: es exactamente el cuadro que la campaña GNOME ya midió y evitó —dos
137glib distintas con 370 rutas solapadas y dos registros de GType en un proceso—.
138
139Antes de tocar nada se midió con `yupana radio` (la puerta única, nunca `grep`): `pipewire` tiene
140radio 1 en GNOME y 2 en KDE, pero `glib` del corpus tiene **21 dependientes transitivos y afecta la
141imagen escritorio-gnome**.
142
143**Y después se midió el costo real, que resultó mucho menor que la estimación.** Copiar pipewire a
144`incoming-cosmic` necesita **cuatro** recetas hermanas —`alsa-lib`, `dbus-shared`, `libsndfile`,
145`pulseaudio`—, no las seis contadas a ojo, y **tres de ellas dan hash idéntico y ya están selladas**.
146Sólo `pulseaudio` y `pipewire` hay que construir.
147
148Eso refina la advertencia de arriba en el punto que importa: **el peligro de las dos glib es
149mezclarlas en UNA IMAGEN**, y la imagen COSMIC tiene una sola. Acá el costo es de builds, no de
150corrección. Lo que sigue vigente es no promover a ciegas entre colas.
151
152Gotcha del copiado, que cuesta un minuto de confusión: `[source] patches` se resuelve **relativo a la
153receta**, así que copiar el `.toml` sin su `.patch` da «no pude leer patch» y el hash ni siquiera se
154calcula.
155
156## 🚪 EL GATE PASADO: cosmic-comp compila (2026-08-03)
157
158`b3:b8abc52f`, 51 min 39 s de cargo con `CARGO_BUILD_JOBS=1` y `lto = "fat"`. **A la primera**, salvo
159el `--bin` del gotcha 1. Sin un solo parche a la fuente.
160
161Lo que dice el enlace es la parte que vale, porque es la medición y no la impresión:
162
163```
164NEEDED: libdisplay-info.so.2 · libgbm.so.1 · libseat.so.1 · libudev.so.1
165        libinput.so.10 · libpixman-1.so.0 · libxkbcommon.so.0 · libc.so
166```
167
168Ocho, y las siete primeras son **exactamente** los backends de smithay que la receta declara. La
169octava es musl. **No hay `libgcc_s`** — o sea que el compositor no arrastra la deuda del unwinder que
170sí tienen varias recetas Cargo del corpus, porque acá el enlace es dinámico contra musl y no hay
171`+crt-static` que forzar. Y **no aparece nada que no se haya declarado**: la clausura de build y la de
172runtime coinciden, que es la propiedad que uno quiere y casi nunca se cumple gratis.
173
174Que un compositor Wayland completo —DRM, GBM, EGL, libinput, seat, Vulkan, XWayland, un renderer
175multi-GPU y una capa de UI en iced— cierre con ocho librerías compartidas y ningún parche es el
176resultado más limpio de las cuatro campañas de escritorio.
177
178## 🖥 EL PRIMER ARRANQUE: qué falló y qué no (2026-08-03)
179
180Tres imágenes y tres causas distintas, todas medidas. Vale dejarlas porque **ninguna estaba en el
181camino que uno anticipa**, y las tres se manifestaron a varias capas de distancia de su causa.
182
183**1. `cosmic-session` no es opcional en el sentido que yo asumí — y `cosmic-settings-daemon` tampoco.**
184El compositor arrancó perfecto y le devolvió a la sesión su `WAYLAND_DISPLAY=wayland-1`; el gestor
185entonces intentó lanzar `cosmic-settings-daemon`, no lo encontró, y **panickeó**:
186
187```
188INFO  cosmic_session: got environmental variables from cosmic-comp: [("WAYLAND_DISPLAY", "wayland-1")]
189ERROR panic: 'failed to start settings daemon: NotFound' src/main.rs:255
190```
191
192Es un `.expect()`, sin feature que lo apague. **Y NO es el único — son TRES**, y esto corrige lo que
193esta misma sección afirmaba hace unas horas:
194
195| línea | componente | qué pasa si falta |
196|---|---|---|
197| `main.rs:255` | `cosmic-settings-daemon` | panic |
198| `main.rs:306` | `cosmic-notifications` | panic |
199| `main.rs:325` | `cosmic-panel` | panic |
200
201El resto (app-library, launcher, workspaces, osd, bg, idle) va por `start_component()`, que ante un
202fallo sólo hace `error!("failed to start …")` y sigue (`main.rs:558`). O sea que **el mínimo viable de
203una sesión COSMIC son cuatro binarios**: compositor + esos tres.
204
205**Cómo se equivocó el diagnóstico, que es la parte reusable**: leí `start_component()`, vi que sólo
206logueaba, y generalicé a «todos menos el settings-daemon». Pero notifications y panel **no pasan por
207esa función** — tienen su propio `.expect()` unas líneas antes. Buscar la función que lanza no es lo
208mismo que buscar los `\.expect(` del fichero; **grepear la construcción que mata cuesta lo mismo y no
209generaliza de más**. **Esto corrige lo que decía la sección de
210cosmic-settings-daemon**: no es que sin él se pierdan el brillo y el tema, es que **no hay sesión**.
211Queda por eso el modo `bare` en `cosmic-start` —el camino que upstream ya soporta como
212`install-bare-session`—: se lanza el compositor solo y los clientes a mano contra su WAYLAND_DISPLAY.
213Separa «¿compone?» de «¿arranca la sesión?», que son dos preguntas y estaban viniendo juntas.
214
215**2. La pantalla negra era `libz.so.1`.** El corpus trae `zlib` estática, que le alcanza a todo el
216cierre de COSMIC. Pero `kms_swrast_dri.so` es un `.so` que mesa abre con `dlopen` y NEEDea
217`libz.so.1`:
218
219```
220MESA-LOADER: failed to open kms_swrast: Error loading shared library libz.so.1
221WARN  cosmic_comp::backend::kms: Failed to add device /dev/dri/card0: Failed to initialize GBM device
222```
223
224Sin driver de software no hay GBM, y **el compositor arranca igual y presenta una pantalla negra**. La
225causa está a tres capas del síntoma. Se arregla agregando `zlib-shared` como raíz del hidratador.
226
227**3. Los clientes NO pueden ser estáticos.** `cosmic-bg` moría con «The wayland library could not be
228loaded» **teniendo `libwayland-client.so.0` en el rootfs**. `wayland-client` entra con la feature
229`dlopen`: carga libwayland en runtime en vez de enlazarla, y **un binario musl estático no tiene
230dlopen funcional**. No faltaba la librería: faltaba poder abrirla. Todos los clientes pasan a
231`link = "dynamic"`.
232
233**Lo que NO falló, y conviene decirlo:** el compositor no necesitó un solo parche, ni una perilla de
234KMS, ni forzar modeset legacy. Las cuatro perillas de render por software heredadas de KDE y GNOME
235(`GBM_ALWAYS_SOFTWARE`, `kms_swrast`, `LP_NUM_THREADS=1`) alcanzaron. GNOME costó cinco muros para
236llegar a este punto; COSMIC costó tres, y ninguno era del compositor.
237
238### Dos carreras con /run, la misma causa
239
240`dbus-daemon --system` falló con «Failed to bind socket /run/dbus/system_bus_socket: No such file or
241directory» **aunque el directorio se creaba al principio del script**, y dbus-run-session se quejó
242por su lado de que `XDG_RUNTIME_DIR` no existía. Entre el arranque del getty y el uso pasan varios
243segundos en los que arje-zero sigue montando, y un tmpfs sobre `/run` se lleva puesto lo que hubiera.
244**Crear justo antes de usar** es la única forma de no depender de ese orden; comprobar al principio y
245usar al final es exactamente cómo se cuela una carrera — la misma forma del bug de PolicyKit1 que se
246cerró hoy en GNOME.
247
248## Gotchas medidos (no previstos)
249
2501. **`cargo rustc --` exige UN target.** La fase compile del lab pasa flags tras el `--`, y cualquier
251   paquete con binario + lib + ejemplos corta con «extra arguments to `rustc` can only be passed to
252   one target». Se arregla con `flags = ["--bin", "<nombre>"]`. Le pasa a cosmic-comp y a cosmic-bg
253   (workspace con `config/`), y le va a pasar a casi todos: **ponelo desde el principio.**
2542. **`start-cosmic` es bash de verdad, no sh.** Usa `mapfile`, `[[ ]]` y expansión indirecta
255   `${!var}`; con el busybox del rootfs no arranca. `bash` ya está en el corpus, pero **tiene que
256   entrar en la imagen** — y no lo pide ningún `[deps]`, así que no lo va a traer nadie por accidente.
257   Lo mismo con `dbus-run-session` (de `dbus`), que es lo que el script exec-uta al final.
2583. **`rust-toolchain.toml` con `channel = "1.93"` es inerte acá** — es una perilla de rustup, y el
259   sandbox tiene el rustc de Alpine (1.96) pelado. Conviene saberlo antes de «arreglarlo».
2604. **`cosmic.desktop` existe DOS veces** (en cosmic-session y en cosmic-comp) con contenido distinto
261   y en la misma ruta. El de cosmic-comp es el de la «bare session» y apunta a `/usr/bin/cosmic-service`,
262   que vive en la rama de systemd que acá no va. Se instala **sólo** el de cosmic-session; si entraran
263   los dos, quién gana depende del orden de hidratación y el que pierde es el bueno.
2645. **`libsystemd` no es systemd.** La feature `default = ["systemd"]` de cosmic-comp trae el crate
265   `libsystemd`, que es una **reimplementación en Rust** del protocolo, no un binding a la C. Y
266   `logind` habla `org.freedesktop.login1`, que acá lo sirve `arje-logind-compat`. No hay que apagar
267   nada: el nombre de la feature es el del protocolo, no el del programa que lo contesta.
2686. **Las features de smithay que asustan no cuestan deps de build**: `backend_vulkan` (crate `ash`),
269   `backend_x11` (`x11rb` con `dl-libxcb`) y `xwayland` cargan todo por **dlopen**. Son deuda de
270   RUNTIME —libxcb y vulkan-loader viven hoy sólo en la cola KDE—, no un muro de compilación.
2717. **Todo cliente de la suite lleva las MISMAS CUATRO deps: `libxkbcommon`, `pkgconf`,
272   `libudev-zero`, `libinput`** — incluso las piezas sin interfaz. Las tres primeras se descubrieron
273   de a una, un build por dep, hasta que la línea de enlace las mostró juntas:
274
275   ```
276   -Wl,-Bstatic  -ludev  -linput  -lxkbcommon …
277   ```
278
279   **El error dice la que falta primero; el COMANDO dice todas.** Leerlo entero la primera vez habría
280   ahorrado tres builds. Los tres llegan por la cadena de libcosmic —enumeración de monitores y de
281   dispositivos de entrada—, no por nada que el cliente haga a la vista.
282
283   Y el camino hasta ahí fue de razonamientos buenos y falsos, que es lo que vale dejar escrito:
284   - `cosmic-bg` fue sin `[deps]` porque «habla el protocolo en Rust y no toca C». Reventó en el
285     `build.rs` de `smithay-client-toolkit` pidiendo `xkbcommon.pc`: **sctk ENLAZA libxkbcommon**; el
286     que la dlopea es `winit`, que es otra capa. Hablar Wayland en Rust dice cómo viaja el byte, no
287     con qué se interpreta un teclado.
288   - `cosmic-idle` fue sin `[deps]` porque *ese* argumento ya no aplicaba —no usa sctk— y un demonio
289     de inactividad no interpreta teclas. Reventó en el ENLACE con `-lxkbcommon`, arrastrado por
290     **`cosmic-settings-config`**: de ahí sale la tabla de atajos y la usa **todo** componente de la
291     suite. La dep no viene de lo que el paquete hace, sino de la librería de configuración común.
292
293   La lección de método vale más que la dep: **la dep no se deduce de la función del paquete.** Está
294   escrita en el `build.rs` que panickea o en la línea de enlace, y las dos veces el error dijo
295   exactamente quién y por qué. El `link = "static"` sobrevive porque el artefacto de libxkbcommon
296   trae `.a` además de `.so`.
2978. **Una feature apagada en el paquete puede estar prendida por otro.** `cosmic-bg` declara `image`
298   con `default-features = false` y sin AVIF, y aun así compila `dav1d-sys`: **las features de cargo
299   se UNIFICAN en el grafo**. Se resolvió trayendo `dav1d` (b3:aab7a874) en vez de parchear el
300   Cargo.toml de upstream — si el grafo dice que sabe decodificar AVIF, que lo sepa de verdad.
301   Mismo modo de falla que un `.pc Requires` arrastrando una dep que nadie declaró.
302
3039. **`cosmic-wallpapers` no se puede empaquetar del tarball: está en git-lfs.** El default de
304   cosmic-bg apunta a `/usr/share/backgrounds/cosmic/orion_nebula_nasa_heic0601a.jpg`, y el archivo
305   de GitHub de ese repo pesa **20 KB** — son punteros LFS, no imágenes. Una receta escrita del modo
306   normal produciría un paquete de ficheros de texto que además *parecería* correcto: se instalarían
307   con el nombre bueno en la ruta buena. Mientras tanto `cosmic-start` escribe la config de USUARIO
308   con una fuente `Color`, que cosmic-bg soporta de fábrica — y va en el lanzador, no en el artefacto,
309   porque es política de la imagen de prueba y no algo que el paquete prometa.
310
31110. **`build.rs` contento ≠ linker contento: `cosmic-applets` necesita la libdbus COMPARTIDA.**
312    Los applets de red, bluetooth y status-area hablan D-Bus por `libdbus-sys` (FFI a la C, no zbus
313    como el resto de la suite). Declarar `dbus` a secas alcanza para que su `build.rs` pase —le basta
314    el `.pc`, que el paquete del daemon trae— y el fallo aparece **una hora después, en el enlace
315    final del multiplexor**: `unable to find dynamic system library 'dbus-1'`. El corpus sólo
316    empaqueta `libdbus-1.a`; el `.so` vive en `dbus-shared`. Son dos preguntas distintas y las
317    contestan dos herramientas distintas: pkg-config dice si la dep *existe*, el linker si hay algo
318    que *enlazar*. Era la única receta de la cola que pedía la estática.
319
32011. **`cosmic-workspaces` necesita `mesa`, y la dep sale de lo que el programa MUESTRA.** La vista de
321    espacios de trabajo dibuja miniaturas vivas de las ventanas; el compositor se las pasa como
322    DMA-BUF por `zcosmic-toplevel-info` y el cliente importa el fd con GBM ⇒ `-lgbm`. Es el único
323    cliente de la suite que toca el pipeline gráfico además del compositor, así que el juego fijo de
324    cuatro (`libxkbcommon`, `pkgconf`, `libudev-zero`, `libinput`) no alcanza. Corolario del gotcha
325    de siempre: la dep no se deduce de la familia del paquete, se lee de lo que hace.
326
32712. **DOS GPUs en QEMU = el panel dibuja en la pantalla que no mirás.** `-device virtio-gpu-pci` no
328    reemplaza a la VGA por defecto de QEMU: la SUMA. El invitado ve `card0` y `card1`, cosmic-comp
329    crea dos outputs (`Virtual-1` y `Unknown-1`), `cosmic-bg` pinta el fondo en ambos y el panel se
330    ancla en uno solo. El `screendump` captura el otro ⇒ **fondo perfecto, panel invisible, y ni un
331    solo error en el log**. Con `-vga none` el escritorio aparece entero a la primera.
332    **Y esto corrige un diagnóstico anterior de este mismo runbook**: cuando el panel selló y no
333    dibujó, lo atribuí a que le faltaban los applets. Los applets hacían falta —sin ellos el panel
334    está vacío— pero **no eran la causa de la pantalla lisa**; la causa era la segunda GPU, que ya
335    estaba ahí. Un síntoma puede tener dos causas suficientes y arreglar la primera no prueba nada:
336    la prueba es la pantalla, no el razonamiento.
337
33813. **`mkdir: not found` en `cosmic-start`, y la sesión arrancaba igual.** El getty entrega un PATH
339    sin `/usr/bin`, y en la imagen fundida `mkdir` sólo vive ahí (`/usr/bin/mkdir → coreutils`; la
340    base metal no tendió el symlink `/bin/mkdir → busybox` que sí tienen chmod, mount, sed y tee).
341    Los cuatro `mkdir -p` de la preparación fallaban y **no se notaba**, porque los directorios
342    venían horneados en la imagen. Es la forma más traicionera de bug: gratis hoy, fatal el día que
343    alguien cambie cómo se arma la imagen. Fix: exportar `PATH` como primera línea del script.
344
345## Cómo se construye
346
347```sh
348cd ~/hammer
349./target/release/hammer --store store hash recipes/incoming-cosmic/<pieza>.toml   # dry-run, ~2ms
350./target/release/hammer build recipes/incoming-cosmic/<pieza>.toml --store store
351```
352
353## 🏔 EL ESCRITORIO ENTERO, DIBUJADO (2026-08-04)
354
355`docs/evidencia/cosmic-panel-applets-qemu-2026-08-04.png` — 1280×800, **895 colores distintos**:
356
357- **panel superior** (banda `(27,27,27)`, 32 px): botones *Workspaces* y *Applications* a la
358  izquierda, reloj al centro, y a la derecha los applets de accesibilidad, audio, red, batería y
359  encendido;
360- **dock inferior** centrado y redondeado, con lanzador, workspaces, biblioteca de aplicaciones y
361  las entradas `.desktop` del rootfs;
362- fondo `(33,74,135)` = exactamente el `Color(Single((0.13,0.29,0.53)))` que escribe `cosmic-start`.
363
36448 procesos `cosmic-*` estables a los 30 s, sobre el kernel de hammer con arje-zero como PID1.
365Se reproduce así (headless y automatizable; **el `-vga none` no es opcional**, ver gotcha 12):
366
367```sh
368COSMIC_MODE=session bash scripts/cosmic/qemu-desktop-image.sh
369qemu-system-x86_64 -machine q35,accel=kvm -cpu host -smp 4 -m 4096 \
370  -drive if=pflash,format=raw,readonly=on,file=/usr/share/edk2/x64/OVMF_CODE.4m.fd \
371  -drive if=pflash,format=raw,file=/tmp/OVMF_VARS.fd \
372  -drive file=work/hammer-cosmic-qemu.img,format=raw,if=virtio \
373  -vga none -device virtio-gpu-pci,xres=1280,yres=800 \
374  -display none -serial file:/tmp/cosmic.serial -qmp unix:/tmp/qmp.sock,server,nowait
375# y después, por QMP: {"execute":"screendump","arguments":{"filename":"/tmp/shot.ppm"}}
376```
377
378El veredicto es **contar colores distintos del PPM**, no leer el log: con dos GPUs el log salía
379impecable y la pantalla estaba lisa.
380
381## Lo que falta, en orden
382
3831. **Entrada real**: el escritorio está dibujado pero nadie probó un click ni una tecla. `virtio-tablet`
384   + `virtio-keyboard` y verificar que el launcher abre (es lo que separa «pinta» de «se usa»).
3852. `xdg-desktop-portal-cosmic` — sin él no hay captura de pantalla, ni file-chooser, ni compartir.
3863. `cosmic-settings` (el panel de control) y `cosmic-files`: la suite tiene aplicaciones y el
387   catálogo todavía no.
3884. `cosmic-greeter` (hoy `cosmic-session` lo lanza y falla con ENOENT, sin consecuencias) y
389   `cosmic-osd` con teclas de brillo/volumen reales.
3905. Metal: la imagen de QEMU y la de metal comparten todo menos el driver; el viaje físico es el
391   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 hammer 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 hammer 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 hammer, 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 hammer 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 gateb3:b8abc52f
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✅ selladob3:0263288e
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

La cola está COMPLETA: 26 recetas, escritorio-cosmic 62/62.

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/hammer --store store hash recipes/incoming-cosmic/<pieza>.toml   # dry-run, ~2ms
./target/release/hammer 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 hammer 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 4 -m 4096 \
  -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 \
  -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.

Lo que falta, en orden

  1. Entrada real: el escritorio está dibujado pero nadie probó un click ni una tecla. virtio-tablet
    • virtio-keyboard y verificar que el launcher abre (es lo que separa «pinta» de «se usa»).
  2. xdg-desktop-portal-cosmic — sin él no hay captura de pantalla, ni file-chooser, ni compartir.
  3. cosmic-settings (el panel de control) y cosmic-files: la suite tiene aplicaciones y el catálogo todavía no.
  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.