diff --git a/docs/evidencia/sway-qemu-2026-08-08.png b/docs/evidencia/sway-qemu-2026-08-08.png new file mode 100644 index 00000000..54b5a4f5 Binary files /dev/null and b/docs/evidencia/sway-qemu-2026-08-08.png differ diff --git a/scripts/wlr/qemu-sway-image.sh b/scripts/wlr/qemu-sway-image.sh new file mode 100755 index 00000000..10b7ebf5 --- /dev/null +++ b/scripts/wlr/qemu-sway-image.sh @@ -0,0 +1,92 @@ +#!/bin/sh +# qemu-sway-image.sh — imagen QEMU del perfil escritorio-sway. Hermano de +# scripts/cosmic/qemu-desktop-image.sh: MISMO andamiaje probado (base metal + cierre hidratado + +# mesa-llvmpipe + musl), sin las piezas que COSMIC necesita y sway no. +# +# QUÉ SE CAE respecto del de COSMIC, y por qué: +# · arje-logind-compat — sway toma el DRM con `LIBSEAT_BACKEND=builtin` siendo root, así que no +# hace falta un proveedor de login1. Una pieza móvil menos. +# · dbus / messagebus / setuid helper — nada del entorno mínimo de sway lo pide. +# · catálogo AppStream — era para cosmic-store; acá no hay tienda. +# Se conserva TODO lo que ya costó caro descubrir: la inyección de kms_swrast (en QEMU no hay GPU), +# libLLVM.18 (llvmpipe lo JIT-linkea), el runtime C++ de Alpine (deuda conocida de la cadena de +# mesa) y los DOS sonames de musl (zig-cc emite `libc.so`, el gcc de Alpine +# `libc.musl-x86_64.so.1`: es la MISMA libc y sin el segundo symlink lo de gcc no carga). +set -eu +ROOT="$(cd "$(dirname "$0")/../.." && pwd)"; cd "$ROOT" + +BASE="${BASE:-work/metal-rootfs}" +SWAY="${SWAY:-work/sway-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/sway-qemu-rootfs}" +IMG="${IMG:-work/hammer-sway-qemu.img}" + +[ -d "$BASE" ] || { echo "falta la base metal: $BASE" >&2; exit 1; } +[ -d "$SWAY" ] || { echo "falta el cierre sway: $SWAY (correr el hydrate)" >&2; exit 1; } + +echo "==> fundiendo rootfs por hardlinks (base metal + cierre sway)" +rm -rf "$MERGED" +cp -al "$BASE" "$MERGED" +cp -al --remove-destination "$SWAY"/. "$MERGED"/ + +. "$ROOT/scripts/lib/pid1-desde-store.sh" +inyectar_pid1 "$MERGED" "$BASE" + +echo "==> parcheando mesa software desde $(basename "$MESA")" +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/" >&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 "==> runtime C++ de Alpine (deuda conocida de la cadena de mesa)" +for lib in libgcc_s.so.1 libstdc++.so.6; do + src=".dev-fs/alpine/usr/lib/$lib" + [ -r "$src" ] && cp -a --remove-destination "$src"* "$MERGED"/usr/lib/ 2>/dev/null || echo " ⚠ sin $lib" +done + +echo "==> musl y los DOS sonames" +MUSL="${MUSL:-/usr/lib/musl/lib/libc.so}" +[ -r "$MUSL" ] || { echo "no encuentro musl libc ($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" + +echo "==> grupo video (wlroots abre /dev/dri)" +grep -q '^video:' "$MERGED/etc/group" 2>/dev/null || { { cat "$MERGED/etc/group"; echo "video:x:44:"; } > "$MERGED/etc/group.new"; chmod 644 "$MERGED/etc/group.new"; mv "$MERGED/etc/group.new" "$MERGED/etc/group"; } + +echo "==> instalando /usr/bin/sway-start y la config" +install -Dm755 scripts/wlr/sway-start.sh "$MERGED/usr/bin/sway-start" +install -Dm644 scripts/wlr/sway-config "$MERGED/etc/sway/config" + +echo "==> auto-lanzar sway: console-getty ejecuta sway-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/sway-start" if a == "/bin/sh" else a) for a in argv] + print(" console-getty argv →", argv) +tmp = p + ".new" +json.dump(c, open(tmp, "w"), indent=2); os.chmod(tmp, 0o644); os.replace(tmp, p) +PY + +ln -sfn /run "$MERGED/var/run" +mkdir -p "$MERGED/root/.config" "$MERGED/run/user/0" "$MERGED/tmp" "$MERGED/var/log" + +echo "==> rootfs fundido: $(du -sh "$MERGED" | cut -f1)" +need_mb=$(du -sm "$MERGED" | cut -f1) +ROOT_SIZE="${ROOT_SIZE:-$(( need_mb + 1536 ))}" +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 diff --git a/scripts/wlr/sway-config b/scripts/wlr/sway-config new file mode 100644 index 00000000..2ce3ee37 --- /dev/null +++ b/scripts/wlr/sway-config @@ -0,0 +1,4 @@ +# Config mínima para la prueba en QEMU. Los clientes NO se lanzan desde acá: los `exec` de la config +# disparan ANTES de que exista el workspace y mueren con «Failed to create launch context». Los +# lanza sway-start cuando el socket de Wayland ya escucha. Ver el comentario allí. +output * bg #1a4b8c solid_color diff --git a/scripts/wlr/sway-start.sh b/scripts/wlr/sway-start.sh new file mode 100755 index 00000000..48d67221 --- /dev/null +++ b/scripts/wlr/sway-start.sh @@ -0,0 +1,75 @@ +#!/bin/sh +# sway-start.sh — arranque de sway dentro de la VM. Lo ejecuta el `console-getty` de arje-zero en +# lugar de /bin/sh, así que corre como PID de sesión sin login manager. +# +# ── LA SESIÓN VA POR `seatd-launch`, NO POR EL BACKEND «builtin» ─────────────────────────────── +# Primer intento: `LIBSEAT_BACKEND=builtin`, que en libseat abre el DRM directo siendo root. +# **Falló, y el log lo dijo exacto**: +# [libseat] No backend matched name 'builtin' +# [backend/session] Unable to create seat: Invalid argument → Failed to start a DRM session +# Porque NUESTRO libseat no lo trae: `recipes/seatd.toml` construye con `-Dlibseat-seatd=enabled +# -Dlibseat-logind=disabled`, o sea UN solo backend. Confirmado con `strings libseat.so`: lista +# `seatd` y nada más. +# ⇒ La receta manda sobre lo que uno cree recordar de un proyecto. Y la misma receta trae la +# solución: `-Dserver=enabled` construye el demonio Y `seatd-launch`, que lo levanta, corre el +# comando y limpia al salir. +# ── tmpfs SOBRE /run: la raíz de hammer se monta de SÓLO LECTURA ─────────────────────────────── +# El diagnóstico lo dijo sin ambigüedad: «/run/user/0 NO escribible». `/run` no es un tmpfs sino un +# directorio normal sobre la raíz, y la raíz de esta distro es INMUTABLE por diseño (root ro + +# partición de estado aparte). Sin esto, sway no puede crear su lockfile y muere en bucle con +# «unable to open lockfile /run/user/0/wayland-N.lock check permissions» — un mensaje que suena a +# permisos de directorio y en realidad es un sistema de ficheros de sólo lectura. +# Montar un tmpfs es lo que hace cualquier init; acá lo hace el lanzador porque arje-zero deja /run +# para quien lo necesite. +BB=/bin/busybox +$BB mount -t tmpfs -o mode=755 tmpfs /run 2>/dev/null || true + +export XDG_RUNTIME_DIR=/run/user/0 +$BB mkdir -p "$XDG_RUNTIME_DIR"; $BB chmod 700 "$XDG_RUNTIME_DIR" + +# ── RENDERER PIXMAN, NO GLES2: en QEMU no hay driver de GPU que mesa pueda cargar ────────── +# Con gles2 el arranque muere: «MESA-LOADER: failed to open virtio_gpu: /usr/lib/dri/virtio_gpu_dri.so +# No such file or directory» — mesa busca el driver del dispositivo REAL que ve (virtio_gpu) y +# nuestro mesa-llvmpipe sólo trae swrast/kms_swrast. Y «Not allowed to force software rendering when +# API explicitly selects a hardware device»: LIBGL_ALWAYS_SOFTWARE no lo salva porque wlroots ya +# eligió el dispositivo EGL explícitamente. Las dos variables juntas se pedían cosas contradictorias. +# wlroots trae un renderer PIXMAN por software que NO pasa por EGL ni GBM (buffers dumb del DRM) y +# se construye SIEMPRE: -Drenderers sólo elige entre gles2 y vulkan. Es la salida para una VM sin GPU. +export WLR_RENDERER=pixman + +# Diagnóstico: sin esto, cuando algo falla el log dice «no outputs» y nada más. +export WLR_DRM_NO_ATOMIC=1 +export WAYLAND_DEBUG=0 + +# ⚠ EL LOG VA AL SERIAL, NO A UN FICHERO DENTRO DE LA VM. El primer intento lo mandaba a +# /var/log/sway.log y la captura salió 99% negra SIN UNA SOLA LÍNEA que explicara por qué: el +# diagnóstico estaba dentro de la caja que no arranca. Un log que no se puede leer desde fuera no +# es un log. (La causa resultó ser `libz.so.1` ausente del rootfs, invisible en la pantalla negra.) +# ── DIAGNÓSTICO EXPLÍCITO antes de arrancar ──────────────────────────────────────────────────── +# sway falló con «unable to open lockfile /run/user/0/wayland-N.lock check permissions» y yo no +# tenía forma de saber si el directorio existía, de quién era, ni si /run era escribible. Adivinar +# cuesta un ciclo de imagen+arranque (~3 min); imprimir la verdad cuesta cuatro líneas. +echo "== id: $($BB id 2>&1)" +echo "== /run: $($BB ls -ld /run 2>&1)" +echo "== /run/user/0: $($BB ls -ld /run/user/0 2>&1)" +if $BB touch /run/user/0/.probe 2>/dev/null; then echo "== /run/user/0 ESCRIBIBLE"; $BB rm -f /run/user/0/.probe +else echo "== /run/user/0 NO escribible"; fi + +$BB rm -f /run/seatd.sock # el getty reinicia en bucle si sway falla; un socket huérfano impide arrancar seatd +# ── LOS CLIENTES SE LANZAN CUANDO EL SOCKET YA ESCUCHA, NO DESDE LA CONFIG ───────────────────── +# Los `exec` del fichero de config de sway dispararon a los 0,259 s y el workspace se creó a los +# 0,305 s ⇒ los tres murieron con «Failed to create launch context. No workspace». Resultado: el +# compositor arrancaba PERFECTO —salida activada, modo 1280x800, commit OK— y la captura salía 100% +# negra, porque no había un solo cliente dibujando. Es el peor tipo de fallo: todo verde en el log +# y nada en la pantalla, que es exactamente por lo que la regla dice validar CON PANTALLA. +# Acá se espera al socket de Wayland y recién entonces se lanza, que es la condición real. +( + i=0 + while [ ! -S /run/user/0/wayland-1 ] && [ $i -lt 60 ]; do $BB sleep 1; i=$((i+1)); done + export WAYLAND_DISPLAY=wayland-1 + /usr/bin/swaybg -c '#1a4b8c' & + /usr/bin/yambar & + /usr/bin/foot & +) & + +exec /usr/bin/seatd-launch -- /usr/bin/sway -d 2>&1