# 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](../evidencia/cosmic-session-qemu-2026-08-03.png)

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](../evidencia/cosmic-bg-qemu-2026-08-03.png)

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`](../evidencia/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](mirada-usb-nvidia.md),
[KDE Plasma 6](kde-qemu-desktop.md) y [GNOME](gnome-qemu-desktop.md).

## 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 necesita | de dónde sale | costo |
|---|---|---|
| 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 |
| `libdisplay-info` + `hwdata` | copiadas byte a byte de la cola GNOME | **cero** — mismo hash, artefacto ya sellado |
| `bash`, `dbus-run-session`, `hicolor-icon-theme`, mesa-llvmpipe | corpus | cero |
| el andamiaje de arranque en QEMU (`run-qemu-desktop.sh`, base rootfs, console-getty, screendump) | campañas KDE/GNOME | cero |

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.

| pieza | estado | hash |
|---|---|---|
| `cosmic-session` | ✅ sellado | `b3: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** | `b3:b8abc52f` |
| `cosmic-bg` (dinámico, PINTA) | ✅ sellado | `b3:b2cfc410` |
| `cosmic-idle` (dinámico) | ✅ sellado | `b3:e1806acc` |
| `pulseaudio` · `pipewire` (cadena del settings-daemon) | ✅ sellados | `b3:e1ff4736` · `b3:338d1d8c` |
| `cosmic-panel` | ✅ sellado | `b3:ac5e48c5` |
| `cosmic-osd` | ✅ sellado | `b3:f15cb9c1` |
| `cosmic-settings-daemon` | ✅ sellado | `b3:0263288e` |
| `cosmic-notifications` | ✅ sellado | `b3:3e2a5478` |
| `cosmic-applets` · `cosmic-launcher` · `cosmic-app-library` · `cosmic-workspaces-epoch` | receta escrita, **build cortado a mitad** | — |

**Los cuatro últimos quedaron sin sellar porque se cortó la sesión de trabajo, no porque fallaran**:
los tres clientes venían compilando limpio y `cosmic-applets` ya había pasado el vendoreo y la dep de
`libdbus`. Se retoman con `hammer build recipes/incoming-cosmic/<pieza>.toml --store store` — no hay
nada que diagnosticar primero.

**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). Tres están
sellados; falta notifications.

Y `cosmic-applets` es aparte pero no opcional para VER algo: sin sus veinte symlinks el panel arranca,
dice «Done spawning applets» y no dibuja.

**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ínea | componente | qué pasa si falta |
|---|---|---|
| `main.rs:255` | `cosmic-settings-daemon` | panic |
| `main.rs:306` | `cosmic-notifications` | panic |
| `main.rs:325` | `cosmic-panel` | panic |

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.

## Cómo se construye

```sh
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
```

## Lo que falta, en orden

1. Cerrar `cosmic-comp`. Es el gate: sin compositor no hay escritorio que probar.
2. El primer cliente de iced (`cosmic-panel`), que es la pregunta abierta de verdad — libcosmic con
   `winit`/`wgpu` puede pedir más de lo que pide cosmic-bg.
3. `cosmic-settings-daemon`, `cosmic-launcher`, `cosmic-app-library`, `cosmic-notifications`,
   `cosmic-osd`, `cosmic-workspaces-epoch`, `cosmic-idle`.
4. Imagen QEMU con `bash` + `dbus-run-session` + `hicolor-icon-theme`, y **validar CON PANTALLA**
   (`DISP=gtk` + `screendump`, contar colores distintos) — regla heredada de KDE y GNOME.
5. `xdg-desktop-portal-cosmic` y `cosmic-greeter`, que son de la etapa siguiente.
