Sellada b3:25caa359 al primer intento. `--help` corre y los NEEDED traen `libpipewire-0.3.so.0`:
graba CON audio, que era exactamente lo que anoche no se podía.
Es la app que estaba bloqueada por el defecto del triaje —preguntaba «¿existe la receta?» en vez de
«¿la alcanza quien la usa?»—: sus dos backends de captura de sonido vivían sólo en colas y una
receta del corpus no alcanza una cola hermana. Con pipewire en el corpus, cae sola.
`-Ddefault_audio_backend=pipewire` explícito y no `auto`: su meson resuelve `auto` mirando qué
encontró en el sandbox (meson.build:95-101), o sea que el artefacto dependería de lo que quedó
montado en el lab. Misma disciplina que en mpv y ffmpeg.
⚠ SE LISTA SÓLO EN `escritorio-sway`, y la razón es de PROTOCOLO: captura por `wlr-screencopy`, que
implementan los compositores wlroots. KWin y mutter NO lo implementan —usan el portal/ScreenCast—,
así que meterlo en esas dos imágenes sería enviar una herramienta que no puede funcionar ahí: deuda
fantasma con forma de app. En COSMIC hay que verificar si cosmic-comp expone el protocolo antes de
listarlo; no se listó a ciegas.
Cubre el caso de uso principal por el que uno instala OBS, con UNA receta en vez de las 20 + Qt6 +
X11 que OBS pedía. escritorio-sway 146/146.
Cierra la decisión que quedó abierta anoche en `docs/plan-apps-usuario-final.md`: el corpus no
tenía ningún cliente de audio salvo ALSA, y eso bloqueaba a toda app multimedia.
GANA LA VARIANTE DE COSMIC y la promoción es GRATIS: era la única cuyo cierre ya resolvía entero
contra el catálogo padre, así que `recipes/pipewire.toml` sella b3:e240653a — el mismo hash que
tenía en la cola. Con ella suben `libsndfile` (las tres copias sellaban idéntico) y `dbus-shared`
(gnome y cosmic, idénticas). Se jubilan 9 copias de cola.
POR QUÉ ESTO NO REPITE EL EPISODIO DE LAS DOS GLIB — leído de los artefactos sellados, no razonado:
**ningún pipewire de los tres enlaza glib.** Los NEEDED de las tres variantes son exactamente
`libdbus-1.so.3`, `libpulse.so.0` y `libsndfile.so.1`; glib entraba sólo como Requires transitivo
de pkg-config, en tiempo de build. ⇒ `libpipewire-0.3.so` es glib-free y se comparte entre las
cuatro imágenes sin riesgo.
⚠ Y POR ESO `pulseaudio` NO SE PROMUEVE, aunque sea la dep de al lado. Ahí la objeción SÍ aplica y
el número lo dice solo: `libpulse-mainloop-glib.so.0.0.6` mide **45.712 bytes** en la variante de
GNOME (NEEDED `libglib-2.0.so.0`) y **2.455.600** en la del corpus, que se traga la glib ESTÁTICA
del catálogo. Meter ésa en el proceso de gnome-shell —que ya carga la glib sombra dinámica— es
literalmente el cuadro de colord. `incoming-kde/pulseaudio` e `incoming-gnome/pulseaudio` se quedan
donde están: son variantes deliberadas, no duplicados. La copia del corpus existe sólo para que
pipewire tenga contra qué enlazar `libpulse.so.0`, y ese enlace es por SONAME — en runtime lo sirve
la libpulse de cada imagen, ABI-compatible (misma 0.24.3).
Enlazar contra una variante y correr contra otra es legítimo cuando lo que cruza es un SONAME; lo
que NO se puede es hidratar dos artefactos distintos en la misma ruta. Esa distinción es la que
decide qué se promueve y qué no.
mpv (b3:392e9690) pasa a `-Dpipewire=enabled`, con ALSA encendida detrás: prueba pipewire primero y
cae a alsa si no hay demonio —KDE todavía no lista PipeWire entre sus raíces—. Evidencia:
NEEDED … libasound.so.2 **libpipewire-0.3.so.0** libEGL.so.1 libc.so
--ao=help → pipewire (PipeWire audio output), alsa, null
Impacto medido antes de tocar, hasheando las 336 recetas de cola y comparando: 9 retiradas, **5
rebuilds** (wireplumber, xdg-desktop-portal ×2, kpipewire, spectacle) — los 5 sellados. Sin la
salvedad de pulseaudio habrían sido 8, incluido gnome-shell.
Perfiles: KDE 188/188, GNOME 132/132, sway 145/145.
⚠ COSMIC 105/106: `xdg-desktop-portal-cosmic` murió por OOM por TERCERA vez (7 G de RAM sin cgroups,
con otro agente compilando Rust). Sigue siendo la máquina, no la receta.
⚠ INCIDENTE, y queda escrito porque es la regla 2 del CLAUDE.md en vivo: los cuatro `git mv` de
esta promoción (pipewire, pulseaudio, libsndfile, dbus-shared) estaban STAGED y sin commitear
cuando `89da9c8` («qorpa: el mapeo por rango») se los llevó puestos. El commit ajeno barrió el
índice de otro agente — exactamente lo que la regla prohíbe— y encima lo hizo A MEDIAS: se llevó
los cuatro `.toml` pero no el `.patch`, porque el `git mv` del parche se cortó cuando el disco se
llenó al 100%.
Resultado: HEAD quedó con `recipes/pipewire.toml` declarando `patches = ["pipewire-sound-
initialized.patch"]` y ese fichero **inexistente en `recipes/`**. Cualquiera que clonara veía
`Error: receta inválida: no pude leer patch`. Esto lo repara.
El hash confirma que la promoción es gratis: `recipes/pipewire.toml` sella
b3:e240653adae656318d5c3445b36ffb37aa491589b5c34c8688515bc3806944ab, EXACTAMENTE el mismo que tenía
`incoming-cosmic/pipewire`. Era la variante que ya resolvía todo su cierre contra el catálogo padre.
Resuelve §NO-resuelve 2 del ADR 0015, que era el ticket que más desbloqueaba.
Tres síntomas que parecían distintos —apt sin poder bajar a `_apt`, pacman sin
poder chownear a `alpm`, pressure-vessel sin poder escribir su uid_map— eran la
misma causa: bwrap crea el userns con UN SOLO id.
La cura resultó tener TRES partes, y ninguna sobra:
1. `setcap cap_setuid+ep newuidmap` (+ cap_setgid en newgidmap). shadow.toml los
instala pero no los provisiona; sin la capability no escriben el mapa.
2. Crear el userns nosotros, mapear el rango de /etc/subuid con newuidmap y
pasárselo a bwrap con `--userns FD`. bwrap crea el suyo con un solo id A
PROPÓSITO y nunca llama a newuidmap: el trabajo es de quien lo invoca. El fd
lo abre la shell (`exec 3<…`), porque un fd sólo cruza el exec si no es
CLOEXEC y no valía la pena una dep de C para un fcntl.
3. Devolver las capabilities DENTRO del namespace. Ésta no estaba en el plan y
es la que costó: bwrap las tira todas, y en Linux ser root es tener
CAP_SETUID, no tener uid 0. Sin ella apt seguía sin poder seteuid(42) — un
síntoma que parecía de subuid y no lo era. Son seguras por construcción:
dentro de un userns sólo alcanzan lo que ese namespace posee, o sea nuestros
propios subuid. CAP_SYS_ADMIN queda fuera y sigue colgando de `nesting`.
MEDIDO después: uid_map de 65537 ids, setgroups: allow, apt instala SIN el
APT::Sandbox::User=root, pacman sincroniza con DownloadUser=alpm INTACTO, y el
userns anidado monta con root=true ⇒ el conflicto 2 de D9 se disuelve solo.
Dos cosas más que salieron por medir, no por pensar:
- El guardián MENTÍA. qorpa-preflight envolvía al hijo en `timeout`, que forkea,
así que newuidmap apuntaba al PID equivocado y el kernel respondía "Operation
not permitted" — un falso negativo idéntico a un fallo real. Decía que subuid
no andaba cuando a mano andaba. Ahora sale exit 0.
- Quitar el impuesto MUEVE el problema: el upper pasa a contener ficheros de los
subuid (apt deja los suyos con uid 165577) que nuestro uid no puede borrar ⇒
recreate entra a un userns mapeado para limpiar. Y cuando no hay rango, se
degrada diciendo la causa exacta en vez de quedar en misterio.
Y un detalle que no es cosmético: `--perms 1777` antes del `--tmpfs /tmp`, o el
_apt al que apt baja no puede escribir su fichero temporal. Un /tmp que no es
1777 no es /tmp.
2 tests nuevos (el rango se lee por usuario; CAP_SYS_ADMIN NO está en las caps
de root, o `nesting` dejaría de ser una decisión). 45/45.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U5cQtQrYNjWJVXVE6aEpQ2
Continúa lo que ya se hizo el 2026-09-02 con las 8 copias de gnome. Medido receta por receta con
`hammer hash`: de los 30 nombres que viven en el corpus Y en alguna cola, 13 dan el MISMO
ArtifactHash que su gemela del corpus y 22 son variantes deliberadas (glib/gtk4/pango de gnome,
dbus/openssl/libxml2 de kde…) que NO se tocan.
Se van las 13 redundantes: alsa-lib ×3, nasm ×2, zlib-shared ×2, y de incoming-kde ffmpeg,
fontconfig-shared, freetype-shared, libjpeg-turbo-shared, libpng-shared y vulkan-headers. Cada
consumidor las resuelve ahora en el catálogo PADRE, con el mismo hash.
VERIFICADO ANTES Y DESPUÉS, no argumentado: se snapshotearon los 47 consumidores directos, se
borraron las 13, y se re-hashearon. **0 consumidores vivos con hash distinto** (las 4 diferencias
son las propias recetas borradas, que se consumían entre sí). Cero rebuild.
EL EFECTO QUE NO SE BUSCABA Y ES EL MÁS VALIOSO: `build-state.json` guarda sus nodos POR NOMBRE, y
cuando un nombre vive en dos colas la membresía se consulta por el par `(cola, nombre)` de UNA sola
de ellas. Resultado: `ffmpeg`, `alsa-lib`, `vulkan-headers` y `zlib-shared` reportaban
`perfiles: []` —o sea "no está en ninguna imagen"— **mientras estaban en las cuatro**. Ahora dicen
la verdad, y `escritorio-kde` pasa de 183 a 185 porque aparecen dos nodos que el grafo no contaba.
La lección para el próximo análisis, que ya quedó en `scripts/vigia-sonames.py`: sobre el grafo se
pregunta con `yupana.membresia()`, que llavea por `(cola,nombre)`; el campo `perfiles` del JSON es
fiable sólo mientras el nombre viva en una sola cola.
Sin cambios en la deuda: cosmic sigue 105/106 por el OOM de xdg-desktop-portal-cosmic.
Las tres recetas pasan a `-Dpipewire-alsa=enabled`. Sirve a TODA app ALSA del corpus, no sólo a mpv:
un cliente que abre "default" desemboca en PipeWire por `libasound_module_pcm_pipewire.so` en vez de
pelearle la tarjeta al servidor. Es protocolo en vez de ABI compartida — la misma figura con la que
el ADR 0015 cruza el borde de la jaula.
EL FLAG SOLO NO HACE NADA, y ésta es la parte que no se ve venir: meson deja los plugins en
`/usr/lib/alsa-lib` (bien, ahí los busca alsa-lib) pero la CONFIGURACIÓN en
`/usr/share/alsa/alsa.conf.d`, **un directorio que alsa-lib no lee**. Sus `@hooks` cargan
`/var/lib/alsa/conf.d`, `/usr/etc/alsa/conf.d` y `/etc/alsa/conf.d` — leído del `alsa.conf` del
artefacto sellado, no supuesto. Sin el enlace que agrega la fase install, el plugin queda instalado
y no lo usa nadie: sellado e inerte, que es la familia de fallo del artefacto vacío. Se enlazan los
DOS ficheros: `50-pipewire.conf` hace que el destino EXISTA, `99-pipewire-default.conf` hace que sea
el destino POR DEFECTO — sin el segundo, mpv abriendo "default" no llega igual.
EVIDENCIA (test discriminante, porque "no falla" no prueba nada acá):
mpv --audio-device=alsa/pipewire → abre el device, falla al conectar (no hay demonio) ⇒ el PCM
ESTÁ DEFINIDO
mpv --audio-device=alsa/noexiste → "ALSA lib: Unknown PCM noexiste" ⇒ el control
El camino de verdad —que suene— sólo se puede ejercer con un PipeWire vivo, o sea en la sesión; eso
no se probó y no se afirma.
⚠ PRECIO, escrito para que no se diagnostique mal: con `99-pipewire-default.conf` puesto,
`pcm.!default` ES PipeWire. En una máquina donde PipeWire no esté corriendo, un cliente ALSA ya no
cae a la tarjeta: no suena. Es el trato que hace toda distro con pipewire-alsa.
Radio medido antes de tocar (`yupana radio pipewire`): 4 cosmic / 3 gnome / 2 kde. Reconstruidos 10
de 11 dependientes.
⚠ DEUDA DECLARADA, 1: `incoming-cosmic/xdg-desktop-portal-cosmic` murió DOS VECES por OOM (7 G de
RAM, sin cgroups, y otro agente compilando Rust a la vez — `dmesg` confirma «Out of memory: Killed
process (cargo)»). No es un fallo de la receta ni del cambio: es la máquina. Queda visible en el
grafo (escritorio-cosmic 105/106) en vez de escondido, y lo levanta el worker o un reintento con la
máquina libre.
Los otros tres perfiles cierran: KDE 183/183, GNOME 132/132, sway 140/140.
El OSC de mpv es `player/lua/osc.lua`: sin Lua el reproductor anda y responde al teclado pero NO
DIBUJA NADA de interfaz. La `lua` del corpus no sirve — `meson.build:701` acepta 5.1, 5.2 o LuaJIT
y el `lua.pc` genérico va acotado a `>=5.1.0, <5.3.0`; la nuestra es 5.4.8 y entró para wireplumber.
CÓMO CONVIVEN LAS DOS SIN PISARSE, que es lo que hace legítimo tener dos Lua: esta receta instala
**sólo la librería**. Sin `/usr/bin/lua`, sin `luac` y sin `lua.pc` genérico — que son exactamente
los tres ficheros que colisionarían si una imagen hidratara las dos. Las `.so` no colisionan porque
el SONAME lleva la versión, y los headers van a `/usr/include/lua5.2/` porque los de 5.2 y 5.4 se
llaman igual y dicen cosas distintas.
La diferencia con 5.4 que muerde: en 5.2.4 `luaconf.h:43-49` hace que `LUA_USE_LINUX` implique
`LUA_USE_READLINE` (en 5.4 eso vive en el target `linux-readline` del Makefile). Se esquiva
construyendo el target `a` —sólo `liblua.a`— en vez de `all`: `LUA_USE_READLINE` lo consume nada
más que `lua.c`, el binario del REPL, que acá no se compila. Sale gratis y de paso es lo que hace
que no haya `/usr/bin/lua` que colisione.
`-Dlua=lua5.2` y no `auto`: auto recorre ocho nombres de `.pc` y se lleva el primero que encuentre
en el sandbox ⇒ el artefacto dependería de qué otra cosa quedó montada en el lab.
EVIDENCIA, del mpv re-sellado (b3:d9072a10):
List of enabled features: … libass libplacebo **lua5.2** … wayland
[cplayer] Set property: user-data/osc/visibility="auto" -> 1
[cplayer] Done loading scripts.
o sea que el script del OSC no sólo compiló: se carga y corre.
alsa-lib: se corrige el comentario, que decía que las pipewire iban con `-Dpipewire-alsa=disabled`.
Ya no. Y queda escrito el gotcha que costó encontrarlo: el `alsa.conf` que instala carga
`/var/lib/alsa/conf.d`, `/usr/etc/alsa/conf.d` y `/etc/alsa/conf.d`, NO `/usr/share/alsa/alsa.conf.d`,
que es donde meson deja la config del plugin.
Cuatro escritorios que cierran y ninguna forma de abrir un video. El ADR 0015 ordenó los montones
y puso a mpv primero del A —lo que no tiene muro estructural, sólo falta escribirlo—. Éste es.
Sellada b3:8e6ab4e7. Evidencia de que ANDA, no sólo de que sella:
· `mpv --version` corre con el loader musl → v0.41.0, libplacebo v7.360.1, FFmpeg 7.1
· decodifica un h264 640x480 + AAC de prueba, 75 frames, `Exiting... (End of file)`
· `--vo=help` → `gpu-next` (libplacebo) y `gpu`; `--ao=help` → `alsa`
· `--gpu-context=help` → SÓLO `wayland` (Wayland/EGL). Ni un contexto X11 en el binario.
· NEEDED: libav*, libplacebo, libasound, libEGL, libc. Cero glibc, cero X11.
En el corpus y no en una cola: un reproductor lo quieren las cuatro imágenes, y una receta resuelve
sibling-first + catálogo padre, nunca una cola hermana.
`-Dbuild-date=false` no es cosmético: su default estampa la fecha de compilación DENTRO del binario
⇒ dos builds del mismo commit darían bytes distintos y el artefacto dejaría de reproducir.
`-Dprefer_static=true` (como foot) porque sin él meson resuelve los `.pc` sin `Libs.private` y el
link muere con símbolos `XML_*` sin definir «en libfontconfig.a» — culpando a fontconfig cuando lo
que falta es propagar expat.
Las ~90 opciones van EXPLÍCITAS: casi todas nacen `auto`, o sea que miran el sandbox y se prenden
con lo que encuentren. Misma trampa que ffmpeg cierra con `--disable-autodetect`.
TRES HUECOS CONOCIDOS, escritos en la receta para que no se descubran en la mano del usuario:
1. audio ALSA, y las tres pipewire llevan `-Dpipewire-alsa=disabled` ⇒ hoy no suena en un
escritorio con PipeWire vivo. Radio de darlo vuelta: 4/3/2. Es el próximo paso.
2. sin Lua ⇒ sin OSC. mpv 0.41 sólo acepta 5.1/5.2/LuaJIT y la del corpus es 5.4.8 (wireplumber).
3. sin vaapi ⇒ sin decodificación por hardware (libva está sólo en incoming-kde), y el ffmpeg
heredado va `--disable-x86asm` ⇒ tampoco SIMD.
`meson.build:29` de mpv la pide sin `required:` ⇒ es la segunda dep dura, junto con libass.
EL PROBLEMA REAL NO ERA COMPILARLA, era que upstream le entrega CINCO 3rdparty por submódulo git y
`[source]` de hammer no clona submódulos (sólo repo+commit o tarball+sha256). Resolución uno por uno:
· glad + jinja + markupsafe → recetas del corpus (commit anterior), como el `py3-glad` de Alpine
· Vulkan-Headers → sube al corpus, headers-only, hash idéntico (b3:7731a012)
· fast_float → upstream lo guarda con `fs.is_dir` ⇒ su ausencia es camino soportado
La sorpresa fue Vulkan-Headers: hace falta AUNQUE Vulkan vaya apagado. `src/vulkan/stubs.c` se
compila siempre y hace `#include <vulkan/vulkan.h>` — los stubs de «Vulkan no disponible» también
necesitan saber contra qué API no están.
Dinámica y no estática: `src/convert.cc` compila siempre ⇒ el artefacto lleva C++ y arrastra su
runtime. La `.so` se lo lleva puesto en vez de obligar a cada consumidor a pedir `-lc++`. Además es
lo coherente con mesa y ffmpeg, sus dos vecinos en el cierre de mpv.
Todas las perillas explícitas aunque el default sea `auto`: `auto` escanea el sandbox y activa lo
que encuentre — el mismo no-determinismo que ffmpeg cierra con `--disable-autodetect`.
Sellada b3:4346feca, con `pl_has_opengl=1` / `pl_has_vulkan=0` en el .pc y 8 símbolos `pl_opengl`
exportados. Vulkan queda pendiente a propósito: el loader arrastra el stack X11 entero por los WSI
y esta distro es Wayland-only ⇒ subirlo es una decisión con radio propio.
`meson.build:32` de mpv pide libass sin `required:` ⇒ no hay build de mpv sin esto.
libunibreak es "opcional" para upstream (`default=check`) y obligatorio acá: sin él libass parte
los renglones por espacios, que es justo lo que no sirve en japonés, chino o tailandés. Cuesta una
receta leaf de C plano.
`--enable-asm`: 234 símbolos SSE2/AVX2 en la `.a` sellada. Es la razón por la que `nasm` subió al
corpus en el commit anterior.
La lección de las deps, para la próxima receta que resuelva `.pc`: más de la mitad de `[deps]` no
aparece en el `./configure`. La harfbuzz del corpus va `-Dglib=enabled` (por GTK4) ⇒ su `.pc` pide
`glib-2.0`, que arrastra pcre2 y libffi. Y el fallo NO dice que falte glib: dice «Package
requirements (harfbuzz >= 1.2.3) were not met», culpando a la lib que sí estaba.
Selladas: libunibreak b3:a01fb6bd, libass b3:213df6b1.
libplacebo genera su `gl.h` con `python -m glad`, y glad le llega a upstream como submódulo git
(`3rdparty/glad`). El `[source]` de hammer no clona submódulos —sólo repo+commit o
tarball+sha256— así que glad entra como receta propia desde PyPI, que además pinea por sha256.
Es lo mismo que hace Alpine con `py3-glad` como makedepend.
Python puro las dos, mismo molde que `mako`/`markupsafe`: se copia el paquete a site-packages,
sin pip ni wheel. jinja2 es dep de runtime de glad; markupsafe ya estaba en el corpus.
El sdist de glad trae los XML de Khronos adentro ⇒ el generador no sale a la red, que es lo que
lo hace usable dentro del sandbox hermético.
Selladas: jinja2 b3:74a3aa9f (27 ficheros), glad b3:c1d773f2 (75 ficheros).
Primer paso del frente de apps (ADR 0015, montón A: mpv → OBS → Firefox). `mpv` va al corpus
porque lo quieren las cuatro imágenes, y una receta del corpus resuelve sibling-first en
`recipes/`: desde ahí NO alcanza `incoming-kde/` ni `incoming-cosmic/`. Sus tres deps de sistema
vivían sólo en colas.
Lo que hace que esto no sea duplicar: `hammer hash` da el MISMO ArtifactHash a cada par
(nasm b3:3624bdd8, alsa-lib b3:73fb200a, ffmpeg b3:bfef7bf3), porque las deps que las colas les
resolvían ya caían al catálogo padre. Cache hit, cero rebuild, y una imagen que arrastre las dos
recetas hidrata UN artefacto en vez de dos peleando por la misma ruta.
Verificado también que comentar la receta es gratis: las cabeceras nuevas no mueven el hash.
Tercer y cuarto hueco de RUNTIME del triaje. Sin polkit-qt-1, Plasma no puede
elevar privilegios: ni montar un disco, ni cambiar ajustes del sistema.
Son DOS recetas porque hacía falta una segunda polkit. Hammer resuelve las deps
hermano→padre —la cola propia, después recipes/— y nunca cruza a una cola hermana,
así que la polkit de incoming-gnome es invisible desde incoming-kde. Y aunque se
viera no serviría: aquélla va con -Dintrospection=true contra glib-introspected,
que es la ISLA DINÁMICA del shell de GNOME (gjs importa gi://Polkit y necesita el
typelib). KDE enlaza las libs desde C++ y el typelib le sobra; traerlo obligaría a
meter gobject-introspection entero en la cola.
La nueva es homónima a propósito, mismo patrón deliberado que dbus (incoming-kde vs
raíz) y gmp (incoming-kde vs incoming-cosmic), con el mismo coste conocido:
store-gc no puede decidir por NOMBRE cuál de los dos sellados es el vigente. No la
renombré a polkit-kde porque el .pc que busca polkit-qt-1 es polkit-gobject-1, no
el nombre del paquete: renombrar sólo movería el problema al lector.
Radio medido antes de escribirla (yupana radio polkit): la de incoming-gnome tiene
6 dependientes transitivos y no se toca; la nueva, 0. Cero daño colateral.
Nada de esto necesitó descubrirse: glib-shared ya existía en la cola KDE y su
propio encabezado nombraba a polkit como uno de los motivos por los que se escribió.
── El guardián que sí hizo falta ────────────────────────────────────────────────
polkit-qt-1 sondea polkit con check_function_exists, que COMPILA Y ENLAZA. Si ese
enlace falla por algo del sandbox —y no porque la función falte—, cmake NO da
error: imprime «You have an older polkit-1 version» y define
POLKIT_QT_1_COMPATIBILITY_MODE. El build sella, el artefacto pesa lo esperado y la
autorización queda recortada sin que nadie se entere. La fase configure ahora
aborta si HAVE_POLKIT_SYSTEM_BUS_NAME_GET_USER_SYNC no llegó a 1 en el header
generado: polkit 127 tiene esa función, así que un 0 significa «el sondeo no pudo
enlazar», no «polkit es viejo». Pasó: no hay modo compatibilidad.
También va -DQT_MAJOR_VERSION=6 explícito. El default del CMakeLists es "5", y sin
la flag construiría bindings con otros nombres (polkit-qt5-1) que el dependiente
no encontraría hasta mucho después.
── Lo que esto NO enciende, a propósito ─────────────────────────────────────────
Los dos consumidores siguen esquivando polkit igual que antes:
· kauth construye con el backend polkit OPCIONAL apagado (cae al backend fake);
· plasma-workspace apaga el helper de Región&Idioma con -DGLIBC_LOCALE_GEN=OFF
-DGLIBC_LOCALE_PREGENERATED=ON, que es justo lo que evita el PolkitQt6-1
REQUIRED.
Encenderlos es otra decisión con su propio radio (plasma-workspace arrastra medio
escritorio). La pieza está puesta; la palanca no se toca sin pedirlo.
⚠ Y con las librerías viaja la postura de seguridad que ya declaraba
arje-polkit-compat: el demonio responde is_authorized=true a TODO. Estas libs son
la INTERFAZ que el escritorio espera, no la política. Queda repetido en las dos
recetas nuevas porque son las que ponen la pieza en manos del escritorio.
Las dos REPRODUCEN bit a bit (verificar-repro.sh 2/2). Los NEEDED de
libpolkit-qt6-core-1 son exactamente los esperados: Qt6DBus, Qt6Core,
libpolkit-gobject-1, gio/gobject/glib y libc.
corpus 788/788 · escritorio-kde 171/171 · grafo CIERRA · gate --check OK
Queda 1 wanted en KDE: xwayland — y ése no es deuda técnica sino una decisión
(X11 al tacho, Xwayland = compat opcional por-imagen, no en el core).
Segundo hueco de RUNTIME del triaje de la frontera. Nadie lo pide para construir
—por eso el escritorio sellaba completo sin él— pero sin este módulo los controles
QML de Plasma (botones, sliders, combos, los menús de los applets y de los KCM) se
dibujan con el estilo genérico de Qt. No lo reemplaza qqc2-desktop-style: aquél es
el estilo de escritorio integrado con KStyle; éste es la implementación QML nativa
de Breeze, la que Plasma 6 usa por defecto.
Dos cosas que no eran obvias:
1. Se llama casi igual que el vecino y viene de otro tarball. qqc2-DESKTOP-style
sale de Frameworks 6.27.0; qqc2-BREEZE-style sale del release de Plasma 6.7.2,
el mismo de breeze/kwin/plasma-workspace. Copiar la URL del vecino da 404.
El sha256 va contrastado contra el .sha256 que publica KDE al lado del tarball,
no sólo contra lo que bajó acá: la primera descarga volvió con 0 bytes y su
sha256 era el de la cadena vacía — un vacío que se lee como éxito.
2. La lista de deps no es la del vecino copiada: sale de la clausura de
find_dependency de los KF6*Config.cmake que ya están en el store. Ese recorrido
enseñó algo reutilizable: casi todo lo que esos configs piden (X11, XCB,
Wayland, OpenSSL, BZip2, LibLZMA) vive dentro de un `if (NOT TRUE)` — la rama
de build ESTÁTICO — y por lo tanto es código muerto en nuestro corpus, que
compila KF6 dinámico. Por eso acá no hay libX11 ni xorgproto, coherente con
Wayland-only. El find_package(X11) del CMakeLists raíz no es REQUIRED, así que
falla en silencio sin romper el feature_summary(FATAL_ON_MISSING_REQUIRED).
Confirmado a posteriori: los NEEDED del plugin son exactamente los 5 KF6 que la
clausura predijo (KirigamiPlatform, IconThemes, ColorScheme, GuiAddons,
ConfigCore) más Qt6 Quick/Gui/DBus/Core.
La fase install verifica por CONTENIDO, no por presencia: qmldir de org.kde.breeze
y de org.kde.breeze.impl no vacíos, Button.qml presente y el plugin de plataforma
de kirigami instalado. Un módulo QML sin su qmldir ocupa disco y arranca con el
estilo genérico sin decir nada. Salieron 83 controles y 3 .so.
Selló al primer intento y REPRODUCE bit a bit (verificar-repro.sh 1/1).
corpus 788/788 · escritorio-kde 166/166 · grafo CIERRA · gate --check OK
Quedan 2 wanted en KDE: xwayland y polkit-qt-1.
Nota sobre el diff de los cinco grafos: cambian los `dependientes_total` de las
deps de esta receta en TODAS las vistas, no sólo en --kde. Es correcto y está
documentado en build-state.py: ese campo se calcula sobre el grafo entero (todas
las colas del disco), que es la corrección del bug de libdrm.
Primero de los cuatro huecos de RUNTIME del triaje de la frontera. Ninguna receta
lo pedía para construir (por eso escritorio-kde cerraba 163/163 sin él), pero sin
la base el escritorio no sabe qué es un fichero: ni asociaciones, ni iconos, ni
abrir-con. Qt6 y GLib lo leen los dos de /usr/share/mime.
Vive en recipes/ y no en incoming-kde: es freedesktop puro, y en la raíz lo ven
los cinco grafos (GNOME y COSMIC lo quieren igual — gdk-pixbuf hoy lo ESQUIVA con
-Dgio_sniffing=false).
Tres cosas que no eran obvias:
1. Fuente por GIT, no tarball. Freedesktop no publica downloads de release para
este proyecto: la API sólo ofrece los -/archive/ autogenerados de GitLab, que
no son estables byte a byte (mismo criterio ya escrito en wlr-randr). El pin
es el tag 2.5.1 PELADO — acá el tag es objeto tag, la trampa de las 4 del
mirror (ADR 0013).
2. i18n.merge_file de data/meson.build es INCONDICIONAL: no lo apaga
-Dbuild-translations=false, porque es el paso que PRODUCE freedesktop.org.xml,
el payload entero. Eso exige un msgfmt que entienda --xml. El de gettext-tiny
sirve y no degrada nada: desde 2.x el template ya es XML válido (las
traducciones se marcan con reglas ITS externas, cero ocurrencias de "<_"), así
que sin catálogos la salida de un msgfmt real ES el template. Verificado a mano:
copia byte a byte de los 384036 del template.
3. meson install deja SÓLO el XML fuente. Los consumidores no leen ese XML: leen
los índices que genera update-mime-database (mime.cache, globs2, magic,
aliases, subclasses). Sin ese paso el artefacto pasa toda verificación de
presencia y el escritorio sigue sin saber qué es un .png — la regla 3 del repo
en su forma exacta. La fase install lo corre contra /out y verifica por
CONTENIDO: mime.cache y globs2 no vacíos, image/png en types. Salieron 1038
tipos y 1448 globs.
Nace con strip_debug = true y REPRODUCE bit a bit (verificar-repro.sh: 1/1, cero
divergencias), caché binaria incluida. Estático, 0 NEEDED.
corpus 788/788 sealed · escritorio-kde 165/165 · grafo CIERRA · gate --check OK
Quedan 3 wanted en KDE: xwayland, polkit-qt-1, qqc2-breeze-style.
Deja de ser una plantilla con `commit = 000…0`. Tres cosas, y las dos últimas
son el valor del commit:
1. PIN. `v0.1.0` era el tag que la versión declaraba y NO SIRVE: su `Cargo.lock`
está desincronizado con sus manifiestos en el propio repo, y `cargo vendor
--locked` se niega ("cannot update the lock file … because --locked"). No es
de hammer: el vendoreo del fetch arranca bien y muere DENTRO de cargo.
Comprobado con `cargo metadata --locked` en los tres refs — v0.1.0
incoherente, v0.2.0 y main coherentes. Se pinea v0.2.0, PELADO con ^{commit}
porque es objeto tag (93253cc2, no 0beb83b7).
=> Un tag no sirve como pin sólo por existir: hay que verificar que su
lockfile cierre.
2. El requisito de `vendor/` en la fuente que pedía el comentario quedó VIEJO:
hammer vendorea en el FETCH a partir del Cargo.lock, host-side, y por eso el
`--offline --locked` del sandbox se cumple. El repo no trae vendor/ en ningún
tag y aun así construye.
3. ⚠ FASE `install` PROPIA, obligatoria con `--example`. El default de Cargo hace
`find target/release -maxdepth 1 -type f -perm -100`, y cargo deja los
ejemplos en `target/release/examples/`. El find no encontraba nada, salía 0 y
**hammer selló un artefacto SIN BINARIO**: 22 min de compilación y un `sealed`
con sólo `.hammer/recipe.toml` dentro, 20K. `hash --check` decía SELLADO y el
grafo lo habría contado como al día.
No lo atrapa `Store::has` ni el guardián de vacíos de build-state, porque el
directorio NO está vacío — tiene el manifiesto. Es la regla 3 del CLAUDE.md en
su peor forma: un ausente falla a gritos, esto llegó al final diciendo que
todo fue bien. Por eso la fase lleva un `test -x` que hace ruidosa la
ausencia.
Verificado por contenido, no por el `sealed`: 17 M, usr/bin/llimphi-counter, ELF
pie, NEEDED = libc.so (dinámico, como la receta promete para un binario que
dlopea Vulkan/Wayland). El artefacto falso se podó y quedó en el ledger.
`version` pasa de 0.1.0 a 0.2.0 y NO mueve el hash: no está en hash_inputs (como
`license`). La identidad la da el commit.
Corpus: 787/787 SELLADAS, 0 deuda, 0 never. Gate --check OK, grafo CIERRA.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4
gdm, gnome-session y gnome-settings-daemon salen de la cola y quedan aparcadas
en `recipes/incoming-gnome/.deferred/`, que es el mecanismo que el repo ya usa
(precedente: incoming-kde/.deferred/libXft.toml). El glob del worker es
TOP-LEVEL y el del grafo también, así que dejan de molerse cada ciclo y dejan de
contarse como deuda que nadie va a pagar. `targets.toml` ya no las lista desde
el 2026-08-07 («APARCADAS POR DISEÑO»); esto alinea el árbol con esa decisión.
Y el mapa que llevaban estaba mal en las dos direcciones:
1. «La terna GTK3» es un nombre engañoso: **GTK3 no es el muro**. GTK3 tiene
backend Wayland y se construye con -Dx11_backend=false. Autorarlo no habría
destrabado ninguna de las tres. Lo que bloquea de verdad:
- g-s-d 48.1: gtk+-x11-3.0 / x11 / xfixes INCONDICIONALES => imposible en
Wayland-only, no «pendiente».
- gnome-session 48.0: dependency('libsystemd', required: true) en meson:124
es una comprobación de pkg-config EN BUILD. arje-logind-compat no la
satisface y es a propósito — su receta explica que GNOME consulta login1
en RUNTIME por D-Bus y que no hace falta la C-ABI sd-login. Único camino:
parchear ese required a false, que es una decisión, no un arreglo.
- gdm: cuelga de gnome-session, y con mirada-greeter probablemente sobra.
2. La lista «FRONTERA» estaba VIEJA: ya existen libX11, libXfixes, xorgproto,
libXau/Xcursor/Xdmcp/Xext/Xi/Xrender/Xtst, libxcb (casi todas por KDE) y
polkit, upower, geocode-glib, libgweather. Cuatro de las ocho líneas de g-s-d
habían dejado de ser ciertas. Faltan de verdad: gtk3, libnotify y una
variante libcanberra-gtk3.
Verificado contra el TARBALL (sha256 = el del pin), no contra el comentario.
incoming-gnome queda en 79 recetas, sellado=79 deuda=0 nunca=0.
Gate --check OK, grafo CIERRA, escritorio-gnome sigue 119/119.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4
`fontconfig-shared`, `freetype-shared`, `libjpeg-turbo-shared`, `libpng-shared`,
`libtiff-shared`, `libxml2-shared`, `libyaml-shared` y `zlib-shared` existían a
la vez en `incoming-gnome/` y en `recipes/`, y las ocho daban el MISMO
ArtifactHash que su gemela del corpus. Como la resolución de deps cae al padre
cuando no hay hermano, quitarlas deja a las consumidoras resolviendo contra
`recipes/` y con el mismo hash.
Medido, no supuesto: se hashearon las 1144 recetas antes y las 1136 después, y
de las 1136 supervivientes **0 cambiaron de hash**. No hay rebuild.
Lo que NO se toca, y conviene que quede dicho porque se parece:
- Las variantes de la ISLA DINÁMICA (glib, gtk4, gdk-pixbuf, pango, harfbuzz,
graphene, libadwaita, json-glib, libusb, libxcvt, pipewire, pulseaudio,
wireplumber, xdg-desktop-portal): mismo nombre, hash DISTINTO. Son sombras a
propósito — la introspección de GNOME exige .so reales.
- Las copias entre COLAS (alsa-lib, hwdata, xkeyboard-config, libdisplay-info,
lcms2, icu4c, lua, nasm, fuse3, libsndfile, libelogind, hicolor-icon-theme,
dbus-shared): también dan el mismo hash, pero NO sobran. Cada cola necesita su
propio hermano para cerrar su clausura; borrar la de una rompe esa cola. No es
el caso de onda-2, donde la cola entera duplicaba a otra.
Y el criterio, otra vez: `pipewire` y `pulseaudio` son TEXTUALMENTE idénticas a
las de COSMIC y sellan distinto, porque sus deps resuelven distinto según la
cola. El fichero no dice la verdad; el hash sí.
Gate --check OK, grafo CIERRA, escritorio-gnome sigue 119/119. La cola queda en
82 recetas (79 selladas + la terna GTK3) y sus 5 parches, todos en uso.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4
Yo mismo las reporte como "la misma clase de colision que onda1" y me pase: no
lo son. onda1 tenia una copia RANCIA del mismo build; esto son dos builds
distintos, cada uno correcto para su frente:
KDE link=dynamic --enable-shared --enable-cxx => libgmp.so + libgmpxx.so
COSMIC link=static --disable-shared => libgmp.a, sin C++
Las dos citan libqalculate y llegan a conclusiones opuestas sobre gmpxx.h, y las
dos tienen razon para su cola. Unificarlas romperia una de las dos.
Lo que compartir nombre SI cuesta es que hace ambigua la clasificacion de
store-gc: un artefacto rancio de una puede caer como SUPERADO porque la otra
tiene sellado vigente. La condicion es precisa y esta medida — solo hay riesgo si
el hash vigente de una NO esta sellado, y hoy las dos lo estan => exposicion
CERO. Antes de podar con nombres compartidos hay que comprobar ESO, no el nombre.
La salida, si algun dia molesta, es renombrar la dinamica a gmp-shared (la
convencion que el corpus ya usa: zlib-shared, cairo-shared...). No se hizo
porque cuesta: da 7 sellados de KDE que caen a deuda. es
el mismo par por la misma razon.
Los comentarios NO entran en hash_inputs (medido: mismo hash antes y despues),
asi que esto no re-hashea nada.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4
Al verificar la resolucion estricta de parches salio la unica referencia rota
del arbol: `.deferred/kalker.toml` pide `system-libs.patch` y el fichero estaba
en `recipes/`. Los patches se resuelven `base_dir.join(nombre)` sin fallback, asi
que la receta nunca lo habria encontrado.
No es una rotura nueva: `.deferred/system-libs.patch` no existio jamas (el
importador dejo la receta en .deferred y el parche en recipes/). Y es
inequivocamente de kalker — parchea `kalk/Cargo.toml` — ademas de que ninguna
receta lo citaba desde recipes/, o sea que ahi era un huerfano.
Ningun hash se mueve por esto. Referencias de parche: 95 resuelven, 0 rotas.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4
Era la cola de staging de las tandas base-system/foot y sus recetas ya se
habian PROMOVIDO a canonicas commit a commit ("promover 7 tier-2", "promover
cadena crypto", "promover tllist/utf8proc/scdoc/fcft/foot"). Lo que quedaba
eran 0 .toml y 26 .patch: 5 copias byte a byte de la version canonica de
recipes/ y 21 de recetas que ya no viven ahi.
No eran "probablemente inutiles": son inalcanzables POR CONSTRUCCION.
`fetch::apply_patches` resuelve `recipe.base_dir.join(nombre)` y `base_dir` es
el directorio de la receta, sin fallback a `recipes/` — un parche en una cola
sin recetas no lo puede pedir nadie.
⚠ El corolario queda escrito en farm-worker-loop.sh: al promover una cola, los
`.patch` NO viajan con la receta, y la cola queda en un estado que PARECE vivo
(tiene ficheros) sin moler nada.
Sale de QUEUES en farm-worker-loop.sh y de la lista de promote de farm-down.sh.
`incoming-go` se queda en las dos aunque el directorio no exista: es la cola
aislada del frente Go y el guard `ls $Q/*.toml` salta las vacias; quitarla seria
dejarla muerta el dia que ese frente la recree.
Verificado con la regla ESTRICTA de resolucion de parches sobre todo el corpus:
94 referencias resuelven, 0 rotas. (El chequeo que use al jubilar onda-2 era mas
laxo que hammer — aceptaba recipes/<parche> como fallback; su conclusion se
sostiene, pero el metodo queda corregido.)
Gate --check OK, grafo CIERRA, corpus 786/787.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4
El caso LIMPIO del mismo fenomeno que onda-1, sin ninguna de sus asperezas: las
22 recetas son byte a byte identicas a las de incoming-gnome Y las 22 dan el
MISMO ArtifactHash, o sea la misma direccion del store. Sin divergencias, sin
colision de nombre y sin un solo artefacto que podar al retirarla.
La comprobacion que decide NO es `diff` sino `hammer hash`: la resolucion de
deps es hermano->padre, asi que dos ficheros identicos en colas distintas pueden
sellar distinto. Aca coinciden los 22.
Se va tambien `cairo-ctime-r.patch`, verificado identico al de incoming-gnome, y
comprobado que ninguna receta de ninguna cola queda apuntando a un parche
inexistente.
El andamio ya no sostenia nada: el trabajo de la isla dinamica vive en
incoming-gnome, que es la cola que el perfil usa. Sale de QUEUES en el mismo
commit, por la simetrica de la regla que ese fichero documenta.
Gate --check OK, grafo CIERRA, escritorio-gnome sigue 119/119, corpus 786/787.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4
Cierra la jubilacion empezada con gnome-desktop. Las 5 recetas que quedaban:
- glib, glib-introspected, gobject-introspection, py3-setuptools: byte a byte
identicas a las de incoming-gnome Y con el MISMO ArtifactHash, o sea que
sellaban en la misma direccion y entraban por cache-hit. Coste de build cero;
coste real: un segundo sitio donde editar, que se separa en silencio.
- gsettings-desktop-schemas: la unica distinta, y es la version ANTERIOR al
2026-07-28 (introspection=false + link=static). La vigente lo activo porque el
gir de Meta de mutter incluye GDesktopEnums-3.0.gir, que sale de ahi, y sin el
el g-ir-scanner corta en el ultimo target de mutter (721/722).
Lo caro no era el tiempo sino el NOMBRE: el gsettings viejo competia con el
bueno, dos artefactos homonimos con hash distinto en el store. Podado el
huerfano (0fb1703d, 155 KiB exclusivos) y anotado en el ledger anti-churn; los
otros cuatro artefactos siguen vigentes porque los produce incoming-gnome.
Y al jubilar gnome-desktop las dos hojas de la cola se habian quedado sin
consumidor: la cola se alimentaba a si misma y terminaba en el aire.
Sale tambien de QUEUES en farm-worker-loop.sh, por la simetrica de la regla que
ese fichero ya documenta dos veces: una cola ausente de la lista no da error,
da silencio — y una cola retirada que sigue en la lista, tambien.
Gate --check OK, grafo CIERRA, escritorio-gnome sigue 119/119, corpus 786/787.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4
Mismo paquete que `recipes/incoming-gnome/gnome-desktop.toml`: name, version
44.5 y sha256 identicos. La unica diferencia son las `[deps]`, y son las de
antes: a la de onda1 le faltan xkeyboard-config, iso-codes, libseccomp y todas
las variantes `-shared` que la canonica documenta como imprescindibles para el
scanner de introspeccion. Por eso moria con
Run-time dependency xkeyboard-config found: NO (tried pkg-config)
meson.build:64:17: ERROR: Dependency "xkeyboard-config" not found
que se leia como deuda de build y no lo era: arreglarla habria sido reescribirla
hasta volverla identica a la otra.
Nadie dependia de ella (los dependientes -mutter, gnome-shell, gnome-session,
gnome-settings-daemon, xdg-desktop-portal-gnome- viven en incoming-gnome y
resuelven contra su propia hermana). El gate `--check` sigue en OK y ningun
nodo cambia de estado; en los grafos solo baja `dependientes_total` de sus
deps, que es justo lo que tenia que pasar.
Sus dos artefactos quedaban sin receta que los produjera: podados y anotados en
el ledger anti-churn para que la cosecha no los devuelva.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4
Los 8 ejecutables decian static y enlazaban musl dinamicamente. La huella que lo
delata sin mirar el build: `readelf --dyn-syms` daba 21475 entradas contra ~2700
de un binario dinamico normal — eso es --export-dynamic, no un enlace corriente.
NO venia del meson de gtk (ahi no aparece la palabra) sino de un .pc DE
DEPENDENCIA: `gmodule-2.0.pc` trae literalmente `Libs: -Wl,--export-dynamic`, y
`meson.build:426` pedia `dependency('gmodule-2.0')`. El wrapper de cc del lab
(`sandbox.rs:597`) tira el `-static` ante --export-dynamic ⇒ lo ponia el lab y
lo sacaba el lab, por un flag heredado de una dep.
No hace falta parchear el .pc de glib: upstream ya publica la misma libreria sin
el flag. `gmodule-no-export-2.0.pc` trae el `-lgmodule-2.0 -pthread` de verdad y
los otros dos solo hacen Requires sobre el añadiendo --export-dynamic. Es la
palanca prevista, no un apaño — de hecho `gio-2.0.pc` de la propia glib ya usa
la variante no-export.
REGLA GENERAL: cualquier receta que enlace gmodule hereda --export-dynamic y
pierde el -static SIN AVISO. Si link=static no pega y hay glib de por medio,
mirar los .pc de las deps antes que el build del proyecto.
Y entonces aparecio un bug que SOLO existe en estatico: los 8 morian con SIGSEGV
hasta en `--help`. Backtrace con gdb:
g_module_symbol (module=0x0, symbol_name="gtk_progress_get_type")
_gtk_module_has_mixed_deps (module_to_check=0x0) at gtk/gtkmain.c:474
do_pre_parse_initialization () at gtk/gtkmain.c:498
gtk_init_check () at gtk/gtkmain.c:635
`_gtk_module_has_mixed_deps(NULL)` hace `g_module_open(NULL,0)` y pasa el
resultado a `g_module_symbol` sin comprobarlo. En musl ESTATICO `dlopen(NULL)`
devuelve NULL siempre ⇒ deref de nulo en el offset 8, en el arranque. Con libc
dinamica el bug no se ve nunca. Cuarto parche de la receta: la comprobacion que
falta. Devolver FALSE es correcto ademas de seguro — en un estatico no hay
modulos cargables, asi que no puede haber simbolos de GTK 2/3 mezclados.
Control: 423 ficheros intactos, NEEDED=0 en los ocho, .dynsym de 21475 a 0, los
ocho arrancan, gtk4-path-tool hace trabajo real, y `builder-tool validate`
devuelve EXACTAMENTE el mismo «Could not initialize windowing system» que daba
el binario dinamico anterior.
Cae en cascada el re-hash de 7 dependientes; se reconstruyen a continuacion.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
No era purismo. El binario sellado, ejecutado fuera del sandbox:
Error relocating /lib/libncursesw.so.6: __vfprintf_chk: symbol not found
Salia dinamico contra `libncursesw.so.6` y `libc.so`, y NINGUN artefacto del
corpus provee esas dos —el de ncurses solo empaqueta .a—, asi que solo corria
dentro del rootfs de Alpine del sandbox. Esta en los perfiles base, cli,
escritorio-cosmic y escritorio-sway. Mismo sintoma que curl en su dia.
Causa: `configure.ac:1223` pone `LOCAL_LDFLAGS=-rdynamic` en linux* (para que
`enable -f` pueda resolver simbolos de bash), y el wrapper del lab quita el
`-static` ante `-rdynamic`. Es la misma raiz que htop y e2fsprogs.
`--enable-static-link` SOLO NO ALCANZA: pone `STATIC_LD=-static`
(`Makefile.in:170`) pero queda en la misma linea que el `-rdynamic` de
`BASE_LDFLAGS`, y el wrapper mira la linea entera. Van los dos.
PRECIO MEDIDO, y es un cambio de contenido: el artefacto pasa de 122 a 16
ficheros. No es accidente sino acoplamiento de upstream — `configure.ac:981`
salta `AC_CHECK_LIB(dl, dlopen)` cuando `opt_static_link=yes`, y sin
`HAVE_DLOPEN` no se construyen ni instalan los ~40 cargables de `usr/lib/bash/`,
sus 62 headers ni `usr/lib/pkgconfig/bash.pc`. Es correcto: un binario estatico
no puede dlopen, e instalarlos seria publicar una capacidad inexistente. Y no se
pierde nada que funcionara: dinamico no eran «cargables que funcionan», era «un
bash que no arranca». `yupana radio bash` da 0 dependientes.
Control: NEEDED=0, arranca, dice 5.3.0(1)-release (x86_64-pc-linux-musl) y pasa
arrays, asociativos, aritmetica, sustitucion de comandos y `=~`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
`e2fsck/Makefile.in:119` enlaza con `$(RDYNAMIC)` y es el UNICO de las 31
herramientas que lo hace (`configure.ac:109` lo pone en `-rdynamic`; ningun otro
Makefile.in del arbol lo menciona). El wrapper de cc del lab
(`sandbox.rs:597`) tira el `-static` inyectado ante `-rdynamic`, asi que e2fsck
—y sus tres alias fsck.ext{2,3,4}— salian dinamicos mientras las otras 27
herramientas del mismo build salian estaticas. Eso explica exactamente el
«4 de 31» que reporto el audit estricto.
Upstream pide `-rdynamic` para simbolizar el backtrace del manejador de senales
fatales, no para enlazar. Con estatico el backtrace pierde nombres, que es lo
que `link = "static"` ya pedia.
`RDYNAMIC=` va en compile Y en install (install relinkea).
Control: NEEDED=0 en los cuatro, los 149 ficheros del artefacto intactos, y
prueba funcional real — mke2fs crea un ext4 de 16M y e2fsck lo recorre las cinco
pasadas limpio.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
La receta decia `link = "static"` y el audit marcaba `pahole`. Mirando el
artefacto entero no era uno: eran los DIEZ ejecutables, contra cinco librerias.
El `-static` NUNCA faltaba. `build/CMakeFiles/pahole.dir/link.txt` —donde cmake
deja la linea de enlace literal— lo tenia puesto en la posicion 9, y mas
adelante en la misma linea habia un `.so` de ruta absoluta; el wrapper de cc del
lab (`sandbox.rs:597`) quita el `-static` ante cualquier `.so` en la linea, asi
que lo ponia cmake y lo sacaba el wrapper. Ese fichero es el sitio donde mirar
la proxima vez que `link = "static"` no pegue en un proyecto cmake.
Eran TRES `find_*` metiendo `.so`, no uno, y cayeron de a uno:
· FindDWARF.cmake → libelf.so ⇒ -DDWARF_LIBRARY/-DELF_LIBRARY a los .a
· find_package(ZLIB) (CMakeLists:52) → /usr/lib/libz.so ⇒ -DZLIB_LIBRARY
Sin `-static`, ademas, `-llzma`/`-lbz2` resolvian a las .so DEL ROOTFS de
Alpine y no a los artefactos de xz/bzip2 (que solo traen .a): entradas no
declaradas, de las que rompen el cono de affected.py.
`-DCMAKE_FIND_LIBRARY_SUFFIXES=.a` NO sirve y se probo: Platform/Linux.cmake lo
asigna como variable NORMAL en `project()` y esa tapa a la del cache.
Control, ademas de NEEDED=0 en los diez y la lista de ficheros intacta: el
pahole nuevo ARRANCA en el host (el viejo moria con `Error relocating
/lib/libelf.so.1: rawmemchr: symbol not found`), dice v1.30 y emite una seccion
.BTF real con `-J`, que es lo que hace CONFIG_DEBUG_INFO_BTF. Con eso el punto 3
de la deuda de SDD 25 H7 queda pagado y la herramienta probada de punta a punta.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
El `-lwayland-client` que salia de pkg-config resolvia a libwayland-client.so.0
y `link = "static"` era mentira (audit 2026-08-31). `--prefer-static` hace que
meson pida `pkg-config --static` y prefiera los .a; el artefacto de wayland
trae libwayland-client.a, asi que hay con que.
De paso saca el .so de la linea de enlace, que es lo que hacia que el wrapper
del lab tirara el `-static` inyectado (`sandbox.rs:597`) — misma raiz que htop.
Control: NEEDED=0 en todos los ejecutables ELF, lista de ficheros intacta.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
QUINTA causa del frente link=static, y la unica que no es culpa de la receta:
`Makefile.am:243` hace `AM_LDFLAGS += -rdynamic` bajo `if HTOP_LINUX`, y el
wrapper de cc del lab (`sandbox.rs:597`) quita el `-static` inyectado ante
`-rdynamic`/`--export-dynamic`/un `.so` en la linea. La regla del wrapper es
correcta —`-static` no se sostiene contra un link intrinsecamente dinamico—,
pero el `-rdynamic` de htop es solo para simbolizar el backtrace del manejador
de fallos, no una necesidad de enlace.
Efecto que tenia: `-lncursesw` resolvia a la libncursesw.so.6 DEL ROOTFS de
Alpine en vez del artefacto de ncurses (que solo trae .a) — una entrada no
declarada, de las que rompen el cono de affected.py.
`AM_LDFLAGS=-static` va en compile Y en install: si `make install` relinkea sin
el override devuelve el binario dinamico (la leccion de parted).
Seguro: el UNICO dlopen del arbol esta en linux/LibSensors.c y la receta ya
pasa --disable-sensors; delayacct queda en `no` por no estar libnl en [deps].
Control: NEEDED=0 en todos los ejecutables ELF, y la lista de ficheros del
artefacto no perdio nada contra el sellado anterior.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
Sin CONFIG_MEMCG el fichero memory.max no existe y arje-incarnate descarta el error: una
Card pedía un tope de memoria y corría sin ninguno, dejando sólo un warn!. El x86_64_defconfig
de 7.1 ya no trae MEMCG=y, así que había que pedirlo explícito.
Va en tanda con PSI (que el contrato declara como wants) para pagar de una sola vez el
re-hasheo: la fase configure ES la identidad del artefacto. linux.toml queda intocado a
propósito — es el baseline of_tree del selfhost-verify y no hospeda Cards.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACUcwo9mZsE5ocYVE9npih
Sexta de la familia. El arreglo estaba HECHO en disco desde hace horas y lo
reporté como cerrado, pero nunca llegué a commitearlo — sólo se vio al revisar
`git status` por otra cosa. Sin esto, el arreglo se perdía.
El wrapper del sandbox (commit anterior) tapaba el síntoma. Esto arregla la causa:
nuestra receta de binutils no pasaba el flag que Alpine SÍ pasa, así que su `ar` y su
`ranlib` estampaban la hora de pared en la cabecera del miembro `/` de cada `.a`.
POR QUÉ AHORA Y NO "AGENDADO", Y POR QUÉ ESTE ORDEN. Arreglar la raíz PRIMERO evita
la parte fea del arreglo por wrapper: al cambiar el hash de binutils (c316212f… →
4b5fbf3b…) los artefactos contaminados caen en DIRECCIONES NUEVAS. No hay transición
"mismo ArtifactHash, otros bytes" — que es justo el peligro que la huella del lab
existe para eliminar. Los viejos quedan superados y podables, no reemplazados en el
sitio. Re-sellar en el sitio habría sido la opción peligrosa.
COSTE, MEDIDO CON yupana radio (no estimado): 422 dependientes transitivos, 365
sellados caen a deuda, las siete imágenes. Se asume a propósito: el corpus ya está en
campaña de reconstrucción (1093/1125) y la cascada se arrastra sola, porque cada
`hammer build` construye sus deps.
El wrapper del sandbox se mantiene como defensa en profundidad: es byte-neutral para
todo lo que ya sellaba con mtime 0, y cubre a cualquier herramienta futura que archive
sin pedir permiso.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LykB7vw38b4Ck1Zij15RQ4
`unable to find static system library 'bz2'` en el link, a los 2612 s (43 min).
La receta no tenía `[deps]`. Decimotercera de la familia.
NO fue OOM pese a que la RAM estaba en 222 Mi libres cuando lo comprobé: dmesg
limpio. Conviene dejarlo escrito porque la sospecha era razonable y falsa.
Falla con las dos: `unable to find static system library 'lzma'` y `… 'bz2'`,
en el link a los 1331 s. La receta no tenía `[deps]`.
Duodécima de la familia. Sin verificar (mismo criterio que mise/pixi): el
patrón lleva once arreglos idénticos y comprobarlo cuesta 22 min de campaña
parada.
Falla con las dos: `unable to find static system library 'lzma'` y `… 'bz2'`.
La receta no tenía `[deps]`. Undécima de la familia.
Acá la dep es evidente en retrospectiva: `rga` busca DENTRO de archivos
comprimidos, así que necesitar lzma/bz2 es lo esperable — pero el Cargo.toml no
las nombra (las arrastran sys-crates) y sólo aparece en el link, a los 468 s.
Sin verificar (mismo criterio): el patrón lleva diez arreglos idénticos.
`unable to find static system library 'bz2'`. La receta no tenía `[deps]`.
Décima de la familia (appstream, cargo-deb, cargo-make, dprint, dufs, fnm,
macchina, maturin, mise).
⚠ El fallo sale en el LINK, al final, y este build tarda 87 MINUTOS en llegar
(5226 s). Una dep de una línea que falta cuesta hora y media de máquina antes
de decir nada.
Sin verificar a propósito (mismo criterio que mise): comprobarlo costaría otra
hora y media de campaña parada, y el patrón lleva nueve arreglos idénticos
verificados.
`unable to find static system library 'bz2'`. La receta no tenía `[deps]`.
Novena de la familia (appstream, cargo-deb, cargo-make, dprint, dufs, fnm,
macchina, maturin).
⚠ Sin verificar a propósito: el build tarda ~60 min (falló a los 3550 s) y el
flock es exclusivo, así que comprobarlo cuesta una hora de campaña parada. El
patrón lleva ocho arreglos idénticos verificados y la campaña lo verifica sola
en la siguiente pasada.
`unable to find static system library 'lzma'`. La receta no tenía `[deps]`.
Octava de la familia (appstream, cargo-deb, cargo-make, dprint, dufs, fnm,
macchina). Verificado: sella, 0 librerías faltantes, binario en /usr/bin.
⚠ Nota de método: verificar ESTA costó ~20 min de campaña parada, porque el
build tarda más que su fallo (1033 s) y el flock es exclusivo. Para el resto de
esta familia —arreglo de UNA línea con el patrón ya verificado 8 veces— sale
más barato commitear y dejar que la campaña lo verifique en su siguiente
pasada.
`unable to find static system library 'sqlite3'`. La receta no tenía `[deps]`:
el lab trae el toolchain de Rust pero no las libs C que los crates `*-sys`
esperan del sistema. `sqlite` está sellada y aporta /usr/lib/libsqlite3.a.
Séptima de la misma familia (appstream, cargo-deb, cargo-make, dprint, dufs,
fnm). Verificado: sella, 0 errores de librería faltante.
Cadena completa, cuatro recetas nuevas y una modificada:
musl-obstack -> libobstack (find_package(obstack REQUIRED))
argp-standalone -> libargp (find_package(argp REQUIRED))
musl-fts -> fts(3), que usa libdwfl/linux-kernel-modules.c
elfutils-libdw -> libdw + libdwfl (VARIANTE: la canónica sólo hace libelf
porque de ella cuelgan los cuatro kernels)
Lo que costó, en orden de aparición:
· libdw a secas NO necesita argp/obstack/fts — la nota de la canónica es cierta
de libdwfl y src/, no de libdw. El `obstack` de libdw estaba en un COMENTARIO.
· Pero dwarves SÍ usa libdwfl (15 dwfl_* en dwarf_loader.c) ⇒ vuelven argp y fts.
· libdwfl no compila suelto: hay que seguir el orden de SUBDIRS del raíz
(lib → libelf → libcpu → backends → libebl → libdwelf → libdw → libdwfl →
libdwfl_stacktrace).
· El compat/argp.h de la canónica TAPA el argp real y no declara argp_failure.
· libdw.a GORDA: FindDWARF.cmake pone DWARF_LIBRARIES = libdw + libelf y nada
más, pero libdw necesita libdwfl/libebl/libdwelf/backends/libeu, que son libs
internas. Se fusionan en un archive (patrón de la libgtk-4.a) junto con un
`error()` real, que musl no trae y el shim de la receta sólo daba inline.
· Y al final, lzma/bz2: libdw descomprime .debug_* comprimidas.
Verificado de verdad, no por exit code: pahole v1.30 arranca (con el loader
musl del lab) y DECODIFICA DWARF — sobre un binario de prueba imprime los
offsets, los tamaños y hasta el agujero de 3 bytes de padding.
dwarves llama a 15 `dwfl_*` distintas desde dwarf_loader.c, así que libdw sola
no alcanza. libdwfl SÍ arrastra argp (argp-std.c) y fts (linux-kernel-modules.c)
⇒ la receta declara `argp-standalone` y `musl-fts`. Esos objetos casi nunca se
enlazan (dwarves no usa dwfl_standard_argp ni dwfl_linux_kernel_*) pero tienen
que COMPILAR.
Dos cosas que costaron:
· libdwfl no se puede construir suelto: su Makefile lee los .manifest de otros
subdirs. Hay que seguir el orden de SUBDIRS del raíz:
lib → libelf → libcpu → backends → libebl → libdwelf → libdw → libdwfl.
· `rm -f compat/argp.h`: el shim de la canónica es sólo tipos/decls y no declara
`argp_failure`, que usa argp-std.c. Como esta variante trae argp de verdad, hay
que BORRAR el shim para que gane el /usr/include/argp.h real — `-I$PWD/compat`
va primero y si no lo tapa.
Tercera pieza del frente libdw/musl. `libdwfl` de elfutils usa fts en
linux-kernel-modules.c, y dwarves necesita libdwfl.
⚠ Sus símbolos casi nunca se ENLAZAN (dwarves no llama dwfl_linux_kernel_*, así
que ese objeto no sale del archive), pero el fichero tiene que COMPILAR: hace
falta un <fts.h> real con los campos y constantes que usa. Escribir ese header
a mano es fácil de hacer mal en silencio; traer la implementación real es más
honesto.
Se compila a mano como musl-obstack. Gotcha menor: fts.c hace
`#include "config.h"` con COMILLAS (obstack.c lo hace con ÁNGULOS).
Tercera pata del frente libdw/musl, para `dwarves`.
VARIANTE y no tocar `recipes/elfutils.toml`: de la canónica cuelgan los CUATRO
kernels (su tools/objtool enlaza -lelf) y moverle el hash los invalidaría.
Mismo patrón que openssl-threads.
⚠ La canónica avisa que «libdw/libdwfl arrastran argp/obstack/fts, lo
verdaderamente difícil en musl». Es cierto de `libdwfl` y de `src/`, pero NO de
`libdw` a secas, que es lo único que pide dwarves. Verificado sobre el fuente:
· `argp_`/`fts_open` → sólo en libdwfl/, CERO en libdw/.
· `obstack` → en libdw/ aparece UNA vez y es dentro de un COMENTARIO de
libdwP.h: elfutils lleva su propia «simplified thread-local reimplementation
of obstacks» y no incluye <obstack.h> en ningún sitio.
⇒ los stubs de compat/ que ya montaba la receta bastan; libdw compila tal cual.
Verificado: sella y libdw.a exporta 129 símbolos `dwarf_*` reales
(dwarf_begin, dwarf_begin_elf, dwarf_getelf…), más dwarf.h/libdw.h/known-dwarf.h.
Segunda pata del frente libdw/musl: `dwarves` hace `find_package(argp REQUIRED)`
y musl no trae argp. Es lo que Alpine empaqueta como `argp-standalone`.
Se compila a mano (sin autotools, como musl-obstack) y SÓLO los siete
`argp-*.c` que el propio Makefile.am pone en `libargp_a_SOURCES`. Los otros .c
del tarball (mempcpy/strchrnul/strndup/strcasecmp) son LIBOBJS de autoconf —
reemplazos para sistemas que NO los tienen— y musl SÍ los tiene, así que
incluirlos duplicaría símbolos de la libc.
Gotcha: `UNUSED` no es del código, lo define el `acinclude.m4` de autoconf. Sin
él las firmas `char* arg UNUSED` no parsean y salen `expected ')'` y varios
`undeclared identifier 'state'` — errores que parecen del código y son de
config. Va en el config.h a mano.
Primera pieza del frente libdw/musl (para `dwarves`). La necesitan DOS cosas:
`libdw` de elfutils (su libdwP.h mete `struct obstack` en el Dwarf) y el propio
`dwarves`, que hace `find_package(obstack REQUIRED)`. Es lo que Alpine
empaqueta como `musl-obstack`.
Se compila A MANO: el tarball trae Makefile.am pero no un `configure` generado,
y el corpus no tiene autoconf/automake/libtool. Son dos .c.
Dos gotchas resueltos:
· `obstack.c` hace `#include <config.h>` SIN guarda (fuera de _LIBC) ⇒ hay que
fabricarle uno. Lo único que mira es HAVE_LIBINTL_H, que musl no tiene, así
que el config.h mínimo vale.
· Es `<config.h>` con ÁNGULOS, no comillas ⇒ no basta con dejarlo en el cwd,
hace falta `-I.` en el compile.
El nombre importa: dwarves lo busca con `find_library(NAMES obstack)` en
/usr/lib ⇒ instala /usr/lib/libobstack.a y obstack.h.
Los añadí durante el diagnóstico y luego los verifiqué uno a uno quitándolos:
la receta construye igual sin ellos. Dejarlos era cargo-cult, y encima el
comentario afirmaba cosas que mis propias pruebas ya habían desmentido.
La causa real es UNA y las dos únicas piezas necesarias siguen ahí: quitar la
dep `linux-headers` (Linux 6.16 tapando los headers 7.1 del rootfs) y el
cherry-pick de upstream para el `io_uring_buf_reg` de 7.1.
Ambos descartes quedan escritos en la receta para que nadie los re-intente:
`--enable-bundled=yes` no puede ganar porque zig-cc impone sus headers por
delante de cualquier `-isystem`, y la versión de zig es irrelevante porque los
headers que mandan son los del ROOTFS.
Verificado tras la limpieza: sella, y `strace -c /bin/true` traza 30 syscalls.