#!/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/hydrate-profile.py escritorio-cosmic`; # hydrate-cosmic.sh quedó SUPERADO — su lista a mano se saltaba 17 paquetes # que el perfil declara, entre ellos adwaita-cursors y xkeyboard-config) # + 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/hydrate-profile.py escritorio-cosmic --into $COSMIC" >&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 # ``. 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 `` 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 ``. Los metainfo que genera `xdgen` —los de cosmic-edit, # files, settings, store y term— lo escriben con los envoltorios ``/``, y el # parser de la tienda (el crate `appstream`) espera los hijos PLANOS del esquema de catálogo # (``, ``). 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 `` (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 , sin cabecera XML ni comentarios. m = re.search(r"].*", x, re.S) if m: comps.append(re.sub(r"\s*.*?", "", 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('\n') f.write('\n') f.write("\n".join(comps)) f.write("\n\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