audio: wireplumber (b3:39a9a06c) + lua (b3:e6c35f98) — y el Devices vacío es del KERNEL

Cierra el gestor de sesión de PipeWire, que es lo que separa «PipeWire acepta clientes»
de «PipeWire tiene dispositivos». Su política está escrita en Lua, así que arrastró una
receta de Lua que hubo que autorar entera.

LO QUE LA RECETA DE LUA TIENE QUE INVENTAR: el Makefile de upstream sólo produce
liblua.a y los binarios — **no hay regla de .so ni fichero pkg-config**, los agrega cada
distro. Acá la compartida se enlaza a mano desde la .a con --whole-archive (una estática
sólo aporta lo referenciado y hay que llevarse todo) y el .pc se escribe con los TRES
nombres que se usan por ahí, porque wireplumber prueba lua-5.4, lua5.4 y lua54 en orden.
`-fPIC` no es opcional: libwplua es un objeto compartido.

Dos símbolos más en libelogind (tawasuyu 087230054 → re-pineado 749edfe41):
sd_uid_get_seats y sd_uid_get_state, que module-logind.c de wireplumber usa para saber si
el usuario está en un asiento antes de tomar los dispositivos. Van 28 símbolos sd-*.

**EL `Devices:` VACÍO DE wpctl NO ES UN FALLO DE wireplumber: el kernel no tiene ALSA.**
recipes/linux-metal.toml:111 lo apaga explícito (`-d SOUND -d SND`), así que no hay
/dev/snd que enumerar. Y esto NO se dio por supuesto: se agregó una ich9-intel-hda
emulada a QEMU (AUDIO=1, ahora el default de run-qemu-desktop.sh) justo porque un
«Devices: vacío» se lee IGUAL si wireplumber funciona sobre una VM sin hardware que si no
funciona. Con tarjeta y sin ALSA en el kernel, el resultado no cambia — la ambigüedad
queda resuelta. Encender el sonido en la distro es una decisión con costo (re-sellar el
kernel, rehacer la imagen metal) y queda a la vista en vez de escondida en un default.

Bug propio destapado en el camino: el bloque de wireplumber quedó DUPLICADO en
gnome-start y había DOS wireplumber 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.

Y una nota de ADR 0012 que costó un rato: la cascada de libelogind cortó a mitad y dejó
el `output/` de mutter a medio hacer; el reintento moría con «error opening
'...c.o.d': No such file or directory». Un `rm -rf output` en el configure NO alcanzó
—el árbol tenía estado viejo más allá del build dir—; lo que lo arregló fue BORRAR
work/sources/mutter-* para que el fetch lo re-extraiga limpio.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-29 20:40:12 -04:00
co-authored by Claude Opus 5
parent d57fb8c5c7
commit 51c6830f58
7 changed files with 242 additions and 12 deletions
+29 -3
View File
@@ -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
+1 -1
View File
@@ -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`
+95
View File
@@ -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"]
+72
View File
@@ -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"]
+32 -8
View File
@@ -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
+2
View File
@@ -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
+11
View File
@@ -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