Diagnóstico del 1er viaje al OptiPlex 3060 (Coffee Lake, UHD 630). El dmesg del propio USB muestra que la GPU estaba PERFECTA: [drm] Found coffeelake (device ID 3e92) ... Initialized i915 1.6.0 on minor 0 fbcon: i915drmfb (fb0) is primary device Lo que fallaba era el script de arranque. `cosmic-start-qemu.sh` exportaba LIBGL_ALWAYS_SOFTWARE=1 y MESA_LOADER_DRIVER_OVERRIDE=kms_swrast INCONDICIONAL — correcto sobre virtio-gpu, letal sobre Intel real: la mesa del corpus va con -Dgallium-drivers=iris -Dllvm=disabled y NO EXISTE ningún kms_swrast_dri.so ⇒ EGL falla. Y en el mejor caso habría sido peor: si arrancaba, componía por software y ScreenCast fallaba igual que en QEMU ⇒ el viaje NO PODÍA contestar su pregunta. El origen es un comentario que yo escribí en metal-desktop-image.sh afirmando que el script no tenía supuestos del emulador. Los tenía, y el nombre del fichero lo decía. Ahora: - `cosmic-start-qemu.sh` → `cosmic-start.sh` (se llama por lo que hace). - El modo de GL se DETECTA con dos patas: driver DRM del kernel Y .so de mesa presente. Si no hay ninguna, lo DICE en vez de morir dentro de EGL. - La imagen VERIFICA que el script instalado no fuerce kms_swrast (falla el build si vuelve a pasar). Y correr cosmic-start sobre la imagen de metal POR PRIMERA VEZ (antes sólo se validaba que llegara al prompt) destapó que le faltaba entera la preparación de sesión que sólo tenía qemu-desktop-image.sh: sin grupo `video` no arranca seatd, sin usuario `messagebus` no arranca el bus de SISTEMA — y sin bus de sistema NO HAY PORTAL, o sea que portal-probe screencast no habría tenido con quién hablar aunque el GL fuese perfecto. Copiada: arje-logind-compat + política, video, messagebus, setuid del launch-helper, /var/run→/run, COSMIC_MODE, libc.musl-x86_64.so.1. Además, para que el próximo fallo en metal no cueste otro viaje a ciegas: - Los logs van a /var/log/cosmic (ext4) y no a /tmp (RAM, se evapora al apagar). `dump()` también, que iba SÓLO al serial — donde se perdió la explicación de los tres fallos de arriba. - authorized_keys horneada: sshd escuchaba en :22 pero era INALCANZABLE (PasswordAuthentication no, sin clave) ⇒ ahora se depura por red. - netup-wait: el r8169 levanta el enlace a los 20,2s y el ente sshd pedía DHCP a los 13,7s, sin reintento. En QEMU no se ve: virtio-net tiene carrier desde el instante cero. - firmware i915 de otras generaciones (la base sólo traía tgl_*; el Coffee Lake pedía kbl_dmc_ver1_04.bin). Validado arrancando como usb-storage: seatd OK, bus de sistema OK, login1 OK, pipewire+wireplumber OK, compositor OK. La línea de GL dice la verdad en QEMU. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
203 lines
11 KiB
Bash
Executable File
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 `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
|
|
|
|
# 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.
|
|
#
|
|
# hammer 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
|