🖥 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
+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