Files
hammer/docs/runbooks/cosmic-desktop.md
T
sergioandClaude Opus 5 04146ec77b 🛍 cosmic: la tienda LISTA — el catálogo AppStream hubo que fabricarlo, y el <provides> la vaciaba
Validado con pantalla: «Popular apps» poblada (App Library, Files,
Launcher, Settings), «Recently updated», y la búsqueda `text` devolviendo
COSMIC Text Editor. 2564 colores sobre un escritorio completo.

DOS TRAMPAS ENCADENADAS, y la primera invalida lo que dije al escribir la
receta:

1. El backend `pkgar` NO lee /usr/share/metainfo/*.metainfo.xml —los
   ficheros que instala cada receta—. Lee el CATÁLOGO, o sea
   {/usr/share,/var/lib,/var/cache}/{swcatalog,app-info}/{xml,yaml}/, que en
   una distro normal genera `appstreamcli compose`. Sin ese fichero la
   tienda abre perfecta y no lista NADA. Lo genera ahora
   qemu-desktop-image.sh agregando los metainfo en un <components>: doce
   líneas de python, cero AppStream y cero glib.

2. Con el catálogo puesto listaba 2 de 7. Los metainfo que genera `xdgen`
   escriben <provides> con los envoltorios <mimetypes>/<binaries>, y el
   crate `appstream` espera los hijos PLANOS del esquema de catálogo: muere
   con «The tag provide doesn't have a value» y DESCARTA EL COMPONENTE
   ENTERO, callado. Los dos que se veían eran justo los que no tienen
   <provides>. Se elimina el bloque al agregar.

Cómo se diagnosticó, que es lo reutilizable: desde el lanzador la salida del
proceso no llega al log de la sesión. Hay que abrir cosmic-term DENTRO de la
VM y correr `rm -rf ~/.cache/cosmic-store; RUST_LOG=info cosmic-store`. El
rm no es opcional: la tienda cachea el parseo en bitcode y sin borrarlo
repite el resultado viejo.

Y el deadline del panel es una CARRERA, no un umbral: un arranque de esta
tanda salió mutilado (130 colores) con el anfitrión ocioso y -smp 8. Si sale
mutilado con carga baja, rearrancar antes de culpar al propio cambio.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 23:14:01 -04:00

48 KiB
Raw Blame History

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 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 — la calculadora del lanzador ANDA (ver abajo) b3:edb8d14d
cosmic-term 1.5.0 — la 1ª APLICACIÓN sellada a la primera (arrastra cosmic-files y wgpu) b3:d0ea1c1e
cosmic-files 1.5.0 — la 2ª aplicación sellada, sin gvfs ni wgpu (ver abajo) b3:48d2b072
cosmic-settings 1.5.0 — la 3ª aplicación sellada (necesitó el parche de la doble barra) b3:716dac53

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

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

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

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

Es el único con cadena propia. Su workspace incluye audio-server como dep de path no opcional, que arrastra cosmic-pipewirepipewire-syslibpipewire-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.

  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:

  • 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):

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:

// 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/<sha>.tar.gz), porque no hay tag que nombre esa rev. Selló a la primera (b3:64a5cbaa) con un solo NEEDED: libc.so.

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

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

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

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

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

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

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

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

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

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

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

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

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

Gotchas de empaquetado de la cadena:

  • gmp va con --disable-assembly. Elige rutinas en ensamblador mirando la CPU de quien compila, o sea hornearía la ISA del laptop en el artefacto y el hash dejaría de significar lo mismo en la granja. Es una calculadora de escritorio: la implementación en C portable sobra.
  • El configure de libqalculate no encuentra readline porque prueba -lreadline junto a -lncurses, -lcurses, -ltermcap y -ltinfo, y el corpus sólo empaqueta libncursesw.a. Que el paquete se llame ncurses no significa que provea -lncurses. Los alias van en un directorio LOCAL (.curses-compat/) y no en /usr/lib: esa vista es la evidencia de qué se declaró, y ensuciarla para que pase un test deja mintiendo justo al mecanismo que existe para no mentir. Ojo: hay que repetir el -L en el make, porque el LDFLAGS del make PISA al del configure.

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

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

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

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

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

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

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

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

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

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

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

  • gvfs trae los crates gio/glib, y el enlace final pide las glib compartidas (unable to find dynamic system library 'gio-2.0', y detrás gobject, glib, ffi, pcre2, z). El corpus las empaqueta sólo estáticas: haría falta una cadena -shared entera, que es una campaña aparte y mete una segunda glib en la imagen. Se pierden los montajes remotos (SFTP, MTP), no la navegación local. ⚠ Y por eso tampoco se construye cosmic-files-applet: su Cargo.toml fija gvfs a mano, así que apagarla en el paquete raíz no lo alcanza.
  • wgpu desbordó el filesystem: No space left on device creando un temp dir en /src/target, dos veces, con wgpu+naga+ash+glow+spirv a codegen-units=1. Sin él el árbol de build queda en ~2 GB. No se pierde nada: todos los demás clientes de la suite pintan con tiny-skia y la imagen no tiene GPU.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

El catálogo AppStream: dos trampas encadenadas

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

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

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

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

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

Lo que falta, en orden

  1. Más aplicaciones: cosmic-screenshot.
  2. La cadena glib COMPARTIDA (glib + pcre2 + libffi + zlib con .so), que destrabaría gvfs en cosmic-files y su applet del panel. Es una campaña propia y hay que decidir antes si la imagen puede tener una sola glib.
  3. xdg-desktop-portal-cosmic — sin él no hay captura de pantalla, ni file-chooser, ni compartir.
  4. cosmic-settings (el panel de control) y cosmic-files: la suite tiene aplicaciones y el catálogo todavía no.
  5. cosmic-greeter (hoy cosmic-session lo lanza y falla con ENOENT, sin consecuencias) y cosmic-osd con teclas de brillo/volumen reales.
  6. 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.