25 Commits
Author SHA1 Message Date
Sergio e852f48491 takana etapa 5a: los comentarios de las 741 recetas
Barrido de TEXTO puro: 1427 líneas de comentario TOML. Cero ArtifactHash
movidos, y eso está MEDIDO, no deducido: hasheé las 741 antes y después y los
ficheros de hashes son idénticos byte a byte (723 con hash real + 18 que ya no
hasheaban de antes).

El guardián valió la pena: el primer barrido, filtrando por 'la línea empieza
con #', movió el hash de helix, lsof y steam-runtime-sniper. La causa es que una
fase se escribe como compile = <triple> ... <triple> y sus comentarios de SHELL
también empiezan con #, pero viven dentro del VALOR — y las fases sí entran en
hash_inputs. El barrido ahora calcula los rangos de las cadenas multilínea de
TOML y no entra ahí.

Quedan intactos a propósito: .hammer-zig-cc y .hammer-cargo-vendor (literales
dentro de fases), hammerd, hammer-recover y toda ruta que empiece por /
2026-09-09 19:23:26 +00:00
Sergio c3413d293e jarlog APARCADO: rompe atuq, y el coste está medido mientras el beneficio no
Al reconstruir atuq sobre el firefox con jarlog, murió:

    zipfile.BadZipFile: Bad magic number for central directory  (rebrand.py)

El jarlog convierte el omni.ja al formato «jar optimizado» de Mozilla, que mueve
el directorio central al principio:

    sin jarlog:  PK\003\004    ZIP estándar — zipfile lo abre, 5306 entradas
    con jarlog:  \376\204#\0   zipfile lo rechaza

Y atuq reempaqueta el omni.ja con zipfile para su branding. O sea que el jarlog
rompe el navegador propio de la distro.

La cuenta es asimétrica: el COSTE está medido y el BENEFICIO no —el banco corre
con caché caliente, donde reordenar el omni.ja no ahorra ninguna lectura, y dio
-0,1%, por debajo del suelo de ruido del propio banco—. Cambiar algo que
funciona por una ganancia que no se pudo medir, rompiendo algo que sí
funcionaba, es mal negocio.

Revertido a los hashes YA construidos y medidos (perfil 3647c6be, firefox
3d199174, atuq fab2fbfb): cero reconstrucciones. La documentación va fuera de
los campos hasheados, verificado antes y después.

Se retoma cuando (1) se mida el arranque EN FRÍO, que es su terreno, y (2)
rebrand.py sepa leer el jar optimizado o des-optimizarlo antes. El blob
92497cdd del mirror ya trae el jarlog: retomarlo es cambiar una línea.

Y LA LECCIÓN, que es la que más vale: el jarlog parecía GRATIS. «Es lo que hace
upstream y no cuesta nada» lo escribí yo en este mismo documento hace unas
horas. El coste no apareció midiendo el jarlog sino CONSTRUYENDO LO QUE DEPENDÍA
DE ÉL. Una función que se declara gratuita sin haber reconstruido a sus
consumidores no es gratuita: es no medida.
2026-09-08 14:08:25 +00:00
Sergio d9833a581b jarlog: la mitad del PGO que faltaba — el orden de omni.ja para el arranque
El jarlog registra en qué orden se LEEN los ficheros dentro de omni.ja al
arrancar; con él, el empaquetador los reordena y el arranque hace lecturas
secuenciales en vez de saltar por el archivo.

NO hizo falta la extensión Quitter de Mozilla, que era lo que yo daba por
bloqueante: basta MOZ_JAR_LOG_FILE en el entorno del navegador. Su
profileserver.py sólo traduce JARLOG_FILE a esa variable — leerlo costó un grep
y ahorró rehacer el arnés entero.

Y se genera en una corrida APARTE, que está medido y no supuesto. Mozilla lo
emite en la misma sesión del profileserver; nuestro arnés arranca un navegador
POR PÁGINA, así que había que saber si el fichero se acumula o se pisa. Se pisa:
tras rejilla.html y tras tablas.html dio exactamente los mismos 27.652 bytes y
469 líneas. Su contenido lo domina el ARRANQUE, no la página, así que una
corrida dedicada vale igual que una de 46 — rehacer el perfilado sólo para
obtenerlo habría sido gasto sin diferencia. Se conserva el profdata ya medido
(el del -11,4%), que es lo que hace comparables los números.

Guardián propio: un jarlog vacío o que no nombre los archivos reales NO rompe el
build de firefox, sólo deja el omni.ja sin ordenar — o sea que se pierde justo
lo que se vino a buscar, en silencio. Se exige que mencione los DOS archivos que
el navegador abre (omni.ja y browser/omni.ja).
2026-09-08 02:16:53 +00:00
SergioandClaude Opus 5 e0d9752e19 firefox: anotada la deuda del lanzador que pierde AV1 (sin mover el hash)
Su `/usr/bin/firefox` tiene el MISMO lanzador que tenía atuq: el appdir una sola
vez en `LD_LIBRARY_PATH` ⇒ Gecko se come ese primer elemento al lanzar el RDD ⇒
el ffvpx bundleado no carga ⇒ AV1 no reproduce, en silencio.

No se aplica todavía a propósito: el lanzador vive dentro de la fase `install`,
así que tocarlo mueve el ArtifactHash y obliga a reconstruir firefox entero
(LTO+PGO, ~4 h) y con él atuq. Ningún perfil declara `firefox` —el navegador que
se shipea es atuq, ya arreglado—, así que hoy es latente. Se paga en el próximo
re-hash por otro motivo, que es cuando cuesta cero.

Comprobado que el comentario NO mueve el hash: b3:1d730334 antes y después.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNFzrqFBEt6W7maGJKf2Xs
2026-09-07 17:44:23 +00:00
Sergio e6e4d9260d PGO: el perfil sellado y firefox consumiéndolo (SDD 26, unidad 3.a)
merged.profdata: 501.521 funciones, 3.734.991 bloques, 5.885.254.799 ejecuciones
registradas. Recogido corriendo firefox-instrumentado sobre el corpus de
entrenamiento de Mozilla (build/pgo: blueprint para maquetación, js-input y
sunspider para el motor JS), 35 de 36 páginas servidas.

VIVE COMO FUENTE PINEADA, NO COMO RECETA QUE LO GENERA. El perfil NO es
determinista —los contadores dependen del timing—, así que una receta que lo
produjera tendría un ArtifactHash estable sobre bytes cambiantes: firefox
consumiría cosas distintas en la misma dirección y dejaría de reproducir sin que
nada lo dijera. Es el modo de fallo del lab fuera de hash_inputs, fabricado a
propósito. Sellarlo una vez y pinearlo por sha256 hace que el insumo sea no
determinista y el build vuelva a serlo.

El blob (17.426.112 bytes, xz) está publicado en hammer/fuentes/<sha256>.tar del
Storage Box y verificado bajándolo de vuelta por el mismo camino que usa hammer.
La URL upstream no resuelve por DNS a propósito: es el patrón que el ADR 0013 ya
prueba, y como la URL nunca estuvo en hash_inputs, mover el objeto de origen no
re-hashea nada.

Dos cosas que costaron y quedan escritas:

 · strip_components = 0. hammer recorta un componente al extraer (los releases
   GNU traen un proyecto-version/ de más) y este tarball lleva el fichero en la
   raíz, así que el default se llevaba lo único que había. El error era
   «cp: cannot stat merged.profdata» y no menciona el recorte por ningún lado.

 · SIN --with-pgo-jarlog, y es una mitad que falta, no un olvido. Alpine pasa
   además un jarlog que reordena omni.ja para acelerar el ARRANQUE; lo emite el
   profileserver.py de Mozilla con su extensión Quitter, que la corrida headless
   no tiene. Hoy se gana la disposición de CÓDIGO y no el orden del omni.ja.

Y VIAJA UN ARREGLO QUE NO ES DE PGO, porque firefox se reconstruye igual: EL
ARTEFACTO NO ARRANCABA. Medido sobre el sellado: `firefox --version` moría con
«Error loading shared library libnspr4.so / Couldn't load XPCOM». mach install
deja /usr/bin/firefox como symlink pelado, el binario no trae RUNPATH y el
cierre no publica ld-musl-x86_64.path, así que las librerías que viven junto al
binario no las encuentra nadie. atuq andaba sólo porque su receta añade este
mismo lanzador; firefox, que va en las CUATRO imágenes de escritorio, no lo
tenía. Es el NEEDED colgante un escalón más allá —no falta la librería, falta el
modo de encontrarla— y por eso vigia-sonames.py no puede verlo: las librerías SÍ
están en el artefacto y las cuenta como propias.

firefox: b3:8116bdec -> b3:1d730334.
2026-09-07 10:04:42 +00:00
Sergio 3171bc6699 recetas: apuntar al vigía que EXISTE, no al que borré
Siete recetas decían «lo vigila `scripts/audit-needed.sh`» y ese script ya no
existe: lo borré yo mismo en 02facebe, bien borrado, porque duplicaba a
`scripts/vigia-sonames.py` — que además ya estaba en el repo y ahora corre en el
latido. Pero las referencias quedaron, escritas por mí unas horas antes.

Es exactamente la etiqueta que se lee como el hecho, y en mi propia letra: un
comentario que afirma que existe un guardián manda a quien dude de una dep de
runtime a correr un script que no está. En el mejor caso pierde diez minutos; en
el peor concluye que no hay con qué comprobarlo.

Ahora apuntan a `vigia-sonames.py` y a `docs/state/sonames.txt`, que es el
fichero que el latido regenera y donde se ve si algo empeora entre cosechas.

Ningún ArtifactHash se mueve: los siete comentarios caen fuera de campos
hasheados. Verificado uno por uno antes y después, incluido
firefox-instrumentado, que tiene un build en vuelo y se habría invalidado.

Lo levantó hammer-99 desde el frente atuq. `recipes/atuq.toml` NO se toca: su
mención es una nota histórica ya corregida por ellos («…y NO audit-needed.sh»),
y mi primer grep casi la «arregla» por no leerla.

Y la advertencia que traían vale para cualquiera con pruebas de runtime: si el
runner monta `.dev-fs/alpine` debajo, `libstdc++.so.6` se resuelve contra el LAB
y la prueba es ciega. Comprobado que la mía NO lo estaba: el rootfs hidratado
trae el del corpus (20.850.968 bytes) y no el del lab (2.804.104), y el bwrap de
la prueba nunca montó .dev-fs.
2026-09-07 00:32:10 +00:00
Sergio 5b9777bdbd declarar gcc-libs como dep de EJECUCIÓN en las seis — y el navegador arranca
Las seis recetas que el audit daba por colgantes declaran ahora
`runtime = ["gcc-libs"]`. `deps.runtime` NO entra en el ArtifactHash —medido:
firefox sigue en b3:8116bdec, atuq en b3:f2960991, waterfox en b3:88b5a762—
así que esto NO reconstruye nada. Lo que cambia es la CLAUSURA, que es lo que se
hidrata en la imagen.

EVIDENCIA DE QUE ARREGLA EL FALLO REAL, no de que compila: `atuq` bajo sway
headless, sin GPU, renderizando about:buildconfig con sus tablas y la barra de
contenedores de la v0.5. Antes moría con decenas de «Error relocating:
_ZNKSt5ctypeIcE13_M_widen_initEv: symbol not found».
docs/evidencia/atuq-arranca-con-gcc-libs-2026-09-06.png

Y el audit se corrige, porque SUB-REPORTABA en dos formas:

  · Miraba sólo ejecutables y `head -12` por artefacto. El siguiente NEEDED que
    faltaba estaba en una LIBRERÍA (libmozsandbox.so pedía libnspr4.so), así que
    daba «6 recetas» cuando el artefacto de firefox declara 30 sonames. Ahora
    recorre TODOS los ELF.
  · No contaba lo que el propio artefacto TRAE. Firefox bundlea su nspr/nss y
    las encuentra por el LD_LIBRARY_PATH que fija su lanzador; contarlas como
    colgantes era un falso positivo.

Con las dos correcciones y gcc-libs en el store, el corpus baja de 25 artefactos
colgantes a TRES, y son otra cosa:

    sqlite-shared   libreadline.so.8
    python3         libreadline.so.8
    naabu           libdl.so.2 libpthread.so.0 libc.so.6

Los dos primeros piden una receta que falta (readline). El tercero pide sonames
de GLIBC, que en una distro musl es un síntoma distinto y merece su propia
mirada. Quedan anotados, no arreglados.
2026-09-06 20:09:10 +00:00
Sergio 553fc1fb7d wasi-libcxx: el eslabón que faltaba en la cadena wasm (lo encontró el build)
El firefox con RLBox murió en el configure a los SEIS SEGUNDOS:

    /tmp/conftest.cpp:1:10: fatal error: 'cstring' file not found
    ERROR: Cannot find wasi headers or problem with the wasm compiler.

El mensaje culpa a los headers de WASI y los headers de WASI estaban perfectos.
`<cstring>` es un header de C++ y `wasi-libc` sólo trae la C: las librerías que
RLBox enjaula no son todas C —graphite2 es C++— así que el sandbox necesita una
biblioteca estándar de C++ para wasm32. Mismo patrón que la lección de los
headers UAPI: el error nombra lo que buscó, no lo que falta.

El hueco estaba en el GRAFO antes de que el build lo encontrara, y se podía ver
sin construir nada: el `wasi-sdk` de Alpine declara
`depends="wasi-libc wasi-libcxx wasi-compiler-rt"` —TRES— y nosotros teníamos
dos. Queda anotado como método: cuando se porta una cadena ajena, la lista de
depends del paquete equivalente es una comprobación de completitud gratis.

La receta es el mismo tarball de LLVM 22.1.8 que `wasi-compiler-rt` (mismo
sha256) y repite su transcripción del toolchain file de wasi-sdk, por el mismo
motivo: 38 líneas de declaraciones puras no justifican una fuente de red y una
receta. Si se toca una, se toca la otra.

Dos detalles que son trampas y no adorno:

- Se construyen los DOS triples (wasip1 y wasip1-threads). `wasi-libc` ya publica
  los dos y firefox pide el sysroot por nombre de triple, así que dejar uno sin
  libc++ sería presente-a-medias, que es peor que ausente.
- cmake instala en el triple canónico (`wasm32-unknown-wasip1`) y clang busca en
  el corto (`wasm32-wasip1`). Sin el renombre del install, libc++ queda al lado
  del sitio donde se la busca: en el artefacto e invisible para el compilador.

El guardián exige exactamente lo que el configure no encontró —`cstring`,
`libc++.a` y `libc++abi.a`— para los dos triples.

Re-hashea: wasi-sdk b3:c8a587fa -> b3:f13634c6, firefox b3:98d6899a ->
b3:2880e28a.
2026-09-06 01:42:34 +00:00
Sergio 65ee20e437 firefox: guardián que exige RLBox EN EL BINARIO, no en el configure
Tercera repetición de la misma lección en esta receta, y por eso nace junto con
la bandera y no después: `--with-wasi-sysroot` es exactamente la clase de opción
que el configure acepta sin chistar y que puede no llegar al artefacto. Ya pasó
dos veces acá (MOZ_REQUIRE_SIGNING y MOZ_BUILD_DATE) y las dos veces el síntoma
apareció lejísimos, sin nombrar la bandera.

Acá el fallo sería el más caro de los tres: un firefox que dice tener enjaulados
los parsers de fuentes y medios —graphite, ogg, expat, woff2, o sea el código
que come entrada NO confiable de la red— y no los tiene. Una promesa falsa de
seguridad es peor que no ofrecerla (§4 del SDD 26, mismo criterio que el modelo
de adversario de qullqa).

El discriminante está MEDIDO, no supuesto: sobre el artefacto b3:352d7880 —el
último con RLBox apagado, 224 MB de libxul.so— los tres patrones (w2c_, rlbox,
wasm2c) dan CERO. Así que cualquiera apareciendo prueba que la jaula entró.

Se aceptan los tres y no sólo `w2c_` a propósito: el prefijo exacto es un
detalle de la versión de wasm2c, y un guardián que falla por un cambio de
convención de nombres tira 4 h de build por algo que no está roto. Lo que no
puede pasar desapercibido es que los tres sigan en cero.

El método de comprobar el artefacto y no la doc lo aportó la sesión hammer-f8.

Re-hashea: b3:dba8fbf9 -> b3:98d6899a. Entra en el build que arranca ahora, que
es justamente para lo que se agrega antes y no después.
2026-09-06 01:39:20 +00:00
Sergio ff0b5567d6 firefox: RLBox ENCENDIDO — --without-wasm-sandboxed-libraries -> --with-wasi-sysroot
Cierra la unidad 3.b del SDD 26. El flag que se va apagaba la jaula wasm que
envuelve a los parsers de fuentes y medios (graphite, ogg, expat, woff2), o sea
el código que come entrada NO confiable de la red. Estaba apagado por una razón
honesta —«el SDK de WASI no está en ninguna cola»— que dejó de ser cierta hace
un commit: la cadena wasi-libc-headers -> wasi-compiler-rt -> wasi-libc ->
wasi-sdk sella entera.

El flag nuevo es LITERALMENTE el de Alpine, verificado hoy trayendo su mozconfig
de community/firefox: `--with-wasi-sysroot=/usr/share/wasi-sysroot`, que es la
misma ruta donde instala nuestro wasi-libc.

Las deps van las TRES y no es redundancia: en hammer las deps NO son
transitivas —el bwrap apila como overlay sólo lo que está escrito en la lista—
así que declarar sólo `wasi-sdk` no arrastraría al `wasi-libc` que necesita, y
ése es el peor fallo posible de esta cadena: el sysroot no existiría y el error
saldría horas después, hablando de un header de wasm sin nombrar a ninguna
receta. Alpine declara wasi-compiler-rt + wasi-sdk porque su wasi-sdk YA trae el
sysroot dentro; el nuestro está partido en cuatro, así que la lista es distinta
a propósito.

Esto re-hashea firefox: b3:352d7880 -> b3:dba8fbf9. Es un rebuild largo y va a
la máquina que corresponde, no a gioser (4c/7,6 G sin swap).
2026-09-06 01:32:05 +00:00
Sergio 5e489e974e firefox: BuildID determinista — se cierra la deuda de reproducibilidad, con guardián
Medido y probado hoy: `atuq` REPRODUCE bit a bit (`why-differs`: 85 entradas idénticas, 0 divergen),
lo que valida el re-empaque determinista del omni.ja, el XPI, los iconos copiados y el BuildID
derivado del contenido. El que NO reproducía era la BASE: el `BuildID` de firefox era la fecha y
hora del build (`20260905035306` en el artefacto sellado), así que dos construcciones con el mismo
ArtifactHash daban bytes distintos.

Es el agujero exacto que el invariante de reproducibilidad existe para cerrar, y `build-state.json`
no lo puede ver: el hash es input-addressed y no se mueve por esto. Verde y mintiendo.

El valor sale de `SOURCE_DATE_EPOCH`, que el sandbox de hammer YA fija a 1 para todas las recetas —
mejor que una constante escrita a mano, porque usa el mecanismo de hermeticidad que ya existe en vez
de inventar un segundo. Alpine hace lo mismo en su APKBUILD. BuildID esperado: 19700101000001.

Va en LAS TRES FASES, por la lección que costó 50 minutos esta misma tarde con MOZ_REQUIRE_SIGNING:
son shells distintos y `mach build` re-ejecuta el configure cuando le parece.

Y con su GUARDIÁN, hermano del de la firma: la fase install compara el BuildID del artefacto contra
el que SOURCE_DATE_EPOCH obliga, y falla el build si no coinciden. Los dos fallos de esta familia
—flag que el configure acepta y el artefacto ignora— son mudos y viajan lejos; la única defensa es
comprobarlos donde importa, que es dentro del artefacto.
2026-09-05 17:37:30 +00:00
Sergio 66db079b64 firefox: la bandera de firma en LAS TRES fases, y un guardián que lo comprueba en el artefacto
El build anterior selló con `MOZ_REQUIRE_SIGNING: true` DENTRO del omni.ja, pese a que el configure
aceptó la variable sin una queja. Cincuenta minutos para descubrirlo, y no al construir sino al
intentar instalar la extensión de la página de inicio — a dos recetas de distancia y con un error
(`ERROR_SIGNEDSTATE_REQUIRED`) que no nombra la bandera por ningún lado.

LA CAUSA ES LA QUE ESTA MISMA RECETA YA DOCUMENTABA PARA OTRA VARIABLE: las fases son shells
distintos y `mach build` re-ejecuta el configure cuando lo cree necesario. Por eso `RUST_TARGET` se
repite en las tres desde siempre; `MOZ_REQUIRE_SIGNING` estaba sólo en `configure`.

Y como el fallo es de los que viajan lejos y callados, la fase install ahora lo COMPRUEBA donde de
verdad importa —el `AppConstants.sys.mjs` que va dentro de omni.ja— y falla el build si no quedó en
`false`, con el mensaje que dice qué revisar. Un flag que el configure acepta y el artefacto ignora
es justo la clase de cosa que sólo se ve si alguien la mide.

Lo que se leyó para llegar acá, y no se dedujo: el parser de mozbuild confirma que el valor VACÍO
desactiva (`if values == ("",): return NegativeOptionValue()`) y que un env vacío sí se considera
presente (`if env is not None`). O sea que el mecanismo era correcto; lo que faltaba era que la
variable estuviera donde el build la vuelve a leer.
2026-09-05 16:25:39 +00:00
Sergio 37afec380f firefox: MOZ_REQUIRE_SIGNING se desactiva con el valor VACÍO, no con cero
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.
2026-09-05 15:25:48 +00:00
Sergio 1dc662c48d atuq v0.3: página de inicio y pestaña nueva — y la firma de complementos venía COMPILADA
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).
2026-09-05 15:11:37 +00:00
Sergio a828e74c40 la cadena GTK3 se enlazaba estática dentro de dos .so: cuatro variantes -shared y xkeyboard-config al corpus
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.
2026-09-05 12:02:24 +00:00
Sergio 1728fd65e5 firefox NO reproduce bit a bit: el BuildID es la fecha del build — medido y anotado, no arreglado de paso
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.
2026-09-05 08:50:17 +00:00
Sergio 6dbe17f87d firefox: fuera clang18 y llvm18 — una dep del corpus tapaba el llvm-ar del lab
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
2026-09-05 03:51:39 +00:00
Sergio cb3ecd5a00 firefox pasa a clang+lld con LTO: la puerta de las optimizaciones costaba apk add lld, no una receta de LLVM
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).
2026-09-05 03:13:38 +00:00
Sergio 141eb9afd7 firefox: vaciar el checksum de los 5 crates vendorizados que tocan los parches
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.
2026-09-04 22:14:16 +00:00
Sergio c104a64462 firefox: RUST_TARGET — lo exige el parche de Alpine, no Firefox
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.
2026-09-04 22:04:23 +00:00
Sergio 3ffca7c7fe firefox: pasa a compiler=gcc — zig-cc choca con una negativa de upstream
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.
2026-09-04 21:59:52 +00:00
Sergio 61143203f9 firefox: wrappers de ar/nm/ranlib — zig los trae como subcomandos
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.
2026-09-04 21:55:45 +00:00
Sergio 2fd738bf82 firefox: quitar --enable-linker=lld — el sondeo de Mozilla no reconoce a zig
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.
2026-09-04 21:53:59 +00:00
Sergio 90364fabbf nodejs y firefox: strip_debug — medido, no supuesto
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.
2026-09-04 21:24:47 +00:00
Sergio 428be5e8d1 firefox 154.0 — la cabeza de la familia Gecko (receta, sin construir todavía)
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.
2026-09-04 21:02:51 +00:00