Ocho paquetes más (giflib, pcre2, nghttp2, libogg, libvorbis, speexdsp, libuv, tea) con la misma
regla de evidencia. Su tarball no estaba en `work/tarballs/`, así que el modo `--fetch` lo baja a un
temporal, lo verifica contra el sha256 QUE LA RECETA PINEA —o es el árbol exacto o no se mira— y lo
borra. No se escribe en la caché de hammer a propósito: es suya, y un fichero puesto ahí por otro
camino es una vía de envenenamiento que nadie audita.
Dos correcciones al criterio, las dos porque produjo una afirmación falsa:
· `gmp` salía GPL-3.0-or-later. Su `COPYING` es la GPLv3, pero al lado trae `COPYING.LESSERv3` y
`COPYINGv2` porque la biblioteca es LGPL. La regla anterior —«el `COPYING` a secas gana al
sufijado»— arregla socat, cuyo `COPYING.OpenSSL` es una excepción, y rompe gmp, donde el extra
nombra otra VERSIÓN y no una dep. Esa diferencia es demasiado fina para codificarla sin
equivocarse, así que ahora se identifican TODOS los ficheros de la raíz y sólo hay veredicto si
dicen lo mismo. gmp, ffmpeg y los tres `gi-*` quedan pendientes, que es lo correcto: en ffmpeg
la respuesta ni siquiera está en el árbol, la deciden los flags de la receta.
· Pero «varios ficheros» no es «ambigüedad»: el `COPYING` de pcre2 son dos líneas apuntando a
`LICENCE.md`, y los dos dicen BSD-3. Por eso se unen las identificaciones en vez de contar
ficheros — ambiguo es que digan cosas DISTINTAS.
Y el TSV pasa a ser ACUMULATIVO. Una receta ya sembrada sale del conjunto de entrada porque ya tiene
`license`, así que regenerar el fichero entero borraba su cita — y la cita es el rastro de
auditoría, lo único que deja revisar un veredicto sin repetir el trabajo. Un fichero de evidencia
que se olvida de lo que ya probó no es evidencia.
Los 10 ArtifactHash afectados, idénticos antes y después.
Hasta ahora atuq estaba sellado y NO estaba en ninguna imagen. Es exactamente la lección que este
fichero ya aprendió con `foot` en el perfil de sway: una receta sellada que ningún perfil declara no
la lleva nadie, y la métrica de clausura no lo puede ver porque mide lo DECLARADO.
Va en los cuatro por el mismo motivo que `mpv` y desde el mismo sitio: vive en el CORPUS, y una
receta resuelve sibling-first y después el catálogo padre, así que las cuatro colas lo alcanzan.
Arrastra la cadena GTK3 en sus variantes `-shared`, y el comentario lo dice: con las estáticas,
libgtk-3 y libgdk-3 se llevaban cada una su copia de pango/cairo y el navegador no llegaba a pintar.
NOTA sobre la métrica: el grafo del hub sólo reporta base/cli/escritorio-mirada — los cuatro
escritorios no aparecen en `by_profile` porque sus raíces viven en las colas. Es previo a este
cambio y no lo introduce; queda anotado porque significa que esta declaración NO se ve todavía en
`build-state.json`.
`licencias.sh --sembrar` escribe desde una tabla curada a mano y `licencias-desambiguar.sh` resuelve
`-only` vs `-or-later` preguntándole a la búsqueda de código de GitHub. Los dos dejan fuera lo mismo:
lo que nadie curó y lo que no vive en GitHub.
Pero `work/tarballs/` indexa cada tarball por su sha256, así que el árbol EXACTO que la receta pinea
ya está en disco: la declaración del propio autor, en el commit que construimos, sin red. De las 76
sin licencia, 55 tenían su tarball cacheado y 25 salieron con evidencia citable.
`scripts/licencias-tarball.py` la busca en tres niveles: la declaración del autor (`Cargo.toml`,
`meson.build`), un único fichero en `LICENSES/` (REUSE, que usa KDE), y el texto del COPYING más la
CONCESIÓN buscada en las cabeceras de los fuentes — excluyendo COPYING/LICENSE, porque el apéndice
de la GPL trae literalmente «or (at your option) any later version» y buscarla ahí da siempre
positivo siendo plantilla. Es la regla que ya fijó `licencias-desambiguar.sh`, aplicada al árbol
pineado en vez de a GitHub.
La primera versión resolvía 40, y CUATRO estaban mal. Las dejo escritas porque son la forma del
error, no accidentes:
· `socat` salía BSD-2 por su `COPYING.OpenSSL`, que es la excepción, no la licencia. Un `COPYING`
a secas gana ahora a cualquier sufijado.
· `pigz` salía Apache-2.0 por `zopfli/COPYING`: la licencia de una pieza VENDORIZADA leída como la
del contenedor. Sólo se mira la raíz.
· `openssh` salía MIT porque su `LICENCE` es un compendio de cuatro y me quedaba con la primera
que pegara. Si el texto trae varias, no hay veredicto: lo compone un humano.
· `nano` salía GPL-3.0-**or-later** citando el «either version 2» de su `aclocal.m4` — plantilla de
autotools, no del proyecto, y encima de otra versión. Ahora la cita tiene que venir de un fichero
del autor Y hablar de la misma versión mayor que el COPYING.
Y dos bugs míos que producían el mismo daño en silencio: la marca de BSD-3 era «Neither the name of»,
que libpcap y libzip no usan («The names of the authors may not be used to endorse») ⇒ se declaraban
BSD-2; y el mayor de versión lo sacaba de `"GPL-3.0".rsplit("-",1)[0][-1]`, que da «L», así que la
comprobación de coherencia rechazaba TODA cita válida.
La validación final no es una regex: un identificador vale si tenemos su texto en `licenses/`. La
obligación legal es acompañar el binario del TEXTO, así que un SPDX que no podemos entregar no
adelanta nada y sí crea una afirmación que no se sostiene. Eso es lo que atrapa el «GPL2+» que meson
deja escribir en `gsd-schemas` y `libgdm`, que no es un SPDX sino taquigrafía.
Nada se sembró a ojo: cada línea de `docs/licencias-evidencia.tsv` lleva la cita que la decidió, y
las 50 que quedan salen listadas con lo que SÍ se encontró en vez de rellenarse.
Comprobado que no se invalida nada: los 26 ArtifactHash afectados son idénticos antes y después
(`license` no está en `hash_inputs`) — medido, no supuesto.
Tres mecanismos medidos para poner una página de inicio: `distribution/extensions/` no instala nada
(sideloading retirado, y el fallo es MUDO), `policies.json` sí se lee y falla nombrándose, y la pref
de firma no alcanza porque la exigencia viene compilada.
Lo que destrabó el diagnóstico fue un TESTIGO: `defaultPref` no deja rastro en prefs.js, así que un
autoconfig que no corre es indistinguible de uno que corre y no hace nada. Con una pref de usuario
escrita desde el .cfg se pudo separar las dos hipótesis sin abrir el navegador.
Y el hallazgo que cambia el plan: sin `MOZ_REQUIRE_SIGNING` vacío, el `sct` del §6.1 —el
diferenciador que nadie más tiene— tampoco se podría shipear nunca. La unidad 6 colgaba de ésta y el
documento no lo decía.
El primer intento murió en un segundo:
mozbuild.configure.options.InvalidOptionError: MOZ_REQUIRE_SIGNING takes 0 values
Es una bandera booleana: el `0` no es «falso», es un VALOR que la opción no acepta. La semántica no
se dedujo, se leyó del parser de mozbuild sacado del propio tarball
(`python/mozbuild/mozbuild/configure/options.py`):
if values == ("",): return NegativeOptionValue() ⇒ VAR= (vacío) DESACTIVA
if values == ("1",): return PositiveOptionValue() ⇒ VAR=1 activa
Y el `export` con valor vacío es justo lo que hace llegar la variable PRESENTE y vacía: desexportarla
la dejaría sin definir y volvería al default, que acá es exigir firma porque el default sale de
`milestone.is_release_or_beta and not milestone.is_esr` — 154.0 es release.
Queda escrito en la receta con las dos líneas del parser, porque «=0 no es falso» es la clase de
detalle que cuesta una hora la segunda vez que aparece.
La página de inicio va en una extensión de sistema (`inicio@atuq.tawasuyu`) y no en una pref, porque
la pestaña NUEVA no se puede redirigir con prefs desde hace años: el único mecanismo soportado es
`chrome_url_overrides.newtab`. La misma pieza resuelve la home con `chrome_settings_overrides`.
La página: el medallón, el nombre y una barra que decide sola. Si lo tecleado PARECE una dirección,
navega; si no, BUSCA CON EL MOTOR POR DEFECTO DEL USUARIO vía `browser.search.query`. No se cablea
ningún buscador: una distro que hornea su motor en la página de inicio está tomando por el usuario
la única decisión que esa página debería respetar. Y dice cuál es el motor, para que la búsqueda no
sea una caja negra.
DOS MECANISMOS MUERTOS Y UNO VIVO, TODOS MEDIDOS:
- `distribution/extensions/` (el clásico de las distros) NO instala nada: Firefox retiró el
sideloading. El `extensions.json` del perfil ni lo mencionaba y el log no decía una palabra —
fallo perfectamente silencioso.
- `policies.json` con `ExtensionSettings` SÍ se lee (`browser.policies.applied=true` en el perfil) y
falla con un error que al menos se nombra: `ERROR_SIGNEDSTATE_REQUIRED`.
- Y `xpinstall.signatures.required=false` TAMPOCO alcanza.
PARA SABER SI EL AUTOCONFIG SIQUIERA CORRÍA, HIZO FALTA UN TESTIGO. `defaultPref` no deja rastro en
`prefs.js` (sólo se guarda lo que difiere del default), así que un `.cfg` que no se ejecuta es
INDISTINGUIBLE de uno que sí y no hace nada. `atuq.cfg` ahora escribe `atuq.autoconfig.ok` como pref
de USUARIO, legible desde fuera sin abrir el navegador. Salió `true` ⇒ el .cfg corre, y la pref de
firma se estaba ignorando: la exigencia viene COMPILADA. El default de `MOZ_REQUIRE_SIGNING` sale
del MILESTONE (154.0 es release), no del canal — el nuestro es `default` y aun así estaba activa.
De ahí `export MOZ_REQUIRE_SIGNING=0` en firefox.toml, con su precio escrito: este Firefox deja de
exigir la firma de Mozilla para CUALQUIER complemento. La alternativa es firmar en AMO —cuenta y
revisión de Mozilla por versión—, que es la dependencia externa que esta distro existe para no
tener. Y no es un desvío: sin esto, el `sct` del SDD 26 §6.1 tampoco se podría shipear nunca.
La prueba se hizo SIN reconstruir: se montó el `.cfg` con el testigo por encima del artefacto con
`--ro-bind`, en vez de editar un hardlink del store (que habría corrompido el artefacto sellado).
`fetch_git` clona a un mirror y materializa el árbol con `git archive | tar -x`, a propósito, para
no pagar un worktree. Pero `git archive` NO emite nada por una entrada gitlink (modo 160000): un
árbol con submódulos salía INCOMPLETO y el fallo aparecía recién en la fase configure, minutos
después y hablando de un fichero "que no existe".
Por eso un `--recurse-submodules` en el clone no arreglaba nada: el que los pierde es el archive,
no el clon. Lo que hace falta es leer `.gitmodules` + los SHA de gitlink DEL COMMIT, mirrorear cada
submódulo y archivarlo en su subruta, recursando. Encaja con el ADR 0006 sin ceder determinismo: un
submódulo ya viene pineado por SHA en el commit del padre.
Dos detalles que no son obvios:
· La URL se reescribe SSH→HTTPS. `.gitmodules` suele declarar `git@github.com:X/Y.git` y nuestro
fetch es anónimo, sin claves. Apunta al mismo repo y el commit está pineado, así que el
contenido no puede diferir: no añade nada que verificar. Las relativas (`../l10n.git`) se
resuelven contra la URL del padre tratada como DIRECTORIO, que es lo que hace git — `..` se
come el nombre del propio repo, no el del directorio que lo contiene.
· `.gitmodules` se lee del COMMIT (`git config --blob`), no del working tree, porque no hay
working tree. Y los gitlinks se listan con `ls-tree -z`: sin `-z` git escapa las rutas con
espacios entre comillas.
El test reproduce el caso completo con repos locales: comprueba primero que sin esto el gitlink
queda como directorio VACÍO —el síntoma exacto que costó el diagnóstico— y después que con esto el
fichero del submódulo llega.
El operador pasó un tablero de identidad: zorro en medallón con cenefa andina, disco teal, anillo
noche. Se adapta la FORMA y el COLOR. Los eslóganes NO — los descartó explícitamente, y de todos
modos `brand.ftl` no es sitio para copy: lleva el nombre y nada más.
EL ICONO DEJA DE DIBUJARSE EN CÓDIGO. Hasta la v0.2 `rebrand.py` generaba una marca geométrica en
Python puro, con el argumento de que un PNG en el árbol no se puede revisar en un diff. Ese
argumento valía mientras el icono era un marcador de posición inventado por falta de arte. Ya hay
arte, así que el criterio se invierte: un asset de diseño ES la fuente, no un derivado. Lo que se
conserva es la disciplina — los seis tamaños salen del MISMO master (recorte del medallón con
máscara circular y alfa, LANCZOS desde 1024 px), se generan una vez y se copian, así que dos builds
ponen los mismos bytes.
LA PALETA SE MIDIÓ, NO SE ESTIMÓ. Los cuatro tokens salen del color más frecuente de cada zona del
medallón: #c15728 el zorro, #1b6470 el disco, #04161d el anillo, #f5ecde los reflejos. Escribirlos
«parecidos» a ojo es cómo una identidad se desalinea de su propio logo en tres ediciones. Con eso el
CSS suma `accent-color` —un solo cambio alcanza decenas de widgets nativos del chrome sin enumerar
ninguno— y el fondo de la barra de pestañas verticales, para que marco e icono cuenten lo mismo.
⚠ Anotado y no escondido: a 16 px el medallón pierde el detalle y queda como una mancha cálida
redonda. Es intrínseco a un logo con cenefa; la salida sería un dibujo simplificado para ese tamaño
y lo decide quien dibujó el zorro, no este script.
Construido y sellado EN LOCAL en 3 segundos (b3:97328d1c), hidratado y abierto: 8 procesos vivos,
anunciándose como `(atuq:9581)`.
Con la cadena GTK3 arreglada, atuq arrancó de verdad: parent vivo, CERO procesos de contenido
muertos, y pintando una página local. Quedaban dos cosas.
1. SE ANUNCIABA COMO `firefox-default`. El application.ini decía RemotingName=atuq y aun así el
proceso se presentaba así — porque esa cadena es un MOZ_APP_REMOTINGNAME horneado en el ELF, y es
la que Gecko pasa a g_set_prgname(), o sea el **app_id de Wayland**. Consecuencia visible: el
escritorio no casaba la ventana con nuestro .desktop (StartupWMClass=atuq) ⇒ icono genérico y sin
agrupar.
Se parchea EN SITIO, no recompilando: cambiarlo en recipes/firefox.toml brandearía la BASE como
atuq, y firefox tiene que seguir siendo firefox para los otros forks. Es seguro porque es una
cadena C terminada en NUL en el pool de .rodata —`firefox-default\0` son 16 bytes exactos y se
escriben 16—, y se EXIGE una única ocurrencia: si upstream la duplica o la renombra, falla en vez
de parchear el sitio equivocado. Verificado después: 0 ocurrencias de la vieja, y el log dice
`(atuq:26423)`.
2. UN BUG QUE SÓLO SE VE FUERA DE ROOT. `cp -a` preserva los modos y los ficheros del store están
sellados sin permiso de escritura; todo lo que viene después los modifica. En el worker pasaba
inadvertido porque corre como root, que ignora los bits. El primer build local murió con
`PermissionError: /out/usr/lib/atuq/application.ini`. Se arregla con `chmod -R u+w` en la receta y
no en el entorno: un artefacto no debe depender de con qué uid lo construiste.
Y el runner pasa a hidratar las variantes `-shared`: en runtime una `.a` no sirve de nada, y son las
que gtk3 declara NEEDED desde el arreglo del cuadro de las dos Pango.
Este atuq se construyó EN LOCAL en segundos, que era la promesa entera del diseño derivado del
SDD 26: iterar el envoltorio sin volver a pagar un build de Gecko.
Con `atk` arreglado, atuq llegó más lejos y murió igual, ahora con Pango:
GLib-GObject-CRITICAL: cannot register existing type 'PangoFontMap'
… decenas de líneas … tipo '<invalid>'
MISMO CUADRO, CULPABLE DISTINTO Y MÁS GRANDE. `gtk3` produce DOS objetos compartidos —libgtk-3.so y
libgdk-3.so— y libxul declara NEEDED las dos, así que se cargan siempre juntas. Con las variantes
ESTÁTICAS de sus deps, cada una se llevaba adentro su propia copia. Medido con el mismo instrumento
que cazó a atk, `nm -D --defined-only`, preguntando quién DEFINE cada símbolo:
pango_font_map_get_type → libgdk-3.so, libgtk-3.so, libgailutil-3.so
cairo_create → libgdk-3.so, libgtk-3.so
gdk_pixbuf_get_type → libgdk-3.so, libgtk-3.so
hb_buffer_create → libgdk-3.so, libgtk-3.so, libgailutil-3.so
Y se comprobó el otro lado: libxul NO embebe ninguna —enlaza libgtk-3/libgdk-3 como debe—, así que
la duplicación es toda interna del par GTK3.
Cuatro variantes nuevas: harfbuzz-shared, cairo-shared, gdk-pixbuf-shared, pango-shared. Sólo cambia
el modo de librería; se conservan todos los switches del canónico para no arrastrar deps nuevas.
DOS DEUDAS ANOTADAS, NO OLVIDADAS: `fribidi` y `pixman` siguen estáticos —no existe variante— pero
quedan embebidos en UNA sola .so cada uno, así que no hay copia que colisione; y `libepoxy` sí queda
en las dos, y se deja porque no registra tipos de GObject ni mantiene estado global. Si algún día
otra .so del mismo proceso los embebe, vuelve el cuadro.
`firefox` swapea las mismas cuatro: declarar las ESTÁTICAS junto a un gtk3 que trae las compartidas
son dos artefactos peleando por el mismo `pango.pc`, que es la otra forma conocida de este fallo.
Y `xkeyboard-config` SUBE AL CORPUS. Existía idéntica en las cuatro colas de escritorio (un solo md5
entre las cuatro, verificado antes de mover) y atuq, que vive en el corpus, no podía alcanzar
ninguna: sibling-first y después el catálogo padre, nunca una cola hermana. Sin sus datos el
navegador ni pinta («xkbcommon: failed to add default include path /usr/share/X11/xkb»). Mismo hash
en el corpus que en las colas ⇒ cero rebuilds, y las copias de las colas se quedan donde están.
El script daba por bueno el rootfs si el directorio estaba: «¿ya está hidratado?». Es la pregunta
equivocada. Tras el re-hash de la cadena atk→gtk3→firefox→atuq el directorio seguía ahí con los
artefactos VIEJOS, así que habría abierto contento la versión que acabábamos de arreglar y el
diagnóstico habría sido buscar en el sitio equivocado. Misma forma del cache-hit que congela
regresiones.
Ahora resuelve los hashes vigentes, los anota en `.raices` y rehidrata cuando difieren.
De paso: nada de `diff <(...)`, que es de bash. El shebang dice /bin/sh y un script que sólo anda
cuando /bin/sh resulta ser bash es una trampa que salta en otra máquina.
Con atuq sellado y verificado, se hidrató y se abrió en waypipe. No arrancó, y los tres motivos son
justamente la diferencia entre «sellado» y «usable»: el EXDEV de hidratar a otro mount, el lanzador
que no puede ser un symlink porque los ELF no traen RPATH, y un bug del CORPUS —atk se había tragado
GObject entero por declarar la glib estática siendo una .so—.
El tercero es el que vale escribir: el síntoma (45 GLib-GObject-CRITICAL y un SIGSEGV) no nombra a
atk por ningún lado, y no se ve mirando el rootfs porque había UNA sola libgobject. Se caza
preguntando quién DEFINE el símbolo, no quién lo usa.
Y una corrección de número: firefox se reconstruye en ~50 minutos, no en las cuatro horas que este
documento venía repitiendo. Ese número era folclore de la receta, no una medición.
Dos cosas que salieron de intentar ABRIRLO, que es lo único que distingue «sellado» de «usable».
1. EL LANZADOR. Con `/usr/bin/atuq` como symlink, el navegador moría antes de pintar:
XPCOMGlueLoad error for file /usr/lib/atuq/libmozsandbox.so:
Error loading shared library libnspr4.so: No such file or directory
El motor carga sus propias librerías desde `/usr/lib/atuq` y ni el binario ni esas `.so` traen
RPATH/RUNPATH — comprobado con `readelf -d`, no supuesto. Pasa a ser un script que exporta
`LD_LIBRARY_PATH` (lo mismo que hacen Debian y Fedora) y hace `exec`, para que el proceso que
queda sea el motor y `/proc/self/exe` siga resolviendo el appdir. El `LD_LIBRARY_PATH` además
tiene que HEREDARSE: Firefox lanza un proceso por pestaña y todos cargan las mismas librerías.
La alternativa limpia es grabar RPATH=$ORIGIN con `patchelf` —es lo que hace Alpine— pero
`patchelf` todavía no existe como receta del corpus; cuando exista, esto vuelve a ser un symlink.
2. `scripts/atuq-nested.sh` — hidrata el cierre de runtime y abre atuq como ventana anidada en el
compositor que ya está delante (waypipe, mirada, sway). Trae dos cosas aprendidas a golpes:
· EL ROOTFS VA EN EL MISMO MOUNT QUE EL STORE. `hydrate` proyecta con hardlinks y `linkat()`
rechaza cruzar un punto de montaje aunque sea el mismo filesystem. Acá el store es /dev/sdb
bind-monteado y `work/` vive en /dev/sdc: hidratar a `work/…` muere con «Invalid cross-device
link (os error 18)». El volumen entero está en /mnt/cosecha, así que el rootfs va ahí. Se
comprueba con `findmnt -T`, nunca con `stat -c %d`.
· `dejavu-fonts` va en las raíces por necesidad, no por completismo: un navegador sin una sola
fuente arranca, pinta y muestra cuadraditos. Ya nos costó una tarde en GNOME.
Y no saltea en silencio: si falta un artefacto de la lista, sale con error en vez de armar un
rootfs al que le faltan tres paquetes y falla tres capas más abajo.
Al abrir el primer atuq en pantalla, murió así:
GLib-GObject-CRITICAL: cannot register existing type 'gpointer'
… 45 líneas iguales … Segmentation fault (exit 139)
El síntoma no nombra a atk por ningún lado, y las dos glib no estaban como dos ficheros en el
rootfs —había UNA sola libgobject—. La segunda copia estaba EMBEBIDA: `atk` declaraba la variante
ESTÁTICA de glib y produce un objeto compartido, así que el enlazador le metió GObject entero
dentro de `libatk-1.0.so`. Todo proceso que cargue atk y libgobject a la vez —o sea cualquier app
GTK— acaba con dos sistemas de tipos peleando.
SE ENCONTRÓ MIDIENDO, NO LEYENDO: `nm -D --defined-only` sobre cada `.so` del rootfs, buscando
quién DEFINE `g_type_register_static`. Dos respuestas: `libgobject-2.0.so`, que debe, y
`libatk-1.0.so`, que no.
Es la regla que el corpus ya tenía escrita desde GTK4 y que esta receta se saltó: las deps de una
`.so` van a las variantes `-shared` EN LUGAR DE las estáticas, nunca junto a ellas.
RADIO MEDIDO ANTES DE TOCAR (`yupana radio atk`): 4 transitivos, 3 sellados caen a deuda —
atk, gtk3 y firefox. GNOME NO está en el radio: usa gtk4. El costo es un rebuild de firefox de
cuatro horas, y no hay forma de esquivarlo: sin esto el navegador no abre.
Esto es «sellado ≠ usable» otra vez, y la única razón por la que apareció es que alguien lo ABRIÓ
en una pantalla. La clausura decía 100%.
Al abrir `application.ini` para el branding de atuq apareció esto:
BuildID=20260905035306
Es la fecha y hora del build. Dos construcciones de la MISMA receta, con el mismo ArtifactHash,
producen bytes distintos. Es justo el agujero que el invariante de reproducibilidad existe para
cerrar, y no se ve en `build-state.json` porque el hash es input-addressed y no se mueve por esto.
El arreglo es conocido y lo usa Alpine: exportar `MOZ_BUILD_DATE` con un valor determinista.
NO se aplica de paso, a propósito: vive en una FASE, así que re-hashea la receta y cuesta otro build
de cuatro horas más el re-sellado de atuq, que cuelga de ella. Es su propia unidad de trabajo y
queda como 4.c en el plan del SDD 26.
El comentario se comprobó que NO mueve el hash antes de commitearlo (b3:aa90192a antes y después):
un comentario fuera de las fases no entra en `hash_inputs`, así que el firefox sellado sigue válido.
Y el §2.ter del SDD queda con el estado real de atuq v0.2 y su evidencia.
Hash b3:7eff4ba4. La v0.1 dejó el branding fuera porque había que MIRAR el árbol antes de adivinar.
Se miró, y lo que apareció decidió la forma de esta versión:
- `application.ini` del appdir arranca diciendo «This file is not used». Es herencia del árbol de
desarrollo: en un build empaquetado el launcher SÍ lo lee. `rebrand.py` no le cree al comentario:
comprueba que las claves estén y falla si no.
- Las cadenas que la gente VE están DENTRO de `browser/omni.ja` ⇒ sin re-empacar el zip no hay
branding. Y el original no dice «Firefox», dice **Nightly**, porque construimos con
`--with-branding=unofficial`. Sin tocarlo, atuq se presentaría como lo peor de los dos mundos.
- Los ICONOS, en cambio, viven FUERA del zip ⇒ se reemplazan sin abrirlo.
EL RE-EMPAQUE PRESERVA TRES PROPIEDADES DEL ORIGINAL, Y CADA UNA POR UNA RAZÓN MEDIDA:
orden de las 5306 entradas (es el que Gecko lee al arrancar, y es lo que el jarlog del PGO va a
refinar), `compress_type=0` (Mozilla lo deja SIN COMPRIMIR para poder mapearlo) y `date_time`
2010-01-01 (ya venía normalizado: Mozilla también persigue reproducibilidad).
VERIFICADO CONTRA EL ARTEFACTO, NO CONTRA EL LOG:
- diff entrada por entrada entre el omni.ja de firefox y el de atuq: **5306 → 5306, mismo orden,
todo STORED, mismas fechas, y UNA SOLA entrada con contenido distinto**: brand.ftl.
- re-empacar dos veces el mismo zip da el mismo sha256 ⇒ el paso es determinista.
- application.ini: Vendor=tawasuyu, Name/RemotingName/CodeName=atuq. `ID` NO se toca: es el GUID
con el que las extensiones declaran compatibilidad, y cambiarlo dejaría a atuq fuera del
ecosistema de complementos.
EL BuildID SE DERIVA DEL CONTENIDO DEL OVERLAY, no de la fecha. Es la clave con la que Gecko
invalida su startup cache: si atuq cambia su chrome y el BuildID no se mueve, el navegador arranca
con la interfaz vieja cacheada y PARECE que el overlay no agarró. Derivarlo del contenido lo mueve
exactamente cuando hace falta y dos builds del mismo atuq dan el mismo número — con la fecha
pasaría lo contrario en los dos sentidos. Se vio funcionar: al sumar `CodeName` el BuildID cambió
solo, de 81921491609672 a 24128696592342.
El icono se DIBUJA en código (PNG en Python puro, zlib + struct) y no se commitea como binario:
un PNG en el árbol no se puede revisar en un diff, el código sí, y el resultado es idéntico en cada
build. ⚠ Es un marcador de posición declarado —una marca geométrica plana, 5 tamaños—; lo correcto
es que alguien dibuje el zorro. Su única función irrenunciable ya la cumple: que la ventana y el
lanzador NO muestren el icono de otro producto.
Binarios: firefox → atuq y firefox-bin → atuq-bin, con symlink `firefox` → `atuq` dentro del appdir
porque hay scripts de terceros que invocan por el nombre histórico y Gecko resuelve su directorio
por /proc/self/exe (entrar por el symlink resuelve al mismo sitio). Más `.desktop` con
StartupWMClass=atuq, que casa con el app_id que da RemotingName bajo Wayland.
El documento daba por hecho que la receta derivada se podía escribir con lo que hammer ya tenía.
No: `[source]` era obligatoriamente git o tarball. Queda escrito el muro, por qué los rodeos eran
peores (fetchear una fuente que se ignora miente sobre la identidad; un repo aparte obliga al worker
a leer algo privado) y la salida — `source.dir`, hasheado por contenido con el `of_tree` que ya
existía para Stage 2.
Y el estado real del plan: la unidad 4 tiene su v0.1 escrita, con el branding y el re-empaque de
omni.ja separados como 4.b porque los dos necesitan mirar el artefacto que `firefox` todavía no
selló.
Primera receta del navegador de la distro (SDD 26). No compila nada: depende de `firefox`, copia su
árbol y le pone encima cuatro ficheros. Hash b3:2d6e4dcf.
LA CAPA SON CUATRO FICHEROS Y CADA UNO ESTÁ DONDE ESTÁ POR UNA RAZÓN:
- `defaults/pref/autoconfig.js` — el único gancho que corre ANTES de que exista un perfil.
- `atuq.cfg` — prefs de fábrica + carga del chrome. Con `defaultPref` y no `lockPref`: son valores
de arranque, no una cárcel; lo que de verdad se bloquea va en policies.json.
- `chrome/atuq.css` — el aspecto.
- `distribution/policies.json` — telemetría, updates, primer arranque.
POR QUÉ EL CSS NO ES `userChrome.css`: ese fichero vive en el PERFIL y exige que el usuario prenda
`toolkit.legacyUserProfileCustomizations.stylesheets`. atuq tiene que verse como atuq en el primer
arranque, con un perfil recién creado, sin que nadie prenda nada. El único gancho a nivel de
APLICACIÓN es nsIStyleSheetService desde autoconfig — y llegar a él es la razón de
`sandbox_enabled = false`. Se registra como USER_SHEET, el mismo nivel de cascada que userChrome,
para que el usuario pueda seguir pisándolo.
DOS TRAMPAS DEL FORMATO, ESCRITAS EN EL PROPIO FICHERO:
- la PRIMERA LÍNEA de un autoconfig se ignora siempre, por diseño (herencia de Netscape). Código en
la línea 1 no corre y no da error: el fallo más caro de ese fichero es el que no dice nada.
- el CSS va acotado con @-moz-document a browser.xhtml. Una hoja USER sin acotar alcanza CUALQUIER
documento chrome —visor de PDF, inspector, diálogos— y un selector como `toolbar` termina pintando
ventanas que nadie miró.
`extensions.autoDisableScopes = 0` es la otra mitad del `--with-unsigned-addon-scopes` que ya viaja
en el build de firefox: sin los dos, las extensiones de la distro se instalan DESACTIVADAS.
LAS ASERCIONES VAN PRIMERO Y FALLAN RUIDOSAS. Un derivado que no encuentra su base produciría un
directorio con cuatro ficheros de config, `Store::has` lo daría por presente y el fallo aparecería
el día que alguien abra el navegador (regla 3 del CLAUDE.md). Y al final imprime el inventario de la
capa con sus tamaños, que es la primera pregunta del diagnóstico cuando «atuq se ve como Firefox».
LO QUE LA v0.1 NO HACE, A PROPÓSITO: no renombra el binario ni toca application.ini, y no
re-empaqueta omni.ja. Las dos cosas necesitan mirar la forma real del árbol que selle `firefox`, y
adivinarla desde acá daría un artefacto que arranca en una máquina y en ninguna otra.
No se puede construir todavía: su dep `firefox` está en vuelo.
Al ir a escribir `recipes/atuq.toml` apareció un muro que el SDD 26 no había visto: `[source]` era
obligatoriamente git o tarball (`Source::kind` no tiene tercera salida), así que una receta cuyo
contenido no viene de upstream sino de NOSOTROS —un envoltorio sobre otro artefacto, un tema, una
configuración— no se podía ni escribir. Los rodeos posibles eran todos peores: fetchear una fuente
upstream que después se ignora MIENTE sobre la identidad del artefacto, y colgar el overlay de un
repo aparte obliga a que el worker tenga acceso de lectura a un repo privado.
`dir = "atuq"` apunta a un árbol dentro del propio repo, relativo al directorio de la receta.
SE HASHEA POR CONTENIDO, NO POR RUTA. `ArtifactHash::of_tree` ya existía (lo usa Stage 2 para
verificar bit-reproducibilidad) y hace exactamente lo que hace falta: rutas ordenadas, bit de
ejecución, contenido, sin seguir symlinks. Es la misma disciplina que ya tenían los `patches`, que
entran al hash por bytes y no por nombre. Comprobado a mano: editar un CSS del overlay mueve el
ArtifactHash y revertirlo lo devuelve exacto.
`dir` es EXCLUYENTE con repo/tarball y se comprueba primero. Declarar las dos cosas no es una
ambigüedad para resolver por precedencia: es un error de quien escribió la receta, y decirlo antes
del fetch evita bajar algo que después se pisa.
Los cuatro sitios que hacían match sobre `SourceKind` se cierran a mano y no con un `_`:
- `swm.rs` (×2) y `swm_bridge.rs` (×2): un `.swm` es un manifiesto COMPARTIBLE y necesita un puntero
que el otro lado pueda resolver (commit o sha256). El árbol de una derivada vive en este repo y no
hay puntero que mandar ⇒ error explícito en vez de emitir un manifiesto con el source vacío, que
viajaría bien y rompería del otro lado. Error y no `unreachable!`: esto es librería, y un panic
mataría al llamador por una receta mal escrita.
- `hammer pin`: una derivada ya está anclada por contenido ⇒ no hay ref flotante que fijar, lo dice
y sale con 0.
- `hammer` → file_drop: mismo tratamiento que un source que no se puede expresar.
Dos tests: que `dir` resuelve y excluye a los otros dos, y que el mensaje de «source vacío» nombra
los TRES modos — ese texto es la única guía de quien escribe una receta a mano, y si sumamos un modo
sin tocarlo mandamos a la gente a buscar un campo que no existe.
⚠ Al correr la suite aparece un fallo AJENO a esto y que NO toqué:
`kernel::contract::tests::el_contrato_del_repo_cierra_sobre_si_mismo` — «proceso-por-descriptor no
declara símbolos», del frente kernel (commit abda7d3, alta de pidfd). Queda anotado para su dueño.
Primer build con clang+ThinLTO: murió a los 4 minutos, y el error nombra las dos versiones, que es
lo que lo hace diagnosticable de un vistazo:
/usr/bin/llvm-ar: error: …log.o: 'Unknown attribute kind (102)'
(Producer: 'LLVM22.1.8' Reader: 'LLVM 18.1.8')
`clang18` y `llvm18` estaban en [deps].build de cuando esta receta compilaba con gcc: aportaban el
libclang para bindgen y la suite llvm-ar/llvm-objdump que Mozilla exige. Con `compiler = "clang"` se
volvieron ACTIVAMENTE DAÑINAS: una dep del corpus se materializa en el sandbox y TAPA el binario del
lab, así que llvm-ar pasó a ser el de LLVM 18 con un compilador clang 22. Mientras los .o eran
objetos no se notaba; con ThinLTO son BITCODE, y el bitcode no es compatible hacia atrás.
Misma forma que la lección de los headers UAPI: una dep tapa al lab y el fallo aparece lejos del
sitio que lo causó.
El arreglo no es apuntar AR a mano, es no traer el 18: el lab ya expone
/usr/bin/llvm-{ar,objdump,profdata} → llvm22 y /usr/lib/libclang.so → llvm22 (paso 3a-ter del
bootstrap). Es además lo que hace Alpine: clang22 + clang22-libclang, sin un segundo LLVM en la mesa.
Y llvm-profdata del 22 es el que va a hacer falta para el PGO.
Nuevo ArtifactHash: b3:aa90192a141f9fc6d2b59acd074db6caa36854af7100fb08fb51c8fe441b72b7
Al ir a re-pinear la imagen para propagar el `lld` a la granja, hub y worker empaquetaron a shas
DISTINTOS. La imagen se creó para que el sha VERIFIQUE, así que un desacuerdo ahí no se pinea: se
mide.
Los dos rootfs son idénticos: 9550 entradas con la misma lista de ficheros, los mismos tamaños, y
CERO diferencias de metadato (modo, nlink, destino de symlink) en las 8778 entradas de fichero y
symlink. La única diferencia de contenido en todo el rootfs era `var/log/apk.log`, donde apk anota
la FECHA de cada operación — o sea que dos labs equivalentes empaquetaban distinto sólo porque las
instalaciones ocurrieron en momentos distintos. Un sha que no se puede reproducir desde un rootfs
equivalente no verifica nada, que es justo lo que este fichero existe para dar.
Se excluye ese log del tar (no todo `var/log`: cambio mínimo; apk lo recrea solo). Con la exclusión,
hub y worker dan el MISMO tar: 179d04f054f28d8dafe7c626d6b8c686a33a00a96715ee544a9bcf6f62cf586d.
⇒ LA GRANJA YA ESTABA SINCRONIZADA Y AHORA SE PUEDE DEMOSTRAR, sin correr `--traer` en un worker que
está construyendo firefox — reemplazarle el rootfs a mitad de build habría sido la forma cara de
descubrir lo mismo.
Nueva imagen publicada: hammer/lab/lab-d1e341d5….tar.zst (310 M), pin actualizado en
bootstrap-devfs.sh. Queda en el comentario cómo comparar dos máquinas: se compara el TAR y no el
`.tar.zst`, porque el sha comprimido depende de la versión de zstd de cada máquina y dos labs
idénticos con zstd distintos darían un falso desacuerdo.
Primer intento en el worker: murió en CUATRO SEGUNDOS, no en cuatro horas. `mach build` abortó con
«FATAL ERROR PROCESSING MOZBUILD FILE» sobre browser/moz.build porque referencia
browser/locales/moz.build y ese fichero NO ESTÁ en el árbol traído.
Lo que eso significa, y es la parte que vale: LOS ONCE PARCHES DE MUSL SÍ AGARRARON. La apuesta
explícita de esta receta —reusar los parches de 154 sobre un árbol 153.1.0— no es lo que falló. Lo
que falla es el FETCH: falta un trozo del árbol. Sospechoso número uno el `--filter=blob:none`
contra cómo BrowserWorks compone su repo, no la receta.
Se aparca por decisión del usuario: la prioridad pasa a firefox con la cadena de optimización y a
atuq (SDD 26). El primer paso al retomarla es un `git ls-tree` del commit pineado, no un build.
El documento estimó una receta `llvm-toolchain` (clang+lld+libc++ desde fuente) como la unidad de
mayor palanca del frente. Mirar el lab en vez de suponerlo la redujo a un `apk add`: clang22 y
llvm22 ya estaban, `Compiler::Clang` ya estaba cableado en hammer y ninguna receta lo usaba.
Se corrige el §3 con la evidencia y la medición de la huella (no se movió ⇒ 837 artefactos
intactos), y el plan del §8: la unidad 2 queda cerrada, la 3 pasa a ser construir, y el PGO se
separa como 3.a porque tiene sus propios dos muros (sin X11 para el profileserver, profdata no
determinista).
El SDD 26 estimó que habilitar PGO/LTO exigía una receta `llvm-toolchain` (clang+lld+libc++ desde
fuente). ERA CARO DE MÁS. Al mirar el lab en vez de suponerlo:
- `.dev-fs/alpine` YA TRAE clang22 + llvm22 22.1.8 — la MISMA major que usa el APKBUILD de Alpine
para este mismo Firefox (`_llvmver=22`).
- `Compiler::Clang` YA EXISTE en hammer, cableado de punta a punta: `parse_compiler` lo acepta y
`hammer-build/src/lib.rs` pone CC=clang, CXX=clang++ y AR=llvm-ar. NINGUNA receta lo usaba.
- Lo único que faltaba era `ld.lld`. `apk add lld` ⇒ lld22 22.1.8, dos paquetes, cero upgrades.
CON ESO CAEN LOS TRES MUROS QUE OBLIGABAN A gcc, sin perder lo que gcc daba: el sondeo de linker se
satisface con `--enable-linker=lld`, el `ar` lo pone hammer solo, y el `NEEDED` de la stdlib de C++
existe porque clang++ de Alpine usa la libstdc++ COMPARTIDA — la prueba no es teórica, Alpine
construye este Firefox con clang22 y sin libcxx en sus makedepends.
LA HUELLA DEL LAB NO SE MOVIÓ, Y SE MIDIÓ ANTES DE TOCAR NADA. `lld` no casa ningún prefijo de
TOOLCHAIN_PREFIXES (hammer-core/src/lab.rs), así que los 43 paquetes que entran en `hash_inputs`
salieron idénticos ⇒ los 837 artefactos sellados quedan intactos. Eso es lo que hace barato el
cambio HOY, y a la vez es un agujero escrito en los dos sitios: la versión de lld no es parte de la
identidad del artefacto, y sólo expone a las recetas `compiler="clang"`, que hoy es una. Meter
"lld" en la lista de prefijos es lo correcto y cuesta re-hashear el corpus entero: próxima campaña.
En el mozconfig entran, además de lld: `--enable-lto=cross`, `--enable-packed-relative-relocs` y
`--with-unsigned-addon-scopes=app,system`. El último no es cosmético: sin él un Firefox de release
rechaza las extensiones que la distro deja en distribution/extensions/, así que la capacidad de atuq
de shipear su propio `sct` se decide ACÁ, en la base, y no en el overlay del derivado.
PGO no entra en esta pasada y el porqué queda escrito en la receta: el perfil se junta corriendo el
navegador (Alpine y Arch usan xvfb-run; nosotros no tenemos X11 ⇒ sway headless) y el profdata NO es
determinista, así que tiene que sellarse como artefacto propio y consumirse por hash.
ThinLTO se capa con la misma cuenta que -j y por la misma razón que ella no es un literal: un número
fijo ataría el ArtifactHash a la RAM de quien escribió la receta.
Nuevo ArtifactHash: b3:6f2a3b2f6db4452ed0d2d3f4e2ff7cd6562a86878d4360653859720be1c3d94d
(el firefox 154.0 sellado con gcc queda SUPERADO, no perdido).
Pregunta del usuario: «¿firefox está sin pgo-lto-bolt? ¿eso es una desventaja frente a las otras
distros?». Sí, y para medirlo se trajeron el APKBUILD + mozconfig de community/firefox de aports y
el PKGBUILD de Arch, y se compararon línea a línea con el mozconfig de recipes/firefox.toml.
La comparación importa porque ALPINE ES NUESTRO PROPIO UPSTREAM: de ahí salen los once parches de
musl. No es una distro lejana sacándonos ventaja, es la que ya construye este mismo código con
`--enable-lto=cross` + `--enable-profile-use=cross` + jarlog. Arch hace lo mismo («Do 3-tier PGO»).
BOLT NO ES LA DESVENTAJA: cero menciones en los dos ficheros. Lo que nos separa es PGO+LTO.
Y el diff completo da más que velocidad:
- RLBOX APAGADO (`--without-wasm-sandboxed-libraries`) es una desventaja de SEGURIDAD y pesa más que
el PGO: es la jaula wasm de graphite/ogg/expat/woff2, o sea el código que come entrada no
confiable. Encenderlo pide wasi-sdk + wasi-compiler-rt en el corpus: receta, no flag.
- `--with-unsigned-addon-scopes=app,system` nos falta y ATUQ LO NECESITA para shipear sus propias
extensiones ⇒ una capacidad de atuq que NO se resuelve en el overlay: exige tocar la base.
- Falta RELR; el linker es GNU ld y no lld.
- `--disable-jemalloc` NO es desviación nuestra: Alpine también lo pasa. Un fantasma menos que
perseguir.
- Bundlear en vez de `--with-system-*` es diferencia a propósito: el corpus ES la fuente.
Corolario que ordena el plan: TODO ESO ENTRA EN UN SOLO REBUILD. Cada uno son cuatro horas y
re-sella la cola Gecko entera, así que la unidad 3 pasa a ser una pasada única y RLBox se separa
como 3.b.
Y dos muros del PGO que las distros no tienen, porque no persiguen lo que nosotros perseguimos:
- las dos corren el profileserver bajo `xvfb-run`, y NO TENEMOS X11 en el corpus (Wayland-only) ⇒ va
bajo sway headless, que ya existe del frente wlr.
- el profdata NO ES DETERMINISTA (contadores dependientes del timing) ⇒ se genera una vez, se sella
como artefacto propio y se consume por hash, o firefox deja de reproducir.
Bonus para el §2: el jarlog del PGO ordena el omni.ja para el arranque. Re-empacarlo a lo bruto tira
esa optimización a la basura sin que nadie lo note.
La pregunta era «nuestro propio envoltorio en vez de zen». La respuesta tiene tres hallazgos que
cambian el precio, y el documento los pone antes que la lista de features.
1. NO HAY ENVOLTORIO FUERA DEL CHROME. Gecko no tiene API de embebido en escritorio desde que murió
XULRunner (GeckoView es Android) ⇒ una carcasa Llimphi con el motor adentro no es posible. El
envoltorio es chrome-level o no es. Que es exactamente lo que Zen es.
2. ARTEFACTO DERIVADO, NO FORK DE FUENTE. Si atuq parchea el árbol, cada iteración de UI cuesta
cuatro horas de Gecko y el diseño de chrome es iterativo por naturaleza. Como no distribuimos un
binario sino que sellamos artefactos, atuq puede depender de `firefox` e inyectar marca, prefs,
policies.json y omni.ja encima: itera en segundos, no mueve el ArtifactHash del corpus, y hereda
las CVE gratis — que es lo que hundió a los forks de Firefox. Con sus dos gotchas escritos: el
omni.ja es un zip y hay que re-empacarlo determinista, y sin invalidar el startup cache el
overlay PARECE no agarrar.
3. LAS OPTIMIZACIONES NO SON UN FLAG, SON UNA TOOLCHAIN. PGO/LTO/BOLT son cadena de clang en Gecko,
y el corpus no tiene clang usable como compilador: clang18.toml compila SÓLO libclang.so (para
bindgen) y llvm18 se selló con PROJECTS="". La unidad real es una receta llvm-toolchain con
clang+lld+libc++ musl — la misma pieza que resuelve el muro 3 de firefox.toml. Es la mayor
palanca del frente, y por eso encabeza el plan.
Lo que NO se promete va escrito ANTES de la lista de features, con el criterio del modelo de
adversario de qullqa: atuq no es Tor Browser y no va a tener «modo Tor» — es un binario único en el
mundo (musl, wayland-only, branding propio), así que salir por Tor desde acá identifica MÁS, no
menos. Se ofrece proxy por contenedor, que es separación de tráfico y no anonimato.
Los diferenciadores van con una columna «quién más lo tiene» para no contarnos un cuento: sct
(transparencia de scripts, BLAKE3 + testigo) no lo tiene nadie y ya está escrito en tawasuyu;
torrent SÍ lo tiene Vivaldi, y lo nuestro sólo vale por dónde cae lo bajado (el CAS). Todo lo que
no es CSS pasa por UNA costura: un host de native messaging en Rust.
Y atuq no abandona a puriy: el host, el testigo y el archivo con RAG son agnósticos del motor, así
que cuando puriy madure se enchufan del mismo lado. atuq es su andamio, no su desvío.
`hammer build` aplica los parches DESPUÉS de materializar las dependencias, así que en una receta
hoja de la plataforma Gecko el `patch` que no agarra se descubre detrás de horas de compilar OTRA
COSA: waterfox estrena su primer build reconstruyendo nodejs entero (4414 objetivos de V8, a -j2
porque la propia receta capa por RAM). La receta de waterfox dice, con todas las letras, que si
patch falla «falla TEMPRANO, antes de compilar nada». Con el orden real de hammer eso no era
cierto. Este vigía es lo que lo vuelve cierto: saca de los propios parches los ficheros que tocan
(21 en waterfox), los trae por sparse-checkout blob:none, y aplica los once acumulativos y en
orden como hace fetch.rs. Un minuto en vez de cuatro horas.
LO QUE MIDE NO ES SÍ/NO, SON TRES ESTADOS. `patch` también responde «sí, adivinando»: cuando el
contexto no casa aplica igual con FUZZ, y con el `--silent` de fetch.rs eso sale por exit 0 sin que
nadie se entere. Un parche con fuzz puede haber editado el sitio correcto o cualquier otro, y el
exit code no distingue. Por eso `ok` / `FUZZ` / `FALLA`, y el del medio existe para no perderse.
El desplazamiento no se marca: sólo dice que el fichero creció por arriba.
VEREDICTO SOBRE LA APUESTA DE WATERFOX (parches de 154 sobre base 153.1.0): SE SOSTIENE. Los once
entran; los dos que entran con fuzz —time64 y fix-rust-target— se verificaron a mano y los dos
editan el sitio correcto. El contexto que no casa es cosmético: comillas simples vs dobles de un
reformateo con black, y un brazo `cfg` de más.
Y UN HALLAZGO DE PASO: `firefox-patches/time64.patch` tiene la cabecera MENTIROSA. Su diffstat
anuncia tres ficheros y el cuerpo entrega dos — falta el hunk de `wgpu-hal/src/vulkan/adapter.rs`.
Como firefox 154 sella igual, nadie lo había notado. Por eso los ficheros se leen del cuerpo y
nunca del diffstat.
El vigía nació mintiendo en la dirección cara y por eso se probó contra recetas selladas antes de
commitear: suponía el prefijo `a/`+`b/` de git y marcaba FALLA sobre gawk, que sella perfecto — su
parche es un `diff -upr` a secas con caminos `gawk-5.1.0.orig/`. Se descarta el primer componente
se llame como se llame, que es lo que significa el -p1 con el que se aplican.
Flags en inglés (--all, --git-only, --keep, --strict) por la regla 4 del repo.
Waterfox es un fork de Gecko con árbol propio, así que consume EXACTAMENTE la
misma plataforma que Firefox (gtk3, nodejs, clang18, cbindgen, variantes -shared)
y hereda los seis muros ya resueltos sin volver a pagarlos: sin --enable-linker,
compiler=gcc, RUST_TARGET, vaciado de checksums vendorizados, strip_debug y el
build hermético.
Medido con diff ignorando comentarios: las diferencias REALES con firefox.toml son
TRES — la fuente, el branding y la versión. Si aparece una cuarta, conviene
preguntarse si no debería estar en las dos.
LA BASE ES 153.1.0 y los parches son de 154, una release por encima. Se reutilizan
igual porque el código que tocan (LFS64, time64, prctl, sandbox) se mueve poco
entre releases, pero es una apuesta EXPLÍCITA y no un hecho: si patch falla, falla
temprano —antes de compilar nada— y el arreglo es rebasar el que se queje.
ACÁ SÍ VA BRANDING OFICIAL, y no contradice a firefox.toml: allá va sin marca
porque el nombre y el logo de Firefox sobre un binario parcheado entran en la
política de marcas de Mozilla; acá browser/branding/official ES la marca del
propio Waterfox, que BrowserWorks distribuye en su árbol para que se construya
así.
Fuente por commit y no por el tarball_url de GitHub, que es /archive/-style y se
genera al vuelo. hammer clona con --filter=blob:none, así que el árbol Gecko no
trae historia.
Sexto muro: «error: the listed checksum of third_party/rust/zeitstempel/src/unix.rs
has changed». Los crates de Rust vienen VENDORIZADOS en el tarball y cargo
verifica el sha de CADA fichero contra .cargo-checksum.json; en cuanto un parche
de musl toca uno, el build muere. Cargo no ofrece forma de recalcularlo, así que
la salida —la misma del APKBUILD de Alpine— es vaciar la lista de ficheros del
manifiesto dejando el checksum del paquete.
Se hacen los CINCO de una (audio_thread_priority, cc, zeitstempel, wgpu-hal,
alsa) y no sólo el que reportó el error: cargo los destapa de a uno, y buscarlos
por prueba y error costaría cinco vueltas de configure de las que cada una tarda
minutos.
El bucle falla RUIDOSO si un crate no está, en vez de seguir en silencio: si
upstream mueve uno, quiero enterarme acá y no tres capas más abajo.
«cbindgen version 0.27.0 is too old. At least version 0.29.4 is required.»
Subir es gratis: yupana radio cbindgen = 0, ninguna receta la declara todavía —
es herramienta de la plataforma Gecko y nada más.
De paso pasa de tarball /archive/refs/tags/ a commit pineado. Esos tarballs los
genera la forja al vuelo y su sha256 cambia cuando el servidor actualiza git/gzip
(ADR 0006); ya que había que tocar la fuente, se deja anclada. El tag 0.29.4 es
LIGERO —una sola ref en ls-remote, sin ^{}— así que el sha ES el commit.
Cuarto fallo de configure: KeyError: RUST_TARGET. Se lee como un bug de Firefox y
es en realidad el CONTRATO del parche que trajimos: fix-rust-target.patch
reemplaza la autodetección del triple de Rust por un os.environ["RUST_TARGET"]
pelado, y el APKBUILD de Alpine lo exporta como $CTARGET. Traer el parche sin la
variable es traer media cosa.
El valor NO se adivinó: sale de ls .dev-fs/alpine/usr/lib/rustlib/ en el lab, que
da x86_64-alpine-linux-musl. El rustc del sandbox es el de Alpine, no el del host
— el del host es x86_64-unknown-linux-gnu y usarlo habría producido un
cross-compile silencioso, que es peor que un error.
Va en las TRES fases: mach lo vuelve a leer al compilar e instalar.
Van cuatro muros de configure y cada uno destapó al siguiente: linker → ar →
libstdc++ estática (que forzó compiler=gcc) → RUST_TARGET.
Tercer fallo de configure, y el que decide: «Firefox does not support linking
statically with libstdc++». No es un flag. flags.configure:79 compila un C++
mínimo, lo pasa por llvm-objdump --private-headers y EXIGE encontrar un
`NEEDED …libc++`; zig enlaza libc++ estática para musl, así que ese NEEDED no
existe nunca. Es una negativa de Mozilla, no una opción.
Cambiar a gcc derriba los tres muros que zig-cc levantó, de una vez:
1. el sondeo del linker (gcc se anuncia como «GNU ld», que Firefox reconoce),
2. «Cannot find ar» (hay un ar de verdad; se quitan los wrappers de zig),
3. libstdc++ COMPARTIDA — el gcc del lab es 15.2.0 con libstdc++.so.6.
Es una excepción consciente y del mismo tipo que la que el repo ya mantiene para
el kernel y cmake (ADR 0011), a la que se llegó por eliminación y no por
comodidad: los tres intentos con zig-cc están documentados en la cabecera.
⚠ El precio queda escrito en la receta: el lab NO entra en hash_inputs, así que un
Firefox construido con el gcc del lab queda más expuesto a la deriva del rootfs
que uno con zig-cc — dos labs con gcc distinto pueden sellar bytes distintos sin
que el store lo note. Misma deuda que ya cargan el kernel y las otras recetas
compiler=gcc, y razón para no ampliar esa lista sin agotar antes el camino zig.
Segundo fallo de configure: «ERROR: Cannot find ar». moz.configure hace
check_prog("AR", …) y en el sandbox no existe un ejecutable llamado `ar`: existe
`zig ar`. Un AR="zig ar" con espacio tampoco sirve, se ejecutaría como un binario
único llamado «zig ar» — el mismo gotcha que ya documentan la receta de ffmpeg y
el de los crates cc en las recetas Rust.
Wrapper de UN SOLO TOKEN por herramienta (ar, ranlib, nm, objcopy), que es el
patrón ya establecido en el corpus, exportados en configure Y en compile porque
las dos fases los necesitan.
El linker ya pasó: este fallo es posterior, lo que confirma que quitar
--enable-linker fue correcto.
Primer intento de build: configure murió con «Could not use lld as linker».
NO era que faltara lld. toolchain.configure sondea con
`$CC -fuse-ld=lld -Wl,--version` y clasifica por la SALIDA, buscando «mold»,
«GNU ld», «GNU gold» o «LLD». zig se identifica como `zig ld 0.16.0`, que no casa
con ninguno ⇒ kind = "unknown".
«unknown» por sí solo NO rompe: el código lo acepta explícitamente. Lo que rompe
es PEDIR el linker por nombre, porque eso vuelve fatal cualquier fallo del sondeo:
if linker: result = try_linker(linker)
if result is None: die("Could not use %s as linker")
Sin el flag, unas líneas más abajo Firefox prueba lld igual —c_compiler.type es
clang y la versión 21.1.0 pasa el umbral de 15.0— y si el sondeo falla sigue a
gold y al linker por defecto, que es exactamente el lld que zig trae adentro.
Mismo linker, sin el die.
Diagnóstico hecho leyendo toolchain.configure en el árbol del worker, no
suponiendo: el comando sondeado devuelve rc=0 y sí imprime la versión cuando se
corre a mano.
El nodejs que selló en el LXC sin strip pesa 984 MB, de los cuales .debug_info son
497 MB y .debug_line otros 43: MÁS DE LA MITAD del artefacto es información de
depuración, para una herramienta que nadie va a depurar y que ni siquiera se
publica.
Se aplica también a firefox ANTES de construirlo, que es donde importa: Firefox es
varias veces V8 y sin strip su artefacto entraría en varios GB. El store no es
sólo disco — se sincroniza hub↔worker y se respalda.
Es el hallazgo del SDD 23: el 79% del contenido binario del store era .debug_*, y
quitarlo hace que artefactos que no reproducían PASEN a reproducir, porque lo que
difería eran las rutas del árbol de build embebidas en esas secciones.
El campo entra en hash_inputs a propósito (cambia el contenido) y usa zig objcopy,
que siempre está en el sandbox ⇒ no agrega dep de build. Node se reconstruye.
154 y no 155 pese a que Zen sigue 155.0.1 y Waterfox también va por release: los
parches de musl de Alpine son para 154.0, y son ONCE. Ese es el trabajo de
portabilidad que un import de nix pierde y sin el cual Firefox no compila contra
musl. Un Firefox que compila con el set probado vale más que uno con el número
correcto que no compila; y como Firefox se mueve cada 4 semanas, la paridad exacta
con Zen es una cinta de correr. Lo que se reutiliza entre los tres es la
PLATAFORMA (gtk3/nodejs/clang18/cbindgen, ya en el corpus) y este set de parches.
Subir a 155 después es un rebase, no un port.
Se traen los 11 de musl y NO los de ppc64le, loongarch ni Android: cada parche que
no hace falta es una forma más de que un rebase falle sin motivo.
TODO BUNDLEADO salvo GTK3. Alpine usa --with-system-{icu,nspr,nss,av1,libvpx,
webp,libevent} y de ésas el corpus tiene cero; Firefox las trae en el árbol.
Menos piezas móviles para el primer build, que es cuando conviene minimizar
variables.
SIN BRANDING OFICIAL, y no es descuido: el binario lleva once parches, y poner el
nombre y el logo de Firefox sobre un build modificado entra en la política de
marcas de Mozilla — es la historia de Iceweasel en Debian. Misma cautela que
dejavu-fonts-nerd con la licencia de Bitstream Vera: una fuente modificada no
puede llamarse como la original, y un navegador parcheado tampoco.
HERMÉTICO: --disable-bootstrap (su trabajo es descargar toolchains),
MACH_BUILD_PYTHON_NATIVE_PACKAGE_SOURCE=system (si no, mach arma un virtualenv con
pip y sale a la red) y MOZBUILD_STATE_PATH al árbol (por defecto escribe en $HOME,
que en el sandbox no es suyo). Los crates vienen vendorizados en el tarball.
Wayland-only heredado de gtk3 (-Dx11_backend=false) ⇒ este Firefox NO correrá como
cliente X11 ni bajo Xwayland. Escrito en las dos recetas.
Firefox aborta con «Could not find a Node.js executable»: lo usa para empaquetar
los bundles de JS. Waterfox y Zen lo necesitan igual ⇒ al corpus con el resto de
la plataforma.
TODO BUNDLEADO, a diferencia de Alpine, que lo construye contra diez deps del
sistema de las cuales el corpus tiene dos. La decisión de fondo: acá Node es un
COMPILADOR, no software que la distro publique — entra al sandbox de Firefox y
desaparece, así que el cierre chico gana al cierre puro y el precio (binario
gordo, openssl bundleado que nadie expone) no lo paga ningún usuario. Si algún
día se quiere PUBLICAR Node, esa es otra receta; queda avisado en la cabecera.
Dos AVX-512 apagados, y los dos por la perilla que upstream mismo deja:
· simdjson → SIMDJSON_AVX512_ALLOWED=0. ⚠ EN EL .h Y EN EL .cpp: la librería se
distribuye AMALGAMADA y el .cpp lleva su propia copia (línea 2493), así que
tocar sólo el header no sirve — y engaña, porque ninja sigue con otros objetos
y el log parece avanzar.
· simdutf (dentro de V8) → se antepone SIMDUTF_IMPLEMENTATION_ICELAKE 0, que es
el override que habilita su #ifndef.
No se pierde nada: todo el corpus va -mcpu=baseline y ambas eligen implementación
en RUNTIME. CXXFLAGS NO sirve acá — GYP no lo propaga a la línea de compilación
(verificado buscando el -D en el comando que ninja reportó al fallar).
EL PARALELISMO SE CAPA POR RAM Y SIN ESCRIBIR UN NÚMERO. Sin cap, ninja lanza
nproc+2 y muere con `job terminated due to signal 9` en el objeto 2936/4414
compilando lo que genera Torque (~2 G por job). Un `-j2` fijo ataría el
ArtifactHash a la RAM de quien escribió la receta; la cuenta es un job cada 3 GiB
de MemTotal, mínimo 1, tope nproc. Da 2 en momento (7,6 GiB) y 5 en el LXC
(16 GiB, 6 cores) con el MISMO texto y el mismo hash.
Genera las cabeceras C++ de los componentes Rust de Gecko (Stylo, WebRender) y el
build de Firefox la exige. Mudanza gratis: [deps] vacío ⇒ no hay cierre que
cambiar, mismo ArtifactHash (b3:bd2e6ca6) en las dos rutas, radio 0.
El build de Firefox corre bindgen sobre los headers de C++ y bindgen carga
libclang.so en RUNTIME. La receta ya existía —nació en incoming-cosmic por un
crate *-sys del portal, y su commit de origen dice «sólo libclang.so»— pero desde
el corpus no se alcanza una cola hermana. Sin esto no hay Firefox, ni Waterfox,
ni Zen.
Mudanza medida ANTES, no después: sus cinco deps (cmake, samurai, python3,
pkgconf, llvm18) viven sólo en el corpus, así que incoming-cosmic ya las resolvía
por caída al padre y el cierre no cambia. hammer hash dio el MISMO ArtifactHash
(b3:25d95279) en las dos rutas.
Verificado que xdg-desktop-portal-cosmic —su único dependiente— sigue SELLADO y
que la cola cosmic queda en 36/36 sin deuda: cero rebuilds.
Se MUEVE y no se copia: dos ficheros para un artefacto es el cuadro de las dos
glib esperando a que uno de los dos derive.
GTK3 no existía en NINGUNA cola y es el único toolkit de Gecko en Linux. Como
Waterfox y Zen son forks de Gecko, los tres navegadores consumen exactamente esta
misma pieza: una receta sostiene a los tres. Por eso va al CORPUS y no a una cola
—una receta resuelve sibling-first y después el catálogo padre, nunca una cola
hermana—, que es la lección de mpv y justo lo que OBS no pudo cumplir al quedar
atado a incoming-kde por Qt6. La plataforma nace bien puesta para que los tres
navegadores no repitan ese error.
Al medir el cierre antes de escribir, TODO estaba ya en el corpus salvo `atk`. Un
ladrillo chico destrabando una pieza grande: por eso atk fue primero.
WAYLAND-ONLY de verdad, no de intención: -Dx11_backend=false. A diferencia de OBS
—que exigía find_package(X11) incondicional— GTK3 sí deja apagarlo, y eso saca
libX11/libXext/libXi/libXrandr/… del cierre. Esa cadena vive sólo en incoming-kde
y meterla en el corpus la pondría en las CINCO imágenes.
Verificado en el artefacto: los .pc son gdk-wayland-3.0 y gtk+-wayland-3.0 (no hay
gdk-x11-3.0), libgtk-3.so.0 no enlaza X11 ni xcb, y libgdk-3.so trae NEEDED
libwayland-client/cursor/egl + libxkbcommon.
⚠ Consecuencia escrita en la receta: un Firefox contra este GTK3 no correrá como
cliente X11 ni bajo Xwayland. Bajo Wayland nativo sí. Es la postura de la distro
aplicada al navegador, no un accidente.
Las deps van a las variantes -shared EN LUGAR DE las estáticas, nunca junto a
ellas: libgtk-3.so es un objeto compartido y un .a sin PIC adentro da
`relocation R_X86_64_32 against png_default_write_data`. Se sustituye y no se suma
porque ambas variantes instalan los MISMOS .pc y cabeceras — declarar las dos
serían dos artefactos peleando por lib/pkgconfig/glib-2.0.pc.
Una receta sellada que ningún perfil declara NO ESTÁ EN NINGUNA IMAGEN, y la
métrica de clausura no lo puede ver porque mide lo declarado — la lección que este
fichero ya aprendió con foot en sway y con las 22 apps KDE. OBS estaba sellado y
en ninguna imagen.
Entra SÓLO en escritorio-kde, y no por preferencia: su frontend es Qt6 y las 13
recetas Qt viven sólo en esa cola. Verificado con `yupana perfiles obs-studio`.
El perfil pasa de 263/263 a 274/274 — arrastra 11 nodos a la clausura, todos ya
sellados, y el grafo sigue cerrando.
De paso: el aviso de los «tres huecos de mpv» estaba repetido en los CUATRO
perfiles de escritorio y hoy quedó vencido en los cuatro (ALSA/PipeWire, Lua/OSC
y vaapi están cerrados). Corregidos los cuatro; el assert de count==1 fue lo que
destapó que no era uno solo.
Entran libglvnd, uthash, nlohmann-json, jansson, x264, simde, pciutils-shared y
mbedtls al corpus, y obs-studio a incoming-kde. Cero en deuda en los 7 perfiles.
13 plugins sellados, entre ellos los que importan: linux-pipewire (captura de
pantalla en Wayland), obs-ffmpeg, obs-x264, obs-outputs (RTMP RTMP),
linux-pulseaudio/alsa y text-freetype2.
EL PORT. El glad de OBS hacía dlopen("libGL.so.1") + dlsym("glXGetProcAddressARB")
y devolvía 0 si no lo hallaba, o sea que el renderer no arrancaba: OBS estaba
atado a GLX —y por tanto a X11— aun corriendo en Wayland puro. Acá nadie publica
libGL.so.1 y el libglvnd del corpus va sin GLX a propósito. El parche carga por
libEGL.so.1 + eglGetProcAddress, que es lo que OBS ya usa para las extensiones.
Verificado en el artefacto, no en el log: libobs-opengl.so.30 tiene CERO
ocurrencias de glXGetProcAddressARB, sus NEEDED son libEGL.so.1 +
libwayland-egl.so.1, y el binario no filtra a glibc.
DÓNDE VIVE. En incoming-kde y no en el corpus porque su frontend es Qt6 y las 13
recetas Qt viven sólo en esa cola. Se midió antes de descartar el corpus: qtbase
sin X11 cierra en 26 nodos que YA están en el corpus, pero el qtbase de la cola va
xcb=ON, así que un qtbase de corpus tendría otro hash con el MISMO nombre de
artefacto ⇒ dos Qt peleando por /usr/lib/libQt6Core.so. Unificar obligaría a
apagarle xcb a KDE: radio 119.
Los símbolos indefinidos salieron por CAPAS y ninguno nombraba al culpable:
freetype (.a no PIC) → fontconfig (idem) → i2d_SSL_SESSION → inflateInit_. Las dos
primeras se arreglaron usando las variantes -shared que YA existían en el corpus;
las dos últimas con -lssl -lcrypto -lz, porque OBS resuelve curl con el FindCURL
de CMake, que toma libcurl.a por fichero y no lee su Libs.private. Un símbolo
indefinido nombra a la librería que lo USA, no a la que falta.
Huecos escritos en la receta para que no se descubran en la mano del usuario:
sin webcam (ENABLE_V4L2=OFF, libv4l2 no está en ninguna cola), sin salida MPEG-TS
(SRT/RIST ausentes) y sin scripting (falta swig). Grabar, capturar pantalla y
publicar por RTMP no pasan por ninguno de los tres.
pciutils-shared: obs-ffmpeg (el plugin de codificación, sin perilla para
apagarlo) hace find_package(Libpci) para identificar la GPU por su ID. La
pciutils canónica va SHARED=no e instala SÓLO bin/sbin/share — provee.py
confirmaba que libpci.so no lo publicaba NINGUNA cola. Patrón zlib-shared: una
variante, sin mover el hash de la canónica.
Verificado que NO colisionan listando los dos artefactos: la variante sólo pone
usr/lib y usr/include; la canónica no pone nada en usr/lib. Rutas disjuntas
conviven; rutas pisadas son el cuadro de las dos glib. Por eso el install usa el
target `install-lib` y no `install`, que traería los binarios.
mbedtls: obs-outputs (streaming RTMP) hace find_package(MbedTLS REQUIRED) y NO
acepta OpenSSL — no hay opción en su CMake, está cableado.
Va por TARBALL DE RELEASE y no por commit, que es excepción consciente al ADR
0006: mbedtls 3.6 tiene framework/ como submódulo y [source] de hammer no clona
submódulos, así que el árbol llegaría incompleto. Un fichero de
/releases/download/ lo sube upstream y su sha256 es estable; lo inestable es
/archive/<tag>, que la forja genera al vuelo. La propiedad que el ADR exige —que
la fuente sea la misma mañana— se cumple igual.
PIC ON porque quien la consume es obs-outputs.so, un módulo dlopen.
libobs/CMakeLists.txt:10 hace find_package(SIMDe REQUIRED) y linkea SIMDe::SIMDe,
así que no es opcional para OBS. Sólo cabeceras, pero se instala por meson y no
copiando a mano porque genera simde.pc, que es por donde el FindSIMDe de OBS lo
encuentra: dejar las cabeceras sin su fichero de descubrimiento da el «está pero
no lo encuentra», peor de diagnosticar que una ausencia.
Las cuatro no existían en NINGUNA cola (medido cruzando el grafo, no con grep).
Van al corpus y no a una cola porque OBS es una app que quieren las cuatro
imágenes, y una receta resuelve sibling-first y después el catálogo PADRE, nunca
una cola hermana.
Verificadas por CONTENIDO del artefacto, no por exit 0 (regla 3 del repo):
· uthash 5 cabeceras; no compila nada y la fase `compile` dice `true`
explícito, porque que una librería sea sólo cabeceras es un
hecho suyo, no un descuido de la receta.
· nlohmann-json pasa por CMake y no por un `cp` para que instale sus
nlohmann_jsonConfig/Targets.cmake: OBS hace find_package, y
copiar los .hpp a mano dejaría «está pero no lo encuentra».
· jansson libjansson.so.4.
· x264 libx264.so.165 + x264.pc, y `asm: yes` — con SIMD, que es la
lección que ffmpeg acaba de costar hoy.
⚠ jansson trajo su propia trampa: la perilla es JANSSON_BUILD_SHARED_LIBS, NO la
estándar BUILD_SHARED_LIBS de CMake (CMakeLists.txt:5, default OFF). Con la
estándar el build sale exit 0 y sella un artefacto con SÓLO libjansson.a — la
receta diciendo link="dynamic" y el artefacto estático. Se vio mirando usr/lib/
del artefacto, no el código de salida.
Hasta hoy NINGUNA cola publicaba libGL.so, libGL.so.1 ni gl.pc (medido con
provee.py): mesa va -Dglx=disabled -Dglvnd=false, así que sólo había libEGL y
libGLESv2. Toda app de GL fijo compilaba —mesa SÍ instala GL/gl.h— y moría al
LIGAR. Lo destapó OBS: deps/glad linkea PUBLIC OpenGL::GL, que ES libGL.so, y eso
es un link, no un gate de configure que se pueda apagar.
SIN GLX Y SIN X11, que es lo que hace esto coherente con la distro: glvnd parte el
libGL.so.1 histórico en libGL.so.1 (GL+GLX ⇒ implica X11) y libOpenGL.so.0 (GL
pelado ⇒ no implica nada). Vamos por la segunda. Encender glx metería
libX11/libxcb/xorgproto —hoy sólo en incoming-kde— en el CORPUS, o sea en las
cinco imágenes, para servir a una sola app.
Entrega verificada en el artefacto: libOpenGL.so.0, libGLdispatch.so.0,
libEGL.so.1, libGLESv{1_CM,2}, y opengl.pc. Cero GLX.
El wrapper: meson emite `-Wl,--version-script <ruta>` con ESPACIO. Con gcc anda de
casualidad (reenvía al linker el argumento suelto que no entiende); zig-cc lo
rechaza con «unrecognized file extension». El wrapper une el par con `=`. No se
parchea upstream: el defecto es del contrato meson↔driver, no de glvnd.
⚠ Instala libEGL.so.1, que HOY la pone mesa. Mismo soname, otro dueño ⇒ mesa se
reconstruye con -Dglvnd=true en esta misma campaña, no después.
libva sube al corpus y la copia de incoming-kde se JUBILA (git mv, no copia: dos
ficheros para un artefacto es el cuadro de las dos glib esperando a derivar). La
mudanza fue gratis y medida antes: las cinco deps de libva viven sólo en el
corpus, así que incoming-kde ya las resolvía por caída al padre y hammer hash dio
el MISMO ArtifactHash en las dos rutas (b3:405c6211).
libva pasa a -Dwith_wayland=yes, que eso SÍ cambia el hash. La razón está en
mpv/meson.build:1464: el feature `vaapi` se requiere contra
`vaapi-drm or vaapi-wayland or vaapi-x11 or vaapi-win32` ⇒ -Dvaapi=enabled a
secas no habilita NADA. De los cuatro backends, x11/win32 están fuera por
Wayland-only y vaapi-drm exige features[drm] de mpv, apagado porque vo=drm pide
libdisplay-info. Queda vaapi-wayland, que pide libva-wayland.pc.
Evidencia, no sólo sello: meson lista «vaapi vaapi-wayland» entre los features
habilitados, y el binario sellado trae NEEDED libva.so.2 + libva-wayland.so.2.
Radio medido antes de tocar: 2 dependientes (kpipewire, spectacle), los dos
reconstruidos. Las 7 imágenes siguen en 0 en deuda.
⚠ El driver es RUNTIME (intel-media-driver / gallium VA por dlopen): que esto
selle y que --hwdec=vaapi funcione en una máquina son cosas distintas.
La receta llevaba --disable-x86asm heredado de cuando ffmpeg entró SÓLO para
kpipewire (codificar un stream de escritorio, donde da igual). Para el
reproductor era decodificar sin SIMD.
Dos recetas —ffmpeg.toml y mpv.toml— prohibían tocarlo con la misma razón: un
hash distinto dejaría DOS ffmpeg peleando por las mismas rutas en una imagen KDE,
y el arreglo correcto sería «promover una sola y jubilar la de la cola». Eso ya
había pasado: incoming-kde/ffmpeg.toml no existe y hay UNA sola receta ffmpeg en
todo el disco. La prohibición sobrevivió a la condición que la justificaba.
--x86asmexe=nasm explícito y nasm (2.16.03) a [deps].build: sin ensamblador
declarado configure apagaría x86asm en SILENCIO, sellando un artefacto con otro
hash y sin la SIMD que dice traer.
Evidencia, no sólo sello: configure imprime «x86 assembler nasm», compila los
objetos X86ASM, y libavcodec.so pasa de 12.685.744 a 14.454.288 bytes (+1,77 MB).
Radio medido antes de tocar (yupana radio ffmpeg): 4 dependientes, ninguno
transitivo de más — mpv, wf-recorder, kpipewire, spectacle. Los 4 reconstruidos y
sellados; las 7 imágenes siguen en 0 en deuda.
Todo el encabezado del fichero era comentario `#`, así que __doc__ era None y
`main()` hace `print(__doc__)` en dos caminos: `--help` y verbo desconocido. La
puerta única de la metodología no sabía explicarse, y el error de verbo tampoco
decía cuáles son los verbos válidos.
La ayuda operativa pasa a docstring; el diseño y el porqué se quedan arriba como
comentario. Cotejada contra el despacho real: los 11 verbos de la ayuda existen y
los 11 que existen están en la ayuda (faltaba `sembrar`).
targets.toml declara 7 perfiles; drenaje.json listaba 5. escritorio-cosmic (cola
incoming-cosmic) y escritorio-sway (cola incoming-wlr) declaraban una cola que
ningún grafo de GRAFOS contenía, así que cargar() devolvía None y --todos hacía
`continue` pelado. Dos imágenes enteras fuera del artefacto sin dejar rastro, y
los dos números —5 medidos, 7 declarados— no se cruzaban en ningún lado.
Mismo olvido que ya costó 17 días de build-state-wlr.json congelado: la lista de
grafos crece a mano y se queda atrás cuando se abre una cola.
Tres cambios:
- GRAFOS suma los grafos de cosmic y wlr.
- Un perfil sin medir se anota en el artefacto (`sin_medir`), se grita por stdout
y drenar.py sale 1. Regla 3 del repo: un ausente falla ruidosamente.
- cosecha-cron deja de tragarse la salida; filtra las líneas ⚠ al log, que si no
llegaban como un "falló" mudo que no dice cuál imagen falta.
Medido: las 2 imágenes que nadie miraba estaban limpias, 0 en deuda. Ahora son
7/7 verificadas en vez de 5 verificadas y 2 supuestas.