# 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** (filtra `ScreenReader`, ver atajos) | `b3:e871eee4` | | `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 (filtra `ScreenReader`, ver atajos) | `b3:642919a5` | | `cosmic-notifications` | ✅ sellado | `b3:3e2a5478` | | `cosmic-launcher` | ✅ sellado | `b3:c17b4956` | | `cosmic-app-library` | ✅ sellado | `b3: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 primera | `b3:64a5cbaa` | | `gmp` 6.3.0 · `mpfr` 4.2.2 (cadena de la calculadora) | ✅ sellados a la primera | `b3:d99a0a5d` · `b3:13982d20` | | `libqalculate` 5.12.0 (`qalc`) | ✅ sellado — **pero el plugin del lanzador no lo puede usar, ver abajo** | `b3:6bb435a7` | **La cola está COMPLETA: 30 recetas, `escritorio-cosmic` 68/68.** **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í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", ""]`. 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 ```sh cd ~/hammer ./target/release/hammer --store store hash recipes/incoming-cosmic/.toml # dry-run, ~2ms ./target/release/hammer build recipes/incoming-cosmic/.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**: - **panel superior** (banda `(27,27,27)`, 32 px): botones *Workspaces* y *Applications* a la izquierda, reloj al centro, y a la derecha los applets de accesibilidad, audio, red, batería y encendido; - **dock inferior** centrado y redondeado, con lanzador, workspaces, biblioteca de aplicaciones y las entradas `.desktop` del rootfs; - fondo `(33,74,135)` = exactamente el `Color(Single((0.13,0.29,0.53)))` que escribe `cosmic-start`. 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): ```sh 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 hammer 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: ```jsonc // 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: | fichero | lo instala | qué liga | |---|---|---| | `…Shortcuts/v1/defaults` | `cosmic-comp` | tecla → acción | | `…Shortcuts/v1/system_actions` | `cosmic-settings-daemon` | acció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/.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`: empaquetado y andando, e inservible para el plugin `qalc` está sellado y **funciona**: `qalc -t '2+2'` → `4`, `-f fichero`, y el modo interactivo sobre una terminal calcula perfecto (`123 × 456 = 56088`). La cadena que baja desde una tecla —`Super` → cosmic-launcher → pop-launcher → plugin `calc` → `qalc` → **mpfr → gmp**— está entera en el catálogo. **Y aun así el lanzador sigue mostrando una fila vacía.** El plugin no le pasa la cuenta por argv: arranca `qalc` SIN expresión, con stdin y stdout en tubería, le ESCRIBE la expresión y cierra. Ese modo, en nuestro binario, no lee nada. Acotado con dos invocaciones que sólo difieren en cuándo llega el EOF: ```sh { printf '123*456\n'; sleep 3; } | qalc # LEE (aparece la pregunta de primer arranque) printf '123*456\n' | qalc # NO lee NADA: imprime el prompt y sale con rc=0 ``` O sea: **con el EOF ya pendiente en un stdin que no es terminal, `readline()` devuelve NULL antes de entregar la línea que sí está en el búfer** — y así es exactamente como lo invoca el plugin. Lo que ya se DESCARTÓ midiendo, para que nadie lo repita: - **No es el toolchain.** Una sonda estática musl del propio sandbox (C y C++) lee la tubería sin problema. El binario no tiene nada raro: un solo `NEEDED`, `libc.so`. - **No es readline vs no-readline.** Sin readline es peor (no lee nunca, ni con la tubería abierta); con readline al menos lee cuando el escritor no ha cerrado. - **No es la versión de readline.** Se construyó una 8.3 como sombra de cola y el síntoma no se movió; la sombra se descartó en vez de dejar una receta duplicada que no arregla nada. - **No es el descriptor 0.** `qalc -f /dev/stdin` —el MISMO pipe, reabierto por ruta— sí funciona. El que no sirve es el `FILE*` `stdin`. La pista que queda es justamente esa última asimetría. Y el aviso de método: **empaquetar el binario que falta no garantiza que el consumidor lo pueda usar** — «qalc no está instalado» y «qalc está y no contesta» son dos estados distintos, y el segundo se ve peor porque la fila sale en blanco. ### ⏱ 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**. No es carga del anfitrión (falla con el anfitrión ocioso) sino la estampida del arranque: el panel registra su servidor D-Bus mientras catorce clientes de iced escanean fuentes al mismo tiempo. **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.** ## Lo que falta, en orden 1. **Aplicaciones.** No hay ni una: los 20 `.desktop` de la imagen son `NoDisplay=true`. El primer candidato barato es `libqalculate` (el binario `qalc`), que es lo único que le falta al plugin de la calculadora para dar un resultado en vez de decir que no está. 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`.