diff --git a/docs/runbooks/gnome-qemu-desktop.md b/docs/runbooks/gnome-qemu-desktop.md index 26b74bdf..7bfc0639 100644 --- a/docs/runbooks/gnome-qemu-desktop.md +++ b/docs/runbooks/gnome-qemu-desktop.md @@ -177,9 +177,35 @@ Gvc-DEBUG: Updating sink: index=33 name='auto_null' description='Dummy Output' El `Failed to connect context: Connection refused` desapareció y gvc ve el servidor. El sink es `auto_null` («Dummy Output»), que es la respuesta honesta: la VM no tiene tarjeta de sonido. -**⚠ Falta `wireplumber`** (el gestor de sesión; Lua + su propio cierre). Sin él PipeWire arranca y -acepta clientes pero **no enumera ni enruta dispositivos**. **«El shell conecta» ≠ «hay sonido»**, y el -log de arriba es exactamente el caso donde confundirlos sería fácil. +**`wireplumber` cerrado** (b3:39a9a06c), y con él `lua` (b3:e6c35f98), que hubo que autorar: el +Makefile de Lua **no produce `.so` ni `.pc`** —los agrega cada distro—, así que la receta enlaza la +compartida a mano desde la `.a` con `--whole-archive` y escribe el pkg-config. `-fPIC` es obligatorio: +`libwplua` es un objeto compartido y una `.a` no-PIC no entra (la regla del corpus que ya obligó a las +variantes `-shared` de zlib, sqlite, libxml2, libuuid y dbus). + +Los TRES procesos y su orden, porque cada uno es cliente del anterior: `pipewire` → `pipewire-pulse` +→ `wireplumber`. `gnome-start` verifica cada paso y termina volcando `wpctl status`, que es el dato que +importa: no el pid, sino **qué ve la política**. + +``` +PipeWire 'pipewire-0' [1.2.7] + └─ Clients: 32. pipewire · 34. WirePlumber · 47. WirePlumber [export] · 48. wpctl +Audio + ├─ Devices: ← vacío + ├─ Sinks: * 33. Dummy Output +``` + +**Y ese `Devices:` vacío NO es un fallo de wireplumber: el kernel de esta imagen no tiene ALSA.** +`recipes/linux-metal.toml:111` lo apaga explícitamente — `-d SOUND -d SND`. No hay `/dev/snd` ni +`/sys/class/sound` que enumerar. Se verificó agregando una `ich9-intel-hda` emulada a QEMU +(`AUDIO=1`, ahora el default de `run-qemu-desktop.sh`): con tarjeta y sin ALSA en el kernel, el +resultado no cambia. **Encender el sonido en la distro es una decisión con costo** —re-sellar el +kernel y rehacer la imagen metal— y queda para quien la tome, no escondida en un default. + +Un bug propio que esto destapó: el bloque de wireplumber quedó DUPLICADO en `gnome-start` por un +merge descuidado, y el resultado eran **dos wireplumber vivos peleándose el grafo**. El síntoma no era +un error sino una lista de clientes que se lee normal si no se cuenta: dos «WirePlumber» con pids +distintos en `wpctl status`. Gotcha de empaquetado, medido: ya existía `incoming-kde/pipewire.toml` sellada y **no se puede reusar** hidratándola acá — los dos cierres traen glib y **no son la misma glib**: `incoming-kde/glib-shared` y diff --git a/recipes/incoming-gnome/libelogind.toml b/recipes/incoming-gnome/libelogind.toml index 45253f5b..d6dee0e5 100644 --- a/recipes/incoming-gnome/libelogind.toml +++ b/recipes/incoming-gnome/libelogind.toml @@ -27,7 +27,7 @@ version = "0.0.1" # versiona el `Cargo.lock` de la raíz: sin él, el vendoreo `--locked` del fetch no es reproducible # (el monorepo lo gitignora en main). repo = "gitea@git.tawasuyu.net:tawasuyu/tawasuyu.git" -commit = "98db584fd28d9ff2bf34c9cf7d7db2e1de0b3fd0" +commit = "749edfe41766e098d3eb18561c6a396610c5cd8e" # El monorepo COMMITEA su propio `vendor/` (`[patch.crates-io] smithay = { path = "vendor/smithay" }`, # la copia parcheada que mirada necesita para el tearing) y el `cargo vendor` de hammer escribe ahí # por defecto, pisándolo: el build muere con `failed to read /src/vendor/smithay/.cargo-checksum.json` diff --git a/recipes/incoming-gnome/lua.toml b/recipes/incoming-gnome/lua.toml new file mode 100644 index 00000000..95c23872 --- /dev/null +++ b/recipes/incoming-gnome/lua.toml @@ -0,0 +1,95 @@ +# lua 5.4.8 — el intérprete y **la librería compartida**, que es lo que hace falta acá. +# +# POR QUÉ ENTRA: `wireplumber` —el gestor de sesión de PipeWire, sin el cual PipeWire acepta clientes +# pero no enumera ni enruta dispositivos— tiene su política escrita EN LUA y embebe un intérprete +# (`lib/wplua`). Su `subprojects/lua.wrap` DESCARGA Lua 5.5 de lua.org, cosa imposible en el sandbox +# hermético (`--wrap-mode=nodownload`), así que va con `-Dsystem-lua=true` y el sistema tiene que +# tener Lua. Su meson acepta 5.5, 5.4 o 5.3 (meson.build:91-95); se elige **5.4**, la serie estable +# larga que usan las distros, en vez de la 5.5 del wrap. +# +# ══ LO QUE ESTA RECETA TIENE QUE INVENTAR, porque upstream no lo da ═════════════════════════════ +# El Makefile de Lua sólo produce `liblua.a` y los binarios. **No hay regla de `.so` ni fichero +# pkg-config**: los agrega cada distro. Acá se agregan las dos cosas, y a mano, porque no hay dónde +# copiarlas de: +# +# 1. `-fPIC` en `MYCFLAGS`. Sin esto la `.a` no es reubicable y `libwplua` —que es un objeto +# compartido— no linkea: es la regla del corpus sobre `.a` no-PIC dentro de un `.so` +# («relocation R_X86_64_32 … recompile with -fPIC»), la misma que obligó a las variantes +# `-shared` de zlib, sqlite, libxml2, libuuid y dbus. +# 2. La `.so` se enlaza a mano desde la `.a` con `--whole-archive`, porque una librería estática sólo +# aporta los objetos que alguien referencia y acá hay que llevárselos TODOS. SONAME +# `liblua-5.4.so.0`, que es el que graba quien enlace. +# 3. El `.pc` se escribe acá. El nombre importa: wireplumber prueba `lua-5.4`, `lua5.4` y `lua54` en +# ese orden, así que el fichero se llama `lua5.4.pc` y ADEMÁS se deja `lua-5.4.pc` y `lua.pc` como +# enlaces — es más barato satisfacer las tres convenciones que adivinar cuál buscará el próximo +# consumidor. +# +# `-DLUA_USE_LINUX` es lo que activa `dlopen` y el resto de POSIX; sin él Lua se compila en modo ANSI +# genérico y `require` de módulos C no funciona. **SIN readline a propósito**: sólo lo usa el REPL +# interactivo, y el ncurses del catálogo es static-only y arrastra `-static` al link (el mismo +# problema que hizo saltar `pw-top` en la receta de pipewire). +# +# `SYSLIBS` pierde el `-Wl,-E` de upstream: exportar todos los símbolos del EJECUTABLE sirve para que +# módulos C cargados por `require` vean la API de Lua, y acá el que exporta es la `.so`. +name = "lua" +version = "5.4.8" + +[source] +tarball = "https://www.lua.org/ftp/lua-5.4.8.tar.gz" +sha256 = "4f18ddae154e793e46eeab727c59ef1c0c0c2b744e7b94219710d76f530629ae" + +[build] +compiler = "zig-cc" +target = "x86_64-linux-musl" +link = "dynamic" + +[build.phases] +configure = "true" +compile = ''' +set -e +# CC se pasa explícito: el Makefile de Lua lo fija a `gcc -std=gnu99` en la línea 9 y ganaría sobre el +# del entorno. +make -C src all CC="hammer-zig-cc -std=gnu99" \ + SYSCFLAGS="-DLUA_USE_LINUX" SYSLIBS="-ldl" MYCFLAGS="-fPIC" +# La .so que upstream no construye. --whole-archive porque de una estática el linker sólo se lleva lo +# referenciado, y acá hay que llevárselo todo. +hammer-zig-cc -shared -o src/liblua-5.4.so.0.0.0 \ + -Wl,-soname,liblua-5.4.so.0 \ + -Wl,--whole-archive src/liblua.a -Wl,--no-whole-archive -lm +''' +install = ''' +set -e +mkdir -p /out/usr/bin /out/usr/lib/pkgconfig /out/usr/include /out/usr/share/man/man1 +install -m755 src/lua /out/usr/bin/lua5.4 +install -m755 src/luac /out/usr/bin/luac5.4 +ln -s lua5.4 /out/usr/bin/lua +ln -s luac5.4 /out/usr/bin/luac +install -m644 src/lua.h src/luaconf.h src/lualib.h src/lauxlib.h src/lua.hpp /out/usr/include/ +install -m644 src/liblua.a /out/usr/lib/liblua-5.4.a +install -m755 src/liblua-5.4.so.0.0.0 /out/usr/lib/ +ln -s liblua-5.4.so.0.0.0 /out/usr/lib/liblua-5.4.so.0 +ln -s liblua-5.4.so.0 /out/usr/lib/liblua-5.4.so +ln -s liblua-5.4.so /out/usr/lib/liblua.so +cat > /out/usr/lib/pkgconfig/lua5.4.pc <<'PC' +prefix=/usr +exec_prefix=${prefix} +libdir=${exec_prefix}/lib +includedir=${prefix}/include +INTERPRETER=${exec_prefix}/bin/lua5.4 +COMPILER=${exec_prefix}/bin/luac5.4 + +Name: Lua +Description: Lua language engine +Version: 5.4.8 +Requires: +Libs: -L${libdir} -llua-5.4 -lm +Cflags: -I${includedir} +PC +# Las tres convenciones de nombre que se usan por ahí; wireplumber prueba lua-5.4, lua5.4 y lua54. +ln -s lua5.4.pc /out/usr/lib/pkgconfig/lua-5.4.pc +ln -s lua5.4.pc /out/usr/lib/pkgconfig/lua54.pc +ln -s lua5.4.pc /out/usr/lib/pkgconfig/lua.pc +''' + +[deps] +build = ["make", "pkgconf"] diff --git a/recipes/incoming-gnome/wireplumber.toml b/recipes/incoming-gnome/wireplumber.toml new file mode 100644 index 00000000..aaeed3c4 --- /dev/null +++ b/recipes/incoming-gnome/wireplumber.toml @@ -0,0 +1,72 @@ +# wireplumber 0.5.15 — EL GESTOR DE SESIÓN de PipeWire. La pieza que faltaba para que el audio sea +# audio y no sólo un socket que acepta conexiones. +# +# QUÉ APORTA, y por qué no alcanzaba con pipewire: `pipewire` se construyó con +# `-Dsession-managers=[]`, así que arranca, acepta clientes y gnome-shell conecta —el +# `Gvc-WARNING: Failed to connect context` desapareció— pero **no enumera ni enruta dispositivos**: no +# hay política de qué es entrada, qué es salida, qué se enlaza con qué. El sink que veía el shell era +# `auto_null` («Dummy Output»). Eso es el nodo de descarte, no una tarjeta. +# +# wireplumber es esa política, y está escrita EN LUA: embebe un intérprete (`lib/wplua`) y sus reglas +# viven en `src/scripts/*.lua`. De ahí que la dep interesante de esta receta no sea PipeWire sino +# **`lua`**, que hubo que autorar (el Makefile de Lua no produce `.so` ni `.pc`; ver esa receta). +# +# `-Dsystem-lua=true` es obligatorio acá: por defecto wireplumber usa su `subprojects/lua.wrap`, que +# **DESCARGA Lua 5.5 de lua.org**, y el sandbox va con `--wrap-mode=nodownload`. Con la perilla +# prendida busca por pkg-config `lua-5.5`/`lua5.5`/`lua55`, después las 5.4 y después las 5.3 +# (meson.build:91-95): nuestra `lua5.4.pc` entra por la segunda tanda. +# +# `-Delogind=enabled` — cuarta vez que la C-ABI de logind aparece en esta campaña, y la primera en que +# está desde el principio: `libelogind` (arje-sdlogin-compat) ya está sellada. wireplumber la usa para +# saber si la sesión está activa antes de tomar los dispositivos de audio, que es exactamente el +# problema que resuelve nuestro shim. +# +# ⚠ EL `-I$PWD/lib/wp` DE `c_args` ESTÁ EN DUDA, y conviene decirlo antes de que alguien lo copie. +# Se agregó porque el build cortaba con `../lib/wp/wp.h:13: fatal error: 'client.h' file not found`, +# un header que SÍ existe justo al lado del que lo incluye — o sea que el `#include "..."` relativo +# al fichero incluyente debería haberlo encontrado. Al rato apareció la explicación de verdad: **otro +# agente estaba construyendo en el mismo repo y había borrado árboles de `work/sources/` en pleno +# build**, que es la carrera del ADR 0012 (el árbol de fuentes es caché Y workspace a la vez) y +# produce exactamente esa firma: ficheros que existen al mirarlos después y no existían al compilar. +# La receta sella con la bandera puesta; **falta re-probar SIN ella con el repo quieto** para saber si +# hacía falta o si sólo tapó el síntoma de la carrera. +# +# Por lo mismo se QUITÓ de esta receta un `rm -rf output` que había puesto antes en el configure: con +# el árbol compartido, borrar el directorio de build es sabotear a quien esté compilando en paralelo. +# +# Apagados: `-Dsystemd=disabled` y sus units (acá PID1 es arje-zero, y el arranque lo hace +# `gnome-start`); `-Ddoc=disabled` (falta sphinx/doxygen); `-Dtests=false -Ddbus-tests=false`; +# `-Dintrospection=disabled` porque nadie carga `Wp-0.5` por typelib — el shell habla con PipeWire por +# el protocolo de PulseAudio, no con wireplumber por GObject. +name = "wireplumber" +version = "0.5.15" + +[source] +tarball = "https://gitlab.freedesktop.org/pipewire/wireplumber/-/archive/0.5.15/wireplumber-0.5.15.tar.gz" +sha256 = "baa121bc918df5fa0e0e70755bb1c99ffab0ab107225ecf99aa470e2c6ba5e7b" + +[build] +compiler = "zig-cc" +target = "x86_64-linux-musl" +link = "dynamic" + +[build.phases] +configure = ''' +set -e +PKG_CONFIG_PATH=/usr/lib/pkgconfig PYTHONPATH=/usr/lib/python3.12/site-packages meson setup output \ + --prefix=/usr --buildtype=release --wrap-mode=nodownload \ + -Ddefault_library=shared \ + -Dsystem-lua=true -Dsystem-lua-version=auto \ + -Delogind=enabled -Dsystemd=disabled \ + -Dsystemd-system-service=false -Dsystemd-user-service=false \ + -Dintrospection=disabled -Ddoc=disabled \ + -Dtests=false -Ddbus-tests=false \ + "-Dc_args=-Wno-error=date-time -I$PWD/lib/wp" +''' +compile = "PYTHONPATH=/usr/lib/python3.12/site-packages ninja -C output" +install = "PYTHONPATH=/usr/lib/python3.12/site-packages DESTDIR=/out meson install -C output --no-rebuild" + +[deps] +build = ["meson", "samurai", "python3", "pkgconf", "gettext-tiny", "lua", "pipewire", + "glib", "dbus-shared", "libelogind", "alsa-lib", "libsndfile", "pulseaudio", + "expat", "pcre2", "libffi", "libudev-zero", "zlib-shared"] diff --git a/scripts/gnome/gnome-start-qemu.sh b/scripts/gnome/gnome-start-qemu.sh index 83ae6869..001d54a4 100755 --- a/scripts/gnome/gnome-start-qemu.sh +++ b/scripts/gnome/gnome-start-qemu.sh @@ -292,15 +292,18 @@ esperar_nombre() { return 1 } # ── AUDIO: PipeWire (decisión del usuario, 2026-07-29) ────────────────────────────────────────── -# Van DOS procesos y el orden importa: `pipewire` es el servidor, y `pipewire-pulse` es el puente que -# habla el protocolo de PulseAudio contra él. gnome-shell usa libpulse (gvc), así que lo que necesita -# es el SEGUNDO; sin el primero, el segundo no tiene con quién hablar. +# Van TRES procesos y el orden importa, porque cada uno es cliente del anterior: +# 1. `pipewire` — el servidor. Crea $XDG_RUNTIME_DIR/pipewire-0. +# 2. `pipewire-pulse` — el puente que habla el protocolo de PulseAudio contra él. Es lo que +# gnome-shell necesita (gvc usa libpulse), y sin (1) no tiene con quién hablar. +# 3. `wireplumber` — el gestor de sesión: enumera dispositivos y aplica la política. Sin él (1) y +# (2) funcionan pero el único sink es `auto_null`, el nodo de descarte. +# Los tres son servicios de USUARIO: sus sockets viven en $XDG_RUNTIME_DIR, creado más arriba. # -# Los dos son servicios de USUARIO: su socket vive en $XDG_RUNTIME_DIR, que ya está creado más arriba. -# -# ⚠ Sin gestor de sesión (wireplumber todavía no está autorado): esto hace que el shell CONECTE y deje -# de tirar `Gvc-WARNING: Failed to connect context: Connection refused`, pero NO enumera ni enruta -# dispositivos. «El shell conecta» no es «hay sonido», y conviene no confundirlos. +# ⚠ NO duplicar el bloque de wireplumber. Estuvo dos veces por un merge descuidado de este script y el +# resultado fue DOS wireplumber vivos peleándose el grafo — visible en `wpctl status` como dos clientes +# «WirePlumber» con pids distintos. El síntoma no es un error: es una lista de clientes que se lee +# normal si no se cuenta. if [ -x /usr/bin/pipewire ]; then /usr/bin/pipewire >/tmp/pipewire.log 2>&1 & say "pipewire lanzado (pid $!)" @@ -315,6 +318,27 @@ if [ -x /usr/bin/pipewire ]; then else say "!! pipewire-pulse no creó pulse/native:"; dump /tmp/pipewire-pulse.log fi + # wireplumber: el GESTOR DE SESIÓN. Va tercero porque necesita que el servidor ya esté; es el que + # convierte «PipeWire acepta clientes» en «PipeWire tiene dispositivos»: enumera por udev/alsa y + # aplica la política (qué es entrada, qué es salida, qué se enlaza con qué), y esa política está + # escrita en Lua. Sin él el único sink es `auto_null`, el nodo de descarte. + if [ -x /usr/bin/wireplumber ]; then + /usr/bin/wireplumber >/tmp/wireplumber.log 2>&1 & + WP_PID=$! + say "wireplumber lanzado (pid $WP_PID)" + sleep 2 + if alive $WP_PID; then + say "wireplumber sigue vivo" + # El dato que importa no es el pid sino QUÉ VE. `wpctl status` lista los nodos que la política + # creó: si sale vacío, wireplumber corre pero no enumeró nada. + wpctl status >/tmp/wpctl.log 2>&1 || true + dump /tmp/wpctl.log + else + say "!! wireplumber MURIÓ:"; dump /tmp/wireplumber.log + fi + else + say "!! sin wireplumber — PipeWire acepta clientes pero no habrá dispositivos" + fi else say "!! pipewire no creó su socket:"; dump /tmp/pipewire.log fi diff --git a/scripts/gnome/hydrate-gnome.sh b/scripts/gnome/hydrate-gnome.sh index 42f60d07..1577b695 100755 --- a/scripts/gnome/hydrate-gnome.sh +++ b/scripts/gnome/hydrate-gnome.sh @@ -54,6 +54,8 @@ else # El SERVIDOR de audio (decisión del usuario: PipeWire, no PulseAudio). No es dep de build de # nadie: el shell habla libpulse y `pipewire-pulse` le contesta por el otro lado. recipes/incoming-gnome/pipewire.toml + # Y su gestor de sesión, que es lo que hace que PipeWire tenga DISPOSITIVOS y no sólo un socket. + recipes/incoming-gnome/wireplumber.toml ) fi diff --git a/scripts/kde/run-qemu-desktop.sh b/scripts/kde/run-qemu-desktop.sh index 057e7eb0..4669ca0b 100755 --- a/scripts/kde/run-qemu-desktop.sh +++ b/scripts/kde/run-qemu-desktop.sh @@ -49,6 +49,17 @@ QEMU_ARGS=( -no-reboot ) +# AUDIO=1 (default): darle a la VM una tarjeta de sonido EMULADA. No es para oír nada —el backend es +# `none`, que descarta las muestras— sino para que HAYA UN DISPOSITIVO QUE ENUMERAR. +# +# Sin esto, `wpctl status` sale con «Devices:» vacío y un único sink `auto_null`, y ese resultado es +# AMBIGUO: se lee igual si wireplumber funciona sobre una VM sin hardware que si wireplumber no +# funciona. Con una ich9-intel-hda emulada, el árbol de /sys/class/sound existe y la respuesta +# distingue las dos cosas. AUDIO=0 para volver al caso sin tarjeta. +if [ "${AUDIO:-1}" = 1 ]; then + QEMU_ARGS+=(-audiodev none,id=snd0 -device ich9-intel-hda -device hda-duplex,audiodev=snd0) +fi + echo "==> QEMU (DISP=$DISP, serial=$SERIAL)" echo " imagen: $IMG" if [ -n "${TIMEOUT:-}" ]; then