Misma divergencia que se encontró ayer en GNOME, medida igual: `hydrate-cosmic.sh` nombra 25 raíces escritas a mano dentro del script, el perfil `escritorio-cosmic` declara 43, y **17 paquetes declarados no llegaban al rootfs**: adwaita-cursors · xkeyboard-config · bash · atuq (el navegador) · mpv · swayimg zathura + zathura-pdf-poppler · dejavu-fonts y dejavu-fonts-nerd · libnotify desktop-file-utils · hicolor-icon-theme · y las cinco *-shared de runtime ⚠ EL PRIMERO NO ES UNA COINCIDENCIA: `adwaita-cursors` es EXACTAMENTE el ejemplo con el que la cabecera de `scripts/hydrate-profile.py` advirtió de esto el 2026-09-04 — «se añadió a las raíces de cosmic y sway, y los scripts de hidratación, que no la conocen, habrían seguido armando un rootfs sin cursores». No era hipotético: seguía pasando hoy, cinco días después. Y `xkeyboard-config` es la cicatriz de sway repitiéndose: sin él el compositor arranca sin mapa de teclado. Y AL REVÉS NO SE PIERDE NADA, que es lo que hace el cambio barato y lo separa del caso GNOME (donde primero hubo que mover 11 raíces de runtime al perfil): de las 25 del script, la única que el perfil no declara es `zlib-shared`, y la clausura del perfil la alcanza igual. O sea que el motivo que justificaba la lista a mano —los componentes que `cosmic-session` lanza por PATH, que ningún grafo de build ve— YA ESTÁ RESUELTO en el perfil, que los declara todos como raíces. La lista quedó redundante sin que nadie lo notara. VERIFICADO HIDRATANDO, no leyendo: `hydrate-profile.py escritorio-cosmic` proyecta 162/162 nodos (24166 ficheros, 4,8 G) y ahí están los 17. (Ojo con la sonda: `bash` instala en `/bin/bash`, no en `/usr/bin` — mi primer chequeo lo dio por ausente y era el chequeo el que miraba mal.) ⚠ Y UN DATO QUE MATIZA EL DIAGNÓSTICO Y LO EMPEORA. El rootfs que hay en disco (`escritorios/cosmic-rootfs`, 3,9 G) SÍ tiene cursores, xkb y mpv — o sea que no se armó con la lista por defecto, alguien le pasó raíces a mano. Y aun así le falta `atuq`. El problema real no es que la lista esté mal: es que **lo que llega a la imagen depende de lo que alguien se acuerde de teclear**. Derivarlo del perfil quita esa variable. El script se deja porque su cabecera documenta por qué COSMIC necesita raíces de runtime explícitas —sigue siendo cierto sobre la naturaleza de cosmic-session—; lo que ya no es cierto es que su lista sea la verdad. Los dos armadores de imagen (qemu y metal) ahora apuntan a `hydrate-profile.py`. ── Y de paso, una nota vieja de targets.toml corregida ──────────────────────────────────────────── Decía que el perfil de KDE «son 13 raíces de ARRANQUE EN METAL, no la imagen completa», con konsole/kate/dolphin y otras 19 apps selladas y sin declarar. Ya no: hoy son 48 raíces, con esas apps, las fuentes y las *-shared declaradas — el frente KDE lo reordenó y la nota quedó atrás. De su cola quedan 8 recetas fuera del perfil y son las que corresponde: libICE, libSM, libXtst y los cinco eslabones huérfanos de xwayland desde que se retiró su receta. Lo que a KDE le queda es OTRO problema, también medido hoy: su imagen no se hidrata del perfil sino de un rootfs YA ARMADO (8,4 G) que nadie re-deriva. De las 18 hojas del perfil están 15 — faltan `obs-studio` y `atuq`, justo las dos añadidas después de esa hidratación. Re-hidratarlo es decisión del frente KDE. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LCt3ettR4Z7b6wPCBmbvEV
205 lines
11 KiB
Bash
Executable File
205 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/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
|
|
# `<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
|