Files
takana/scripts/cosmic/qemu-desktop-image.sh
T
Sergio 476168bb07 takana etapa 5c: comentarios de scripts, MOTD, y un BUG que introdujo la etapa 4
250 líneas de comentario en 151 scripts. Control verificado: el diff no toca
NI UNA línea que no empiece por #, y la sintaxis de los 151 pasa.

El barrido saltea heredocs y cadenas triples, y el guardián DISPARÓ 3 veces:
las tres eran el MOTD que el script escribe DENTRO de la imagen construida —
texto del producto, no comentario del script. Se cambiaron aparte y a
propósito, que es rebranding, no limpieza.

Y el hallazgo caro:  casaba contra ,
que es el TARGET de tracing — o sea el module_path!, o sea el nombre del crate.
La etapa 4 lo movió a  y el script quedó casando NADA. No fallaba:
imprimía cero atribuciones, indistinguible de un log sin problemas. Comprobado
con el binario (RUST_LOG=info sobre zlib), no deducido. Ahora acepta las dos, y
tiene que seguir aceptándolas porque los logs viejos en disco dicen la vieja.

Además 14 rutas de módulo  en docs, que el barrido anterior no tocó
porque  no es frontera de palabra.
2026-09-09 19:28:48 +00:00

203 lines
11 KiB
Bash
Executable File

#!/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 `takana 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/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
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
# COSMIC_MODE se hornea como fichero: el getty no hereda ambiente. `bare` (default) lanza el
# compositor solo y los clientes a mano; `session` usa cosmic-session, que EXIGE
# cosmic-settings-daemon o panickea. Ver el runbook.
printf 'COSMIC_MODE=%s\n' "${COSMIC_MODE:-bare}" > "$MERGED/etc/cosmic-mode"
echo "==> modo de arranque: ${COSMIC_MODE:-bare}"
echo "==> instalando /usr/bin/cosmic-start"
install -Dm755 scripts/cosmic/cosmic-start.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"
# ── CATÁLOGO APPSTREAM, o por qué cosmic-store salía VACÍO ─────────────────────────────────────
# El backend `pkgar` de la tienda NO lee `/usr/share/metainfo/*.metainfo.xml` —los ficheros que cada
# receta instala, uno por aplicación—. Lee el **CATÁLOGO**: los ficheros de
# `{/usr/share,/var/lib,/var/cache}/{swcatalog,app-info}/{xml,yaml}/`, que en una distro normal
# genera `appstreamcli compose` agregando todos los metainfo en un solo documento
# `<components version=… origin=…>`. Sin ese fichero la tienda abre perfecta y no lista NADA
# —«No results for …»— porque su catálogo está vacío, no porque le falte nada del sistema.
#
# takana no tiene appstreamcli (sería AppStream + glib, o sea la torre de C que esta campaña evita),
# pero el formato es XML plano y la agregación es exactamente eso: envolver los `<component>` de cada
# metainfo en una raíz. Doce líneas de python valen la librería entera.
echo "==> catálogo AppStream para cosmic-store (agrega los metainfo del rootfs)"
#
# ⚠ Y hay que SACAR el bloque `<provides>`. Los metainfo que genera `xdgen` —los de cosmic-edit,
# files, settings, store y term— lo escriben con los envoltorios `<mimetypes>`/`<binaries>`, y el
# parser de la tienda (el crate `appstream`) espera los hijos PLANOS del esquema de catálogo
# (`<mediatype>`, `<binary>`). Con el envoltorio muere con
# failed to parse … in "/usr/share/swcatalog/xml/hammer.xml": The tag provide doesn't have a value
# y —esto es lo caro— **descarta el componente entero, callado para el usuario**: la tienda abría
# perfecta y listaba sólo 2 de 7, justamente los dos que NO tienen `<provides>` (App Library y
# Launcher). Se descubrió corriendo `RUST_LOG=info cosmic-store` desde cosmic-term DENTRO de la VM;
# desde el lanzador la salida del proceso no llega al log de la sesión.
python3 - "$MERGED" <<'PY'
import glob, os, re, sys
merged = sys.argv[1]
comps = []
for p in sorted(glob.glob(os.path.join(merged, "usr/share/metainfo/*.xml"))):
x = open(p, encoding="utf-8").read()
# Quedarse con el elemento raíz <component …>…</component>, sin cabecera XML ni comentarios.
m = re.search(r"<component[\s>].*</component>", x, re.S)
if m:
comps.append(re.sub(r"\s*<provides>.*?</provides>", "", m.group(0), flags=re.S))
out = os.path.join(merged, "usr/share/swcatalog/xml")
os.makedirs(out, exist_ok=True)
with open(os.path.join(out, "hammer.xml"), "w", encoding="utf-8") as f:
f.write('<?xml version="1.0" encoding="UTF-8"?>\n')
f.write('<components version="0.14" origin="hammer" architecture="x86_64">\n')
f.write("\n".join(comps))
f.write("\n</components>\n")
print(" %d componentes → /usr/share/swcatalog/xml/hammer.xml" % len(comps))
PY
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