Files
takana/scripts/gnome/qemu-desktop-image.sh
T
Sergio 3d79cdf98b SDD 30 4c: GNOME con los demonios supervisados por arje — y el control arregló colord
Los demonios de sistema pasan de lanzarse con `&` desde un script de 500 líneas
a ser Cards del `genesis`. El bloqueo que este frente daba por corpus se levantó
solo: otro agente selló evolution-data-server y gnome-shell mientras esto se
escribía, y escritorio-gnome quedó 309/309.

SE CORRIÓ UN CONTROL PRIMERO, y es lo único que hace interpretable el resultado:

  colord              control: `MURIÓ al arrancar`   cards: `ya vive (pid 175)`
  ColorManager        control: `NO apareció en 40s`  cards: `OK`
  login1/Accounts/UPower  OK en los dos
  compositor          wayland-0 y shell vivo en los dos

El fallo del control DESAPARECIÓ, y no lo buscaba: colord moría arrancado por el
script y vive arrancado por arje, con el bus ya listo porque la espera está
dentro de su argv. Sin el control, «ColorManager OK» sería un dato suelto en vez
de una diferencia. Los PIDs lo confirman: polkit=119, colord=175, upowerd=178,
accounts=180 — de antes de que el lanzador de sesión existiera.

QUÉ NO SE COMPROBÓ: no hay screendump; QEMU salió por timeout y el control
tampoco lo tuvo. La comparación es serial contra serial y lo que se afirma es
sobre los DEMONIOS, no sobre el pintado.

Las piezas donde corresponde: `takana service-cards` (UNA sola implementación de
receta→Card; el formato es contrato con card_core::Card), `targets.py
--service-paths` (une qué-es con si-arranca), y un inyector en FICHERO APARTE
porque anidar dos heredocs de python falló en vivo — el terminador del interno
cerró el externo y media cosa corrió como shell.

La espera del bus va DENTRO del argv de las 5 recetas de sistema: sin ella un
daemon arranca antes de que dbus escuche y queda en modo idle sin registrar su
nombre — un fallo que no se ve, porque el proceso vive y el bus no lo tiene. Los
5 hashes intactos. Y el guardia de gnome-start es por «¿está corriendo?», no por
una perilla: así es correcto venga de donde venga el proceso y la misma copia
sirve donde no se inyectaron cards.
2026-09-14 01:51:19 +00:00

244 lines
14 KiB
Bash
Executable File

#!/usr/bin/env bash
# qemu-desktop-image.sh — imagen de disco EFI GNOME para QEMU (virtio-gpu + software GL), auto-lanza
# gnome-shell. Hermano de scripts/kde/qemu-desktop-image.sh; comparte con él la base metal, la
# inyección de mesa/LLVM/musl y el truco del console-getty, y sólo diverge en lo que GNOME necesita.
#
# work/metal-rootfs base: arje-zero PID1 + busybox + kernel(EFI-stub) + firmware
# work/gnome-rootfs cierre de gnome-shell (lo arma scripts/gnome/hydrate-gnome.sh, 108/108)
# + store/*-mesa-llvmpipe GBM/EGL de software (el rootfs GNOME sólo trae iris_dri, que es Intel real)
# + store/*-llvm18 libLLVM que llvmpipe JIT-linkea
# + store/*-dejavu-fonts el rootfs GNOME NO trae NINGUNA fuente
# → work/gnome-qemu-rootfs (hardlinks) → work/hammer-gnome-qemu.img
#
# Uso: scripts/gnome/qemu-desktop-image.sh
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"; cd "$ROOT"
BASE="${BASE:-work/metal-rootfs}"
GNOME="${GNOME:-work/gnome-rootfs}"
MESA="${MESA:-$(ls -dt store/*-mesa-llvmpipe 2>/dev/null | head -1)}"
LLVM="${LLVM:-$(ls -dt store/*-llvm18 2>/dev/null | head -1)}"
FONTS="${FONTS:-$(ls -dt store/*-dejavu-fonts 2>/dev/null | head -1)}"
MERGED="${MERGED:-work/gnome-qemu-rootfs}"
IMG="${IMG:-work/hammer-gnome-qemu.img}"
[ -d "$BASE" ] || { echo "falta $BASE" >&2; exit 1; }
[ -d "$GNOME" ] || { echo "falta $GNOME — corré scripts/gnome/hydrate-gnome.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 GNOME)"
rm -rf "$MERGED"
cp -al "$BASE" "$MERGED"
cp -al --remove-destination "$GNOME"/. "$MERGED"/
. "$ROOT/scripts/lib/pid1-desde-store.sh"
. "$ROOT/scripts/lib/libs-compartidas-desde-store.sh"
inyectar_pid1 "$MERGED" "$BASE"
inyectar_libs_compartidas "$MERGED"
echo "==> parcheando mesa software desde $MESA"
# El cierre GNOME trae mesa con SÓLO iris_dri.so (Intel real). En QEMU no hay GPU: hace falta
# kms_swrast. Se copian los .so de mesa-llvmpipe ENCIMA (--remove-destination rompe el hardlink).
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. Y además —esto es
# propio de GNOME y quedó documentado al auditar el cierre— **libmozjs-128.so también los NEEDea**,
# o sea que la cadena gnome-shell → libgjs → libmozjs arrastra el runtime C++ de Alpine. Es la
# "última milla" de matar-gcc que se cerró para cmake con -static-libstdc++ y que spidermonkey no
# cubrió. Mientras eso no se re-selle (radio medido: 2), la imagen los inyecta o el shell no carga.
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) y el cierre GNOME no trae libc. En musl el
# loader ES libc (mismo fichero). Van dos nombres porque conviven dos toolchains: zig-cc emite
# NEEDED `libc.so` (359 consumidores) y el gcc/clang de Alpine emite `libc.musl-x86_64.so.1`
# (17: todo nss + spidermonkey). Es la MISMA libc; sin el segundo symlink esos 17 no cargan.
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"
if [ -n "$FONTS" ] && [ -d "$FONTS" ]; then
echo "==> inyectando fuentes ($(basename "$FONTS"))"
cp -a --remove-destination "$FONTS"/usr/share/fonts/. "$MERGED"/usr/share/fonts/ 2>/dev/null || \
{ mkdir -p "$MERGED"/usr/share/fonts && cp -a "$FONTS"/usr/share/fonts/. "$MERGED"/usr/share/fonts/; }
else
echo " ⚠ sin dejavu-fonts en el store: el escritorio saldrá con tofu"
fi
echo "==> inyectando arje-logind-compat (el login1 del fractal)"
# EL ESLABÓN QUE FALTABA, descubierto arrancando la imagen: mutter levanta como display server y
# muere en `Failed to setup: Failed to find any matching session`. Ese chequeo es de logind — mutter
# pregunta por su sesión vía la C-ABI sd-login, que acá provee libelogind (arje-sdlogin-compat)
# LEYENDO /run/systemd/{sessions,users,seats}. Pero alguien tiene que ESCRIBIR esos ficheros, y ese
# alguien es arje-logind-compat, el daemon D-Bus que se hace pasar por org.freedesktop.login1.
# libelogind entra solo (es dep de mutter); el daemon no, porque nadie lo declara como dep de build.
# El artefacto se elige por `takana hash` sobre la receta —el que corresponde a la receta de HOY—, no
# por el más reciente del store: con dos artefactos del mismo paquete conviviendo, la fecha no dice
# cuál es el vigente. ALC_BIN permite inyectar un binario RECIÉN COMPILADO en vez del artefacto sellado. Es para probar
# un cambio en arje-compat antes de commitear/pushear tawasuyu y re-sellar la receta (la receta pina
# un commit, así que sin push no hay artefacto nuevo). Lo que se pruebe así NO es reproducible: es
# andamio de iteración, y el camino de producción sigue siendo el artefacto del store.
if [ -n "${ALC_BIN:-}" ] && [ -x "$ALC_BIN" ]; then
install -Dm755 "$ALC_BIN" "$MERGED"/usr/bin/arje-logind-compat
echo " ⚠ binario LOCAL (no sellado): $ALC_BIN"
elif ALC="${ALC:-store/$(./target/release/takana --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
# …y su POLÍTICA D-Bus, que desde 2026-07-30 viaja en el artefacto. Se copia, no se escribe: el
# dueño del fichero es el paquete. Ojo — inyectar «sólo el binario» era el bug que destapó la
# comprobación de abajo: el artefacto traía el .conf y la imagen lo dejaba afuera.
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/ — mutter no hallará sesión" >&2; exit 1
fi
# La POLÍTICA D-Bus de login1 YA NO SE ESCRIBE ACÁ (2026-07-30): la trae el artefacto de
# `arje-logind-compat`, que es su dueño. Este bloque tenía un TODO diciendo exactamente eso, y estaba
# bien puesto: un servicio que no puede adueñarse de su nombre no es un servicio, así que su política
# es parte de lo que el paquete promete. Teniéndola acá, cualquier OTRA imagen —metal, KDE, mirada—
# se llevaba el daemon y no podía usarlo. Se comprueba abajo, junto con la de polkit.
install -d "$MERGED/usr/share/dbus-1/system.d"
echo "==> inyectando arje-polkit-compat (el PolicyKit1 del fractal) + su política"
# EL TERCER ESLABÓN DEL MISMO TIPO, y lo destapó la lista de nombres del bus: accounts-daemon y
# upowerd quedaban CONECTADOS (`:1.0`, `:1.1`) sin adquirir su nombre, porque los dos se bloquean en
# `polkit_authority_get_sync()` esperando a `org.freedesktop.PolicyKit1`. Nuestra receta `polkit` es
# libs-only a propósito (el demonio lo pone arje), así que nadie servía ese nombre y gnome-shell
# esperaba 25 s por cada uno.
# ⚠ El shim autoriza TODO (postura de sistema confiado ya tomada por arje) — ver su receta.
if APC="${APC:-store/$(./target/release/takana --store store hash recipes/arje-polkit-compat.toml 2>/dev/null | tail -1 | sed 's/^b3://')-arje-polkit-compat}"; [ -d "$APC" ]; then
install -Dm755 "$APC"/usr/bin/arje-polkit-compat "$MERGED"/usr/bin/arje-polkit-compat
install -d "$MERGED/usr/share/dbus-1/system.d"
cp -a --remove-destination "$APC"/usr/share/dbus-1/system.d/. "$MERGED"/usr/share/dbus-1/system.d/ 2>/dev/null || true
echo " ✓ $(basename "$APC" | cut -c1-12)"
else
echo " ✗ falta arje-polkit-compat en store/ — accounts-daemon y upowerd no adquirirán su nombre" >&2
exit 1
fi
# Su política tampoco se escribe acá: viaja en el artefacto (ver recipes/arje-polkit-compat.toml, que
# explica por qué el fichero se llama `zz-` y no reemplaza al de polkit).
#
# ── COMPROBACIÓN, que es lo que reemplaza a escribirlas ────────────────────────────────────────
# Mover algo a un artefacto sólo es una mejora si se NOTA cuando falta. Sin esto, un artefacto viejo
# sin su .conf daría el mismo fallo tardío y confuso de antes: el daemon arranca, no adquiere el
# nombre, y el escritorio espera 25 s por servicio sin decir por qué.
for pol in org.freedesktop.login1.conf zz-arje-polkit-compat.conf; do
if [ -f "$MERGED/usr/share/dbus-1/system.d/$pol" ]; then
echo " ✓ $pol (del artefacto)"
else
echo " ✗ falta $pol — el artefacto que lo trae es viejo; re-sellá su receta" >&2
exit 1
fi
done
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
# El launch-helper del bus de SISTEMA tiene que ser setuid root. Es el que dbus-daemon ejecuta para
# activar un servicio bajo demanda, y comprueba SUS PROPIOS permisos antes de hacer nada: si no es
# setuid root, se niega, y el cliente recibe
# Error calling StartServiceByName for <servicio>: The permission of the setuid helper is not correct
# que es exactamente lo que reportaba colord al arrancar la sesión. Sin esto **ninguna** activación
# por bus de sistema funciona; con esto, cualquier servicio con su `.service` instalado arranca solo y
# no hace falta lanzarlo a mano desde `gnome-start`.
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 # root:messagebus
chmod 4750 "$MERGED/$h"
echo " ✓ setuid en /$h"
fi
done
# GNOME_MODE se hornea como fichero: el getty ejecuta gnome-start sin ambiente heredable.
printf 'GNOME_MODE=%s\n' "${GNOME_MODE:-drm}" > "$MERGED/etc/gnome-mode"
echo "==> modo de arranque: ${GNOME_MODE:-drm}"
# KMS_MODE viaja por el mismo camino, y sólo si se pide: `gnome-start` lo traduce a
# MUTTER_DEBUG_FORCE_KMS_MODE. Vacío = atomic (el default de mutter). `simple` = el modeset legacy
# que se heredó de KDE y dejaba la pantalla en negro.
if [ -n "${KMS_MODE:-}" ]; then
printf 'KMS_MODE=%s\n' "$KMS_MODE" >> "$MERGED/etc/gnome-mode"
echo "==> KMS forzado a: $KMS_MODE"
fi
echo "==> instalando /usr/bin/gnome-start"
install -Dm755 scripts/gnome/gnome-start-qemu.sh "$MERGED/usr/bin/gnome-start"
echo "==> auto-lanzar GNOME: console-getty ejecuta gnome-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/gnome-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
echo "==> inyectando las Cards de servicio en el genesis (SDD 30 4c)"
# LO QUE ESTO CAMBIA: hasta hoy los demonios de sistema los lanzaba `gnome-start` con `&` -- sin
# supervision, sin backoff y sin el `CRASHED` real, o sea sin nada de lo que arje es PID 1 para dar.
# Aca pasan a ser Cards del `genesis`, que es de donde arje los encarna y revive.
#
# Las dos mitades salen de donde viven: QUE es cada servicio, de `[[service]]` en su receta; CUALES
# arranca esta imagen, de `servicios = [...]` del perfil en targets.toml. La traduccion receta->Card
# la hace `takana service-cards` y no este script: el formato es contrato con `card_core::Card`, y
# dos generadores del mismo formato divergen en silencio.
CARDS_JSON="${CARDS_JSON:-$(mktemp)}"
./target/release/takana service-cards $(python3 scripts/targets.py --service-paths escritorio-gnome) > "$CARDS_JSON"
[ -s "$CARDS_JSON" ] || { echo " x no pude emitir las cards de servicio" >&2; exit 1; }
python3 scripts/gnome/inyectar-cards.py "$MERGED/ente/seed.card.json" "$CARDS_JSON"
# /var/run → /run: arje-logind-compat busca el bus de sistema en la ruta LEGACY
# (/var/run/dbus/system_bus_socket) y dbus-daemon escucha en /run/dbus/system_bus_socket. En casi
# toda distro /var/run es un symlink a /run desde hace 15 años; esta base metal es tan mínima que ni
# siquiera tiene /var/run. Sin el enlace el daemon reporta `bus_present=false` y se va a "modo idle",
# o sea que nunca registra login1 ni escribe /run/systemd/sessions ⇒ mutter no halla sesión.
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