`scripts/vigia-sonames.py` sobre los cinco perfiles: `escritorio-mirada` era el único que aún tenía
la fuga al sysroot del lab en componentes de RUNTIME. `mesa-swrast` y `wayland` salen con
`NEEDED libexpat.so.1`, `libz.so.1` y `libffi.so.8`; las recetas canónicas de expat, zlib y libffi
son `--disable-shared`; ningún artefacto del cierre publicaba esos SONAME. O sea que el rootfs los
resolvía contra el Alpine DEL LAB y el USB sólo arrancaba donde hubiera Alpine debajo — que es peor
que una dep faltante, porque el lab no entra en `hash_inputs` y el store no puede ni notarlo.
Es la misma fuga que los otros cuatro perfiles cerraron el 2026-09-03. mirada quedó fuera porque su
lista nació como copia literal del PKGS de `mirada-usb.sh` y nadie la revisó desde entonces. La
dirección hoy está invertida —el script deriva su PKGS de `targets.py escritorio-mirada`— así que
arreglarlo acá arregla el USB, y no hay dos listas que puedan divergir. Actualicé el comentario, que
seguía diciendo «lift verbatim».
Las tres viven en `corpus`, la misma cola del perfil, y ya están selladas ⇒ CERO rebuilds: sólo
entran a la clausura.
NO sumo `bzip2-shared` ni `ncurses-shared`, que sí llevan los otros perfiles: acá esos sonames los
pide únicamente `python3`, que es herramienta de build y no viaja en la imagen. El vigía imprime
siempre QUIÉN pide cada soname justamente para poder separar eso; sin esa columna su informe no se
puede triar y uno acaba tapando ruido.
Verificado: mirada pasa de 10 sonames sin proveedor a 7, y los 7 que quedan son todos de `python3` y
`perl`.
Las dos recetas apuntan a `ssh://gitea@git.gioser.net:2345/sergio/hammer.git`, o sea a este mismo
árbol, que declara MIT en `Cargo.toml` y trae su `LICENSE`. No hace falta clonar nada para saberlo:
la evidencia estaba en el directorio de trabajo. Los dos ArtifactHash, idénticos.
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.
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.
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.
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ó.
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.