🖥 cosmic: COMPONE EN QEMU — cosmic-comp presenta frames sobre el kernel de hammer

Pantalla completa en el gris de COSMIC (39,41,42) con el cursor dibujado, 134
colores distintos, validado CON PANTALLA (DISP=gtk + screendump).

Entra el andamiaje completo: hydrate-cosmic.sh (44 recetas, 0 faltantes),
qemu-desktop-image.sh y cosmic-start-qemu.sh.

Tres causas medidas en tres arranques, ninguna donde uno la busca:

1. cosmic-settings-daemon NO es opcional: cosmic-session lo lanza con .expect()
   (main.rs:255) y panickea si falta. Corrige lo que este runbook afirmaba. De
   ahi el modo bare del lanzador, que separa como compone de como arranca la
   sesion.
2. La pantalla negra era libz.so.1: kms_swrast_dri.so lo NEEDea y el corpus solo
   traia zlib estatica. Sin driver de software no hay GBM y el compositor arranca
   igual, negro. Causa a tres capas del sintoma.
3. Los clientes no pueden ser estaticos: wayland-client entra con la feature
   dlopen y un musl estatico no tiene dlopen funcional — moria 'The wayland
   library could not be loaded' CON el .so presente.

Y dos carreras con /run (dbus y XDG_RUNTIME_DIR) de la misma forma que el bug de
PolicyKit1 de hoy en GNOME: comprobar al principio y usar al final. Se crea justo
antes de usar.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-03 11:48:44 -04:00
co-authored by Claude Opus 5
parent 7e2d4cd1cd
commit ef4b18bc88
17 changed files with 615 additions and 17 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

+59 -3
View File
@@ -1,8 +1,11 @@
# Runbook — COSMIC, el cuarto escritorio
Estado al 2026-08-03: **el gate está pasado.** `cosmic-comp` —el compositor, la pieza de la que todo
lo demás es cliente— **compila y está sellado**, junto con `cosmic-session`, `cosmic-icons`,
`cosmic-bg` y `cosmic-idle`. Falta arrancarlo. Este documento se escribe mientras se hace, no después.
Estado al 2026-08-03: **COSMIC COMPONE EN QEMU.** `cosmic-comp` arranca sobre el kernel de hammer con
arje-zero como PID1, toma el DRM, expone `wayland-1` y **presenta frames**: pantalla completa en el
gris de COSMIC `(39,41,42)` con el cursor dibujado, 134 colores distintos. Falta que los clientes se
conecten. Este documento se escribe mientras se hace, no después.
![cosmic-comp en QEMU](../evidencia/cosmic-comp-qemu-2026-08-03.png)
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),
@@ -128,6 +131,59 @@ Que un compositor Wayland completo —DRM, GBM, EGL, libinput, seat, Vulkan, XWa
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. **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
@@ -19,7 +19,12 @@ sha256 = "39dfa91d807240585ece7794190229832cb0c48162d87a29d62fb6a43a5238e6"
[build]
compiler = "zig-cc"
target = "x86_64-linux-musl"
link = "static"
# ── DINÁMICO, Y NO ES UNA PREFERENCIA ──────────────────────────────────────────────────────────
# `wayland-client` entra con la feature `dlopen`: no ENLAZA libwayland-client, la carga en runtime.
# Un binario musl ESTÁTICO no tiene dlopen funcional, así que el cliente arranca y muere con
# «The wayland library could not be loaded» — con el .so presente en el rootfs, que es lo que hace
# el diagnóstico confuso: no falta la librería, falta poder abrirla.
link = "dynamic"
flags = ["--bin", "cosmic-app-library"]
[build.phases]
@@ -34,4 +39,7 @@ cp data/icons/com.system76.CosmicAppLibrary.svg /out/usr/share/icons/hicolor
'''
[deps]
build = ["libxkbcommon", "pkgconf"]
# libudev-zero: lo pide `libudev-sys`, que llega por la cadena de libcosmic (enumeración de
# monitores y de dispositivos de entrada). Es el tercer miembro fijo del juego de deps de todo
# cliente de la suite, junto con libxkbcommon y pkgconf.
build = ["libxkbcommon", "pkgconf", "libudev-zero"]
+6 -1
View File
@@ -28,7 +28,12 @@ sha256 = "12b30c60e15ac6c0f4b3eca8c37642145683e830833b429958222dfc395db177"
[build]
compiler = "zig-cc"
target = "x86_64-linux-musl"
link = "static"
# ── DINÁMICO, Y NO ES UNA PREFERENCIA ──────────────────────────────────────────────────────────
# `wayland-client` entra con la feature `dlopen`: no ENLAZA libwayland-client, la carga en runtime.
# Un binario musl ESTÁTICO no tiene dlopen funcional, así que el cliente arranca y muere con
# «The wayland library could not be loaded» — con el .so presente en el rootfs, que es lo que hace
# el diagnóstico confuso: no falta la librería, falta poder abrirla.
link = "dynamic"
# Un único target para `cargo rustc --` (ver el comentario de cosmic-comp): el workspace trae también
# la lib de config.
flags = ["--bin", "cosmic-bg"]
+6 -1
View File
@@ -22,7 +22,12 @@ sha256 = "f6eab0ed81bba6bd3a6dfb3ec78471a152852c59e6887a74206f4b59865df7c1"
[build]
compiler = "zig-cc"
target = "x86_64-linux-musl"
link = "static"
# ── DINÁMICO, Y NO ES UNA PREFERENCIA ──────────────────────────────────────────────────────────
# `wayland-client` entra con la feature `dlopen`: no ENLAZA libwayland-client, la carga en runtime.
# Un binario musl ESTÁTICO no tiene dlopen funcional, así que el cliente arranca y muere con
# «The wayland library could not be loaded» — con el .so presente en el rootfs, que es lo que hace
# el diagnóstico confuso: no falta la librería, falta poder abrirla.
link = "dynamic"
flags = ["--bin", "cosmic-idle"]
[build.phases]
+10 -2
View File
@@ -19,7 +19,12 @@ sha256 = "908cb0ef7e609ce0dec59f6d1cb4c249551883da42c957cd9dcb2e45892c7ec7"
[build]
compiler = "zig-cc"
target = "x86_64-linux-musl"
link = "static"
# ── DINÁMICO, Y NO ES UNA PREFERENCIA ──────────────────────────────────────────────────────────
# `wayland-client` entra con la feature `dlopen`: no ENLAZA libwayland-client, la carga en runtime.
# Un binario musl ESTÁTICO no tiene dlopen funcional, así que el cliente arranca y muere con
# «The wayland library could not be loaded» — con el .so presente en el rootfs, que es lo que hace
# el diagnóstico confuso: no falta la librería, falta poder abrirla.
link = "dynamic"
flags = ["--bin", "cosmic-launcher"]
[build.phases]
@@ -34,4 +39,7 @@ cp data/icons/com.system76.CosmicLauncher.svg /out/usr/share/icons/hicolor/s
'''
[deps]
build = ["libxkbcommon", "pkgconf"]
# libudev-zero: lo pide `libudev-sys`, que llega por la cadena de libcosmic (enumeración de
# monitores y de dispositivos de entrada). Es el tercer miembro fijo del juego de deps de todo
# cliente de la suite, junto con libxkbcommon y pkgconf.
build = ["libxkbcommon", "pkgconf", "libudev-zero"]
@@ -15,7 +15,12 @@ sha256 = "8c60dd4c2161a2b9d2314597e141c600da9882fe774858283303370b2409ead7"
[build]
compiler = "zig-cc"
target = "x86_64-linux-musl"
link = "static"
# ── DINÁMICO, Y NO ES UNA PREFERENCIA ──────────────────────────────────────────────────────────
# `wayland-client` entra con la feature `dlopen`: no ENLAZA libwayland-client, la carga en runtime.
# Un binario musl ESTÁTICO no tiene dlopen funcional, así que el cliente arranca y muere con
# «The wayland library could not be loaded» — con el .so presente en el rootfs, que es lo que hace
# el diagnóstico confuso: no falta la librería, falta poder abrirla.
link = "dynamic"
flags = ["--bin", "cosmic-notifications"]
[build.phases]
@@ -26,4 +31,7 @@ chmod 755 /out/usr/bin/cosmic-notifications
'''
[deps]
build = ["libxkbcommon", "pkgconf"]
# libudev-zero: lo pide `libudev-sys`, que llega por la cadena de libcosmic (enumeración de
# monitores y de dispositivos de entrada). Es el tercer miembro fijo del juego de deps de todo
# cliente de la suite, junto con libxkbcommon y pkgconf.
build = ["libxkbcommon", "pkgconf", "libudev-zero"]
+10 -2
View File
@@ -21,7 +21,12 @@ sha256 = "de1a409c95305059f0c79f1dff0089f7cf6aaf9b2b7201e982da9895d58ad4a3"
[build]
compiler = "zig-cc"
target = "x86_64-linux-musl"
link = "static"
# ── DINÁMICO, Y NO ES UNA PREFERENCIA ──────────────────────────────────────────────────────────
# `wayland-client` entra con la feature `dlopen`: no ENLAZA libwayland-client, la carga en runtime.
# Un binario musl ESTÁTICO no tiene dlopen funcional, así que el cliente arranca y muere con
# «The wayland library could not be loaded» — con el .so presente en el rootfs, que es lo que hace
# el diagnóstico confuso: no falta la librería, falta poder abrirla.
link = "dynamic"
flags = ["--bin", "cosmic-osd"]
[build.phases]
@@ -32,4 +37,7 @@ chmod 755 /out/usr/bin/cosmic-osd
'''
[deps]
build = ["libxkbcommon", "pkgconf"]
# libudev-zero: lo pide `libudev-sys`, que llega por la cadena de libcosmic (enumeración de
# monitores y de dispositivos de entrada). Es el tercer miembro fijo del juego de deps de todo
# cliente de la suite, junto con libxkbcommon y pkgconf.
build = ["libxkbcommon", "pkgconf", "libudev-zero"]
+10 -2
View File
@@ -20,7 +20,12 @@ sha256 = "66d8dfbc8aa62ff6a91e83e5bdcb5363c663aac5a58c111152b258054a815222"
[build]
compiler = "zig-cc"
target = "x86_64-linux-musl"
link = "static"
# ── DINÁMICO, Y NO ES UNA PREFERENCIA ──────────────────────────────────────────────────────────
# `wayland-client` entra con la feature `dlopen`: no ENLAZA libwayland-client, la carga en runtime.
# Un binario musl ESTÁTICO no tiene dlopen funcional, así que el cliente arranca y muere con
# «The wayland library could not be loaded» — con el .so presente en el rootfs, que es lo que hace
# el diagnóstico confuso: no falta la librería, falta poder abrirla.
link = "dynamic"
flags = ["--bin", "cosmic-panel"]
[build.phases]
@@ -32,4 +37,7 @@ cp -r data/default_schema/. /out/usr/share/cosmic/
'''
[deps]
build = ["libxkbcommon", "pkgconf"]
# libudev-zero: lo pide `libudev-sys`, que llega por la cadena de libcosmic (enumeración de
# monitores y de dispositivos de entrada). Es el tercer miembro fijo del juego de deps de todo
# cliente de la suite, junto con libxkbcommon y pkgconf.
build = ["libxkbcommon", "pkgconf", "libudev-zero"]
@@ -12,7 +12,12 @@ sha256 = "d7b54ce8dd229c6b76c2132b38d0814df656439bd4c014d04bd052143dcf4249"
[build]
compiler = "zig-cc"
target = "x86_64-linux-musl"
link = "static"
# ── DINÁMICO, Y NO ES UNA PREFERENCIA ──────────────────────────────────────────────────────────
# `wayland-client` entra con la feature `dlopen`: no ENLAZA libwayland-client, la carga en runtime.
# Un binario musl ESTÁTICO no tiene dlopen funcional, así que el cliente arranca y muere con
# «The wayland library could not be loaded» — con el .so presente en el rootfs, que es lo que hace
# el diagnóstico confuso: no falta la librería, falta poder abrirla.
link = "dynamic"
flags = ["--bin", "cosmic-workspaces"]
[build.phases]
@@ -25,4 +30,7 @@ cp data/com.system76.CosmicWorkspaces.svg /out/usr/share/icons/hicolor/scala
'''
[deps]
build = ["libxkbcommon", "pkgconf"]
# libudev-zero: lo pide `libudev-sys`, que llega por la cadena de libcosmic (enumeración de
# monitores y de dispositivos de entrada). Es el tercer miembro fijo del juego de deps de todo
# cliente de la suite, junto con libxkbcommon y pkgconf.
build = ["libxkbcommon", "pkgconf", "libudev-zero"]
+21
View File
@@ -0,0 +1,21 @@
# dejavu-fonts 2.37 — fuentes TTF. Qt ya no trae fuentes ("QFontDatabase: Cannot find font directory");
# sin ellas el escritorio renderiza sin texto legible. Data-only: copia los .ttf a /usr/share/fonts.
name = "dejavu-fonts"
version = "2.37"
[source]
tarball = "https://github.com/dejavu-fonts/dejavu-fonts/releases/download/version_2_37/dejavu-fonts-ttf-2.37.tar.bz2"
sha256 = "fa9ca4d13871dd122f61258a80d01751d603b4d3ee14095d65453b4e846e17d7"
[build]
compiler = "zig-cc"
target = "x86_64-linux-musl"
link = "dynamic"
[build.phases]
configure = "true"
compile = "true"
install = "mkdir -p /out/usr/share/fonts/dejavu && cp ttf/*.ttf /out/usr/share/fonts/dejavu/"
[deps]
build = []
@@ -0,0 +1,33 @@
# hicolor-icon-theme 0.18 — el tema de iconos BASE del estándar freedesktop, y nada más que su
# esqueleto: un `index.theme` y el árbol de directorios vacíos por tamaño y categoría.
#
# QUÉ ES Y POR QUÉ HACE FALTA: `hicolor` es el tema de FALLBACK obligatorio de la especificación de
# iconos. Cada aplicación instala sus iconos en `/usr/share/icons/hicolor/...`, y todo buscador de
# iconos (GTK, Qt, el shell) termina ahí cuando el tema activo no tiene lo que se pide. Sin su
# `index.theme` el directorio existe pero NO es un tema: la búsqueda no lo recorre y los iconos de
# las aplicaciones quedan invisibles aunque los ficheros estén en su sitio.
#
# No trae ni un icono propio, a propósito — es el contrato, no el contenido. De ahí que sea la receta
# más chica del frente y a la vez la que no se puede saltear.
#
# Ojo con la versión: hasta 0.17 era autotools; **0.18 pasó a meson** y el tarball ya no trae
# `configure` (ni `configure.ac`). Un `./configure` heredado de la receta vieja sale con 127.
name = "hicolor-icon-theme"
version = "0.18"
[source]
tarball = "https://icon-theme.freedesktop.org/releases/hicolor-icon-theme-0.18.tar.xz"
sha256 = "db0e50a80aa3bf64bb45cbca5cf9f75efd9348cf2ac690b907435238c3cf81d7"
[build]
compiler = "zig-cc"
target = "x86_64-linux-musl"
link = "dynamic"
[build.phases]
configure = "PYTHONPATH=/usr/lib/python3.12/site-packages meson setup output --prefix=/usr --buildtype=release --wrap-mode=nodownload"
compile = "PYTHONPATH=/usr/lib/python3.12/site-packages ninja -C output"
install = "PYTHONPATH=/usr/lib/python3.12/site-packages DESTDIR=/out meson install -C output --no-rebuild"
[deps]
build = ["meson", "samurai", "python3", "pkgconf"]
@@ -0,0 +1,23 @@
# xkeyboard-config 2.44 — datos de layouts de teclado XKB + `xkeyboard-config.pc` (variable xkb_base).
# plasma-desktop lo EXIGE en ConfigureChecks (pkg_get_variable xkb_base) y kwin/Qt-wayland lo usan en
# runtime (/usr/share/X11/xkb; sin él: "xkbcommon: failed to add default include path /usr/share/X11/xkb").
# Meson, sin compilación real (instala datos + .pc). nls off (evita gettext); sin libxml2 (validación).
name = "xkeyboard-config"
version = "2.44"
[source]
tarball = "https://www.x.org/releases/individual/data/xkeyboard-config/xkeyboard-config-2.44.tar.xz"
sha256 = "54d2c33eeebb031d48fa590c543e54c9bcbd0f00386ebc6489b2f47a0da4342a"
[build]
compiler = "zig-cc"
target = "x86_64-linux-musl"
link = "dynamic"
[build.phases]
configure = "meson setup output --prefix=/usr -Dnls=false -Dcompat-rules=false"
compile = "ninja -C output"
install = "DESTDIR=/out ninja -C output install"
[deps]
build = ["meson", "samurai", "python3", "pkgconf"]
+33
View File
@@ -0,0 +1,33 @@
# zlib-shared 1.3.1 — variante DINÁMICA de zlib para el escritorio KDE.
#
# El zlib canónico (recipes/zlib.toml) es `--static` (sólo libz.a, para el toolchain rust/mrustc). Pero
# el stack GUI dinámico (Qt/KDE/libpng/Mesa) linkea `libz.so.1` por SONAME en runtime, y ese `.so` no lo
# provee ningún artefacto sellado ⇒ el rootfs hidratado caía a la libz.so.1 del sysroot Alpine (fuga de
# soberanía, deuda static↔dinámico de Capa 0). Esta variante sella el `libz.so.1` soberano: los `.so` de
# KDE ya tienen NEEDED libz.so.1 (mismo soname), así que basta hidratar este artefacto — sin rebuild de KDE.
#
# Build shared: el configure de zlib aborta bajo zig cc ("compiler error reporting too harsh"), así que se
# fabrica el libz.so.1 a mano del libz.a PIC con --whole-archive. CLAVE (fix soberanía): se pasa el
# --version-script=zlib.map del propio tarball ⇒ el .so exporta los símbolos VERSIONADOS (ZLIB_1.2.0
# compressBound, ZLIB_1.2.3.4 inflateReset2, …). Qt6 (libQt6Core/Gui) fue enlazado contra un zlib
# versionado y NEEDED esos @tags; sin el mapa, un libz.so con nombres pelados no resolvía
# inflateReset2@ZLIB_1.2.3.4 al enlazar gwenview (--no-allow-shlib-undefined) y Qt caía en runtime a la
# libz.so.1 de Alpine (fuga). El .so versionado satisface consumidores versionados Y no-versionados.
# -fPIC/-O2/-fno-sanitize=undefined igual que la canónica (evita el runtime UBSan de zig).
name = "zlib-shared"
version = "1.3.1"
[source]
tarball = "https://github.com/madler/zlib/releases/download/v1.3.1/zlib-1.3.1.tar.gz"
sha256 = "9a93b2b7dfdac77ceba5a558a580e74667dd6fede4585b91eefb60f03b72df23"
[build]
compiler = "zig-cc"
target = "x86_64-linux-musl"
link = "dynamic"
[build.phases]
configure = "CFLAGS='-fPIC -O2 -fno-sanitize=undefined' ./configure --prefix=/usr --static"
compile = "make -j\"$(nproc)\" && ${CC:-zig cc} -shared -Wl,-soname,libz.so.1 -Wl,--version-script,zlib.map -Wl,--whole-archive libz.a -Wl,--no-whole-archive -o libz.so.1.3.1"
install = "make install DESTDIR=/out && install -Dm755 libz.so.1.3.1 /out/usr/lib/libz.so.1.3.1 && ln -sf libz.so.1.3.1 /out/usr/lib/libz.so.1 && ln -sf libz.so.1 /out/usr/lib/libz.so"
+197
View File
@@ -0,0 +1,197 @@
#!/bin/sh
# cosmic-start — lo que el console-getty ejecuta en vez de /bin/sh en la imagen COSMIC de QEMU.
# Hermano de gnome-start y plasma-start; hereda de ellos todo lo que ya está pago y sólo diverge en
# lo que COSMIC necesita.
#
# Es `/bin/sh` (busybox ash) a propósito, aunque `start-cosmic` de upstream sea bash: este script
# corre ANTES, prepara el entorno, y después le pasa la posta. Cuanto menos dependa, más temprano
# puede reportar.
set -u
say() { echo "== cosmic-qemu :: $*" | tee -a /dev/console /dev/ttyS0 2>/dev/null; }
dump() { echo "---- $1 ----" > /dev/ttyS0 2>/dev/null; cat "$1" > /dev/ttyS0 2>/dev/null; }
alive() { s=$(sed -n 's/^State:[[:space:]]*//p' /proc/$1/status 2>/dev/null); case "$s" in ""|Z*) return 1;; *) return 0;; esac; }
say "arrancando (pid $$)"
# ── FS virtuales y directorios de runtime ───────────────────────────────────────────────────────
mount -t proc none /proc 2>/dev/null
mount -t sysfs none /sys 2>/dev/null
mount -t devtmpfs none /dev 2>/dev/null
mkdir -p /dev/pts /dev/shm && mount -t devpts none /dev/pts 2>/dev/null && mount -t tmpfs none /dev/shm 2>/dev/null
# XDG_RUNTIME_DIR es OBLIGATORIO para Wayland: ahí viven el socket del compositor y los de los
# clientes. Sin él el compositor arranca y nadie puede conectarse — y el error que da es del cliente,
# no del compositor, que es lo que hace confuso el diagnóstico.
export XDG_RUNTIME_DIR=/run/user/0
mkdir -p "$XDG_RUNTIME_DIR" && chmod 700 "$XDG_RUNTIME_DIR"
# …y COMPROBARLO, porque el primer arranque falló justo acá y el error salía del sitio equivocado:
# dbus decía «XDG_RUNTIME_DIR /run/user/0 not available» y eso se lee como un problema de dbus. El
# `mkdir -p` de arriba no garantiza nada si /run no es escribible o si algo lo montó encima.
if [ -d "$XDG_RUNTIME_DIR" ] && [ -w "$XDG_RUNTIME_DIR" ]; then
say "XDG_RUNTIME_DIR OK ($XDG_RUNTIME_DIR)"
else
say "!! XDG_RUNTIME_DIR NO existe/no escribible — /run es: $(ls -ld /run 2>&1)"
say " montando tmpfs en /run"
mount -t tmpfs none /run 2>/dev/null
mkdir -p "$XDG_RUNTIME_DIR" && chmod 700 "$XDG_RUNTIME_DIR"
[ -w "$XDG_RUNTIME_DIR" ] && say " ✓ ahora sí" || say " ✗ sigue sin andar"
fi
mkdir -p /root/.config /root/.cache /var/lib/dbus /run/dbus /tmp && chmod 1777 /tmp
export HOME=/root SHELL=/bin/bash PATH=/usr/bin:/bin:/usr/sbin:/sbin
say "kernel $(uname -r) · drm: $(ls /dev/dri 2>/dev/null | tr '\n' ' ')"
# ── Render por software: la VM no tiene GPU ─────────────────────────────────────────────────────
# Las cuatro perillas vienen pagas de las campañas KDE y GNOME. `GBM_ALWAYS_SOFTWARE` + el driver
# `kms_swrast` son lo que hace que mesa acepte crear un device GBM sobre virtio-gpu sin acelerador;
# `LP_NUM_THREADS=1` evita que llvmpipe se coma los cuatro vcpus y deje al compositor sin planificar.
export GBM_ALWAYS_SOFTWARE=1
export LIBGL_ALWAYS_SOFTWARE=1
export MESA_LOADER_DRIVER_OVERRIDE=kms_swrast
export LP_NUM_THREADS=1
# COSMIC pinta con tiny-skia (libcosmic entra sin `wgpu`), pero el compositor SÍ usa EGL/GL por
# smithay para componer, así que las de arriba siguen haciendo falta.
export RUST_BACKTRACE=1
export RUST_LOG="${RUST_LOG:-info}"
# ── seatd: quién le da los devices al compositor ────────────────────────────────────────────────
# smithay entra con `backend_session_libseat`, y libseat tiene dos backends: hablar con un `seatd`
# corriendo, o —si el proceso ya es root— abrir los devices él mismo (`builtin`). Acá se levanta
# seatd de verdad en vez de usar el builtin, porque es el camino que también sirve fuera de QEMU y
# porque deja un log donde mirar. Si no sube, se cae al builtin: mejor un escritorio con andamio que
# ninguno, pero se dice en el serial para que no pase por bueno.
if [ -x /usr/bin/seatd ]; then
/usr/bin/seatd -g video >/tmp/seatd.log 2>&1 &
SEATD_PID=$!
sleep 1
if alive $SEATD_PID && [ -S /run/seatd.sock ]; then
say "seatd OK (pid $SEATD_PID, /run/seatd.sock)"
else
say "!! seatd no levantó — caigo a LIBSEAT_BACKEND=builtin (somos root)"; dump /tmp/seatd.log
export LIBSEAT_BACKEND=builtin
fi
else
say "!! sin seatd — LIBSEAT_BACKEND=builtin"
export LIBSEAT_BACKEND=builtin
fi
# ── bus de SISTEMA ──────────────────────────────────────────────────────────────────────────────
# cosmic-comp habla logind por zbus (sesión, inhibidores) y el bus de sesión lo abre después
# `start-cosmic` con dbus-run-session. El de sistema hay que levantarlo acá.
if [ -x /usr/bin/dbus-daemon ]; then
# ⚠ El mkdir de /run/dbus va ACÁ, no arriba con los demás. El primer arranque en modo bare 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: entre un punto y otro 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. Mismo motivo que el segundo mkdir de XDG_RUNTIME_DIR.
mkdir -p /run/dbus /var/lib/dbus
[ -f /var/lib/dbus/machine-id ] || dbus-uuidgen > /var/lib/dbus/machine-id 2>/dev/null
[ -f /etc/machine-id ] || cp /var/lib/dbus/machine-id /etc/machine-id 2>/dev/null
dbus-daemon --system --fork >/tmp/dbus-system.log 2>&1
sleep 1
[ -S /run/dbus/system_bus_socket ] && say "bus de sistema OK" || { say "!! sin bus de sistema:"; dump /tmp/dbus-system.log; }
else
say "!! sin dbus-daemon"
fi
# ── login1: el shim de arje ─────────────────────────────────────────────────────────────────────
# Mismo eslabón que en GNOME y KDE. cosmic-comp pregunta por su sesión; quien contesta
# `org.freedesktop.login1` acá es arje-logind-compat. Se ESPERA el nombre, no el pid: un pid no es un
# servicio, y esperar sólo al final deja pasar la carrera (lección de gnome-start, 2026-08-03).
if [ -x /usr/bin/arje-logind-compat ]; then
ARJE_LOGIN_STATE=1 /usr/bin/arje-logind-compat >/tmp/logind-compat.log 2>&1 &
say "arje-logind-compat lanzado (pid $!)"
i=0
while [ $i -lt 20 ]; do
if dbus-send --system --dest=org.freedesktop.DBus --print-reply /org/freedesktop/DBus \
org.freedesktop.DBus.NameHasOwner string:org.freedesktop.login1 2>/dev/null | grep -q "boolean true"; then
say "bus: org.freedesktop.login1 OK"; break
fi
i=$((i+1)); sleep 0.5
done
[ $i -lt 20 ] || { say "!! login1 NO apareció en 10s:"; dump /tmp/logind-compat.log; }
else
say "!! sin arje-logind-compat — cosmic-comp puede no hallar sesión"
fi
# ── COSMIC ──────────────────────────────────────────────────────────────────────────────────────
export XDG_CURRENT_DESKTOP=COSMIC XDG_SESSION_DESKTOP=COSMIC XDG_SESSION_TYPE=wayland
export XDG_SESSION_ID=1 XDG_SEAT=seat0 XDG_VTNR=1
export XDG_DATA_DIRS=/usr/share XDG_CONFIG_DIRS=/etc/xdg
# Y otra vez JUSTO ANTES de arrancar, no sólo al principio: entre el getty y este punto pasan varios
# segundos en los que arje-zero sigue montando cosas, y un tmpfs sobre /run se lleva puesto el
# directorio sin avisar. Comprobar al principio y usar al final es cómo se cuela una carrera.
mkdir -p "$XDG_RUNTIME_DIR" && chmod 700 "$XDG_RUNTIME_DIR"
[ -w "$XDG_RUNTIME_DIR" ] || say "!! XDG_RUNTIME_DIR se perdió entre el arranque y ahora"
# ── MODO: bare (default) o session ──────────────────────────────────────────────────────────────
# `cosmic-session` es el camino de producción, pero HOY no se puede usar: llama a
# `cosmic-settings-daemon` con `.expect("failed to start settings daemon")` (src/main.rs:255) y
# PANICKEA si no está — no es opcional ni está detrás de una feature. Y ese daemon todavía no se
# puede construir acá (arrastra pipewire; ver el runbook).
#
# El modo `bare` es la otra mitad que upstream ya soporta (`make install-bare-session`): el
# compositor se lanza SOLO y los clientes se levantan a mano contra su WAYLAND_DISPLAY. Sirve para lo
# que hace falta ahora, que es saber si el escritorio PINTA — y separa las dos preguntas: «¿compone?»
# es del compositor, «¿arranca la sesión?» es del gestor.
#
# COSMIC_MODE=session para probar el camino de producción cuando el daemon exista.
if [ "${COSMIC_MODE:-bare}" = session ]; then
say "lanzando cosmic-session vía start-cosmic (modo session)"
/bin/bash /usr/bin/start-cosmic --in-login-shell >/tmp/cosmic-session.log 2>&1 &
CS_PID=$!
else
say "lanzando cosmic-comp SOLO (modo bare; cosmic-session necesita settings-daemon)"
dbus-daemon --session --fork --print-address=1 >/tmp/dbus-session-addr 2>/tmp/dbus-session.log
DBUS_SESSION_BUS_ADDRESS=$(cat /tmp/dbus-session-addr 2>/dev/null)
[ -n "$DBUS_SESSION_BUS_ADDRESS" ] && export DBUS_SESSION_BUS_ADDRESS && say "bus de sesión OK" \
|| { say "!! sin bus de sesión:"; dump /tmp/dbus-session.log; }
/usr/bin/cosmic-comp >/tmp/cosmic-session.log 2>&1 &
CS_PID=$!
fi
# ── Reporte: el socket, no el pid ───────────────────────────────────────────────────────────────
# «Un pid no es un servicio» — la lección que costó dos iteraciones en GNOME. Lo que dice que el
# compositor está VIVO es que exista su socket Wayland, no que el proceso no haya muerto todavía.
i=0; WD=""
while [ $i -lt 60 ]; do
WD=$(ls "$XDG_RUNTIME_DIR" 2>/dev/null | grep -m1 '^wayland-[0-9]$')
if [ -n "$WD" ]; then
say "compositor OK ($WD) — cosmic-comp ES el display server"
break
fi
alive $CS_PID || { say "!! la sesión MURIÓ antes de crear el socket:"; dump /tmp/cosmic-session.log; break; }
i=$((i+1)); sleep 1
done
[ -n "$WD" ] || say "!! sin socket wayland en ${i}s:"
[ -n "$WD" ] || dump /tmp/cosmic-session.log
# ── Los clientes, contra el socket que el compositor haya elegido ───────────────────────────────
# En modo bare no hay quien los levante: `cosmic-session` es el que normalmente hace esto, y por eso
# cada uno recibe WAYLAND_DISPLAY explícito. Se lanza sólo lo que ESTÉ instalado — la cola COSMIC se
# construye por partes, y un `[ -x ]` de más es más barato que un arranque perdido.
if [ -n "$WD" ] && [ "${COSMIC_MODE:-bare}" != session ]; then
export WAYLAND_DISPLAY="$WD"
for c in cosmic-bg cosmic-panel cosmic-osd cosmic-notifications cosmic-app-library cosmic-launcher cosmic-idle; do
if [ -x "/usr/bin/$c" ]; then
"/usr/bin/$c" >"/tmp/$c.log" 2>&1 &
say "cliente $c lanzado (pid $!)"
sleep 1
fi
done
fi
# Estado periódico + el log completo al final de cada tramo: el dato que importa no es que siga vivo
# sino QUÉ componentes levantó la sesión.
n=0
while [ $n -lt 12 ]; do
sleep 15; n=$((n+1))
# `pgrep -c` de busybox devolvía 0 con el compositor VIVO y su socket abierto — o sea que el
# contador mentía. Se cuentan los /proc/*/comm, que es el dato del kernel y no una utilería.
say "STATUS +$((n*15))s: $(for c in /proc/[0-9]*/comm; do cat "$c" 2>/dev/null; done | grep -c '^cosmic') procesos cosmic · $(for c in /proc/[0-9]*/comm; do cat "$c" 2>/dev/null; done | grep '^cosmic' | sort -u | tr '\n' ' ')"
[ $n = 2 ] && { say "log del compositor (primer tramo)"; dump /tmp/cosmic-session.log; for l in /tmp/cosmic-bg.log /tmp/cosmic-panel.log; do [ -s "$l" ] && dump "$l"; done; }
done
say "fin de la ventana de observación"
dump /tmp/cosmic-session.log
exec /bin/sh
+23
View File
@@ -38,6 +38,29 @@ else
# Ni binario ni librería: DATOS. El tema de iconos de COSMIC — sin él el escritorio no se ve
# «sin iconos», se ve roto, y con el cursor por software ni siquiera hay puntero.
recipes/incoming-cosmic/cosmic-icons.toml
# …y el tema del que HEREDA (`Inherits=hicolor` en su index.theme). Que un tema herede de otro es
# una relación declarada en un fichero de datos, invisible para cualquier grafo de deps.
recipes/incoming-cosmic/hicolor-icon-theme.toml
# ── LO QUE PIDE `start-cosmic`, Y NO LO PIDE NINGÚN `[deps]` ────────────────────────────────
# bash de verdad: el script usa `mapfile`, `[[ ]]` y expansión indirecta `${!var}`, así que con el
# busybox de la base metal no arranca. Y termina en `exec dbus-run-session -- cosmic-session`,
# o sea que el bus de SESIÓN es parte del arranque, no un extra.
recipes/bash.toml
recipes/dbus.toml
# ── DATOS QUE UNA LIBRERÍA DEL CIERRE NECESITA EN RUNTIME ───────────────────────────────────
# libxkbcommon entra por el grafo (la enlazan comp y clientes) pero sus TABLAS no: sin
# `/usr/share/X11/xkb` no puede compilar un keymap y el escritorio queda sin teclado. Es el mismo
# patrón que los gschemas en GNOME — la librería es la interfaz, los datos son otro paquete.
recipes/incoming-cosmic/xkeyboard-config.toml
# Idem tipografías: nada del cierre trae una sola fuente, y sin fuentes el panel sale con tofu.
recipes/incoming-cosmic/dejavu-fonts.toml
# zlib COMPARTIDA. El corpus trae `zlib` estática, que le sirve a todo el cierre de COSMIC… y no
# al driver de mesa: `kms_swrast_dri.so` es un .so que se abre con dlopen y NEEDea `libz.so.1`.
# Sin ella mesa no carga NINGÚN driver de software, GBM falla, y el compositor arranca igual y
# deja la pantalla NEGRA — el fallo aparece a tres capas de distancia de la causa.
recipes/incoming-cosmic/zlib-shared.toml
)
fi
+154
View File
@@ -0,0 +1,154 @@
#!/usr/bin/env bash
# qemu-desktop-image.sh — imagen de disco EFI COSMIC para QEMU (virtio-gpu + GL por software), que
# auto-lanza cosmic-session. Hermano de scripts/gnome/qemu-desktop-image.sh y scripts/kde/…: comparte
# la base metal, la inyección de mesa-llvmpipe/LLVM/musl y el truco del console-getty.
#
# work/metal-rootfs base: arje-zero PID1 + busybox + kernel(EFI-stub) + firmware
# work/cosmic-rootfs cierre de COSMIC (lo arma scripts/cosmic/hydrate-cosmic.sh)
# + store/*-mesa-llvmpipe GBM/EGL de software (el cierre trae mesa con iris_dri, que es Intel real)
# + store/*-llvm18 libLLVM que llvmpipe JIT-linkea
# → work/cosmic-qemu-rootfs (hardlinks) → work/hammer-cosmic-qemu.img
#
# Uso: scripts/cosmic/qemu-desktop-image.sh
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"; cd "$ROOT"
BASE="${BASE:-work/metal-rootfs}"
COSMIC="${COSMIC:-work/cosmic-rootfs}"
MESA="${MESA:-$(ls -dt store/*-mesa-llvmpipe 2>/dev/null | head -1)}"
LLVM="${LLVM:-$(ls -dt store/*-llvm18 2>/dev/null | head -1)}"
MERGED="${MERGED:-work/cosmic-qemu-rootfs}"
IMG="${IMG:-work/hammer-cosmic-qemu.img}"
[ -d "$BASE" ] || { echo "falta $BASE" >&2; exit 1; }
[ -d "$COSMIC" ] || { echo "falta $COSMIC — corré scripts/cosmic/hydrate-cosmic.sh" >&2; exit 1; }
[ -n "$MESA" ] && [ -d "$MESA" ] || { echo "falta mesa-llvmpipe en store/" >&2; exit 1; }
echo "==> fundiendo rootfs por hardlinks (base metal + cierre COSMIC)"
rm -rf "$MERGED"
cp -al "$BASE" "$MERGED"
cp -al --remove-destination "$COSMIC"/. "$MERGED"/
. "$ROOT/scripts/lib/pid1-desde-store.sh"
inyectar_pid1 "$MERGED" "$BASE"
echo "==> parcheando mesa software desde $(basename "$MESA")"
# El cierre trae mesa con SÓLO iris_dri.so (Intel real). En QEMU no hay GPU: hace falta kms_swrast.
mkdir -p "$MERGED"/usr/lib/dri
cp -a --remove-destination "$MESA"/usr/lib/dri/. "$MERGED"/usr/lib/dri/ 2>/dev/null || true
for so in libglapi libgbm libEGL libGLESv2 libGL libgallium; do
cp -a --remove-destination "$MESA"/usr/lib/$so* "$MERGED"/usr/lib/ 2>/dev/null || true
done
ls "$MERGED"/usr/lib/dri/*swrast* >/dev/null 2>&1 && echo " ✓ kms_swrast instalado" || { echo " ✗ swrast no entró" >&2; exit 1; }
echo "==> inyectando libLLVM.18 (llvmpipe lo JIT-linkea)"
[ -n "$LLVM" ] && [ -d "$LLVM" ] || { echo " ✗ falta llvm18 en store/ (set LLVM=)" >&2; exit 1; }
cp -a --remove-destination "$LLVM"/usr/lib/libLLVM.so.18.1 "$MERGED"/usr/lib/
ln -sf libLLVM.so.18.1 "$MERGED"/usr/lib/libLLVM.so.18
ln -sf libLLVM.so.18.1 "$MERGED"/usr/lib/libLLVM.so
echo "==> inyectando el runtime C++ de Alpine (deuda CONOCIDA, no accidente)"
# libLLVM y mesa-llvmpipe se construyen con gcc ⇒ NEEDean libstdc++/libgcc_s. Nada de COSMIC los pide
# —el compositor cierra con ocho NEEDED y ninguno es de gcc—: esto es deuda de la cadena de mesa.
for lib in libgcc_s.so.1 libstdc++.so.6; do
src=".dev-fs/alpine/usr/lib/$lib"
if [ -r "$src" ]; then cp -a --remove-destination "$src"* "$MERGED"/usr/lib/ 2>/dev/null || true
else echo " ⚠ sin $lib ($src)"; fi
done
echo "==> inyectando musl — y los DOS sonames"
# La base metal es 100% estática (arje-zero+busybox). En musl el loader ES libc (mismo fichero). Van
# dos nombres porque conviven dos toolchains: zig-cc emite NEEDED `libc.so` y el gcc/clang de Alpine
# emite `libc.musl-x86_64.so.1`. Es la MISMA libc; sin el segundo symlink lo de gcc no carga.
MUSL="${MUSL:-/usr/lib/musl/lib/libc.so}"
[ -r "$MUSL" ] || { echo "no encuentro musl libc ($MUSL) — set MUSL=" >&2; exit 1; }
mkdir -p "$MERGED/usr/lib/musl/lib" "$MERGED/lib"
cp -a --remove-destination "$MUSL" "$MERGED/usr/lib/musl/lib/libc.so"
ln -sf /usr/lib/musl/lib/libc.so "$MERGED/lib/ld-musl-x86_64.so.1"
ln -sf /usr/lib/musl/lib/libc.so "$MERGED/usr/lib/libc.so"
ln -sf /usr/lib/musl/lib/libc.so "$MERGED/usr/lib/libc.musl-x86_64.so.1"
echo " ✓ libc.so + libc.musl-x86_64.so.1"
echo "==> inyectando arje-logind-compat (el login1 del fractal) + su política"
# cosmic-comp pregunta por su sesión igual que mutter y kwin. El daemon no entra por el cierre porque
# nadie lo declara como dep de build. Se elige por `hammer hash` —la receta de HOY—, no por el más
# reciente del store. Su política D-Bus viaja DENTRO del artefacto desde 2026-07-30.
if ALC="${ALC:-store/$(./target/release/hammer --store store hash recipes/arje-logind-compat.toml 2>/dev/null | tail -1 | sed 's/^b3://')-arje-logind-compat}"; [ -d "$ALC" ]; then
install -Dm755 "$ALC"/usr/bin/arje-logind-compat "$MERGED"/usr/bin/arje-logind-compat
install -d "$MERGED/usr/share/dbus-1/system.d"
cp -a --remove-destination "$ALC"/usr/share/dbus-1/system.d/. "$MERGED"/usr/share/dbus-1/system.d/ 2>/dev/null || true
echo "$(basename "$ALC" | cut -c1-12)"
else
echo " ✗ falta arje-logind-compat en store/ — cosmic-comp no hallará sesión" >&2; exit 1
fi
# Comprobación, que es lo que reemplaza a escribir la política acá: mover algo a un artefacto sólo es
# una mejora si se NOTA cuando falta.
[ -f "$MERGED/usr/share/dbus-1/system.d/org.freedesktop.login1.conf" ] \
&& echo " ✓ org.freedesktop.login1.conf (del artefacto)" \
|| { echo " ✗ falta org.freedesktop.login1.conf — re-sellá arje-logind-compat" >&2; exit 1; }
echo "==> grupo video (seatd lo exige por NOMBRE)"
# `seatd -g video` sale con «Could not find group by name 'video'» y no arranca. No es cosmético:
# sin seatd, libseat cae al backend `builtin`, que anda sólo porque acá todo corre como root — o sea
# que el escritorio funcionaría por un accidente de privilegios y no por el camino real. La base metal
# es tan mínima que no trae ni los grupos estándar.
if ! grep -q '^video:' "$MERGED/etc/group" 2>/dev/null; then
tmp="$MERGED/etc/group.new"
{ cat "$MERGED/etc/group"; echo "video:x:44:"; } > "$tmp"
chmod 644 "$tmp"; mv "$tmp" "$MERGED/etc/group"
echo " ✓ video:x:44"
fi
echo "==> usuario messagebus (el system bus hace setuid a él)"
for f in passwd:messagebus:x:81:81:messagebus:/dev/null:/sbin/nologin group:messagebus:x:81:; do
file="${f%%:*}"; line="${f#*:}"
if ! grep -q '^messagebus:' "$MERGED/etc/$file" 2>/dev/null; then
tmp="$MERGED/etc/$file.new"
{ cat "$MERGED/etc/$file"; echo "$line"; } > "$tmp"
chmod 644 "$tmp"; mv "$tmp" "$MERGED/etc/$file"
fi
done
for h in usr/libexec/dbus-daemon-launch-helper usr/lib/dbus-1.0/dbus-daemon-launch-helper; do
if [ -f "$MERGED/$h" ]; then
chown 0:81 "$MERGED/$h" 2>/dev/null || true
chmod 4750 "$MERGED/$h"
echo " ✓ setuid en /$h"
fi
done
echo "==> instalando /usr/bin/cosmic-start"
install -Dm755 scripts/cosmic/cosmic-start-qemu.sh "$MERGED/usr/bin/cosmic-start"
echo "==> auto-lanzar COSMIC: console-getty ejecuta cosmic-start (no /bin/sh)"
python3 - "$MERGED/ente/seed.card.json" <<'PY'
import json, sys, os
p = sys.argv[1]
c = json.load(open(p))
for g in c.get("genesis", []):
if g.get("label") == "console-getty":
argv = g["payload"]["Native"]["argv"]
argv[:] = [("/usr/bin/cosmic-start" if a == "/bin/sh" else a) for a in argv]
print(" console-getty argv →", argv)
tmp = p + ".new"
with open(tmp, "w") as f:
json.dump(c, f, indent=2)
os.chmod(tmp, 0o644)
os.replace(tmp, p)
PY
# /var/run → /run: arje-logind-compat busca el bus de sistema en la ruta LEGACY y dbus-daemon escucha
# en /run. Esta base metal es tan mínima que ni siquiera tiene /var/run; sin el enlace el daemon se va
# a modo idle y nunca registra login1.
ln -sfn /run "$MERGED/var/run"
mkdir -p "$MERGED/root/.config" "$MERGED/root/.cache" "$MERGED/run/user/0" "$MERGED/tmp"
chmod 1777 "$MERGED/tmp"
echo "==> rootfs fundido: $(du -sh "$MERGED" | cut -f1)"
need_mb=$(du -sm "$MERGED" | cut -f1)
ROOT_SIZE="${ROOT_SIZE:-$(( need_mb + 1536 ))}"
echo "==> ROOT_SIZE=${ROOT_SIZE} MiB"
export ROOTFS="$MERGED" IMG ROOT_SIZE
export ESP_MB="${ESP_MB:-128}" STORE_SIZE="${STORE_SIZE:-512}" STATE_SIZE="${STATE_SIZE:-768}"
exec ./scripts/install-image-efi.sh