Files
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

507 lines
36 KiB
TOML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Firefox 154.0 — el navegador. Tercera app de usuario final, y **la cabeza de la familia Gecko**:
# Waterfox y Zen son forks suyos y reutilizan todo lo de abajo.
#
# ══ POR QUÉ 154 Y NO 155, QUE ES LA QUE SIGUE ZEN ══════════════════════════════════════════════
# Zen declara `"version": "155.0.1"` en su `surfer.json` y Waterfox 6.7 también va por el canal
# release, así que 155 parecía la base obvia. Se eligió **154** por una razón concreta: **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 que no conviene rehacer a mano — sin ellos 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 cuatro semanas, la paridad exacta con Zen es una cinta de correr: lo
# que de verdad se reutiliza entre los tres es LA PLATAFORMA (gtk3/nodejs/clang/cbindgen, todo ya en
# el corpus) y ESTE SET DE PARCHES. Subir a 155 después es un rebase, no un port.
#
# ══ TODO BUNDLEADO SALVO GTK3 ══════════════════════════════════════════════════════════════════
# Alpine usa `--with-system-{icu,nspr,nss,av1,libvpx,webp,libevent,...}`; de ésas el corpus tiene
# CERO. Firefox trae todas en el árbol, así que se dejan bundleadas: son menos piezas móviles para el
# primer build, que es exactamente cuando conviene minimizar variables. Cuando alguna de esas
# librerías tenga receta propia y valga compartirla, el flag se enciende y se re-mide.
#
# ══ SIN BRANDING OFICIAL, Y NO ES UN DESCUIDO ══════════════════════════════════════════════════
# Alpine pone `--enable-official-branding`. Acá NO: el binario lleva once parches, y usar el nombre y
# el logo de Firefox sobre un build modificado entra en la política de marcas de Mozilla — es
# literalmente la historia de Iceweasel en Debian. Con el branding `unofficial` el navegador es el
# mismo software y nadie tiene que pedir permiso a nadie. Es la misma cautela que la receta de
# `dejavu-fonts-nerd` ya aplicó con la licencia de Bitstream Vera: **una fuente modificada no puede
# llamarse como la original, y un navegador parcheado tampoco.**
#
# ══ WAYLAND-ONLY, HEREDADO DE GTK3 ═════════════════════════════════════════════════════════════
# `--enable-default-toolkit=cairo-gtk3-wayland`. Nuestro GTK3 se construyó con `-Dx11_backend=false`
# ⇒ este Firefox **no puede correr como cliente X11 ni bajo Xwayland**. Bajo Wayland nativo sí. Está
# escrito en `recipes/gtk3.toml` y se repite acá porque es lo primero que alguien va a preguntar.
#
# ══ HERMÉTICO: NADA DE RED DURANTE EL BUILD ════════════════════════════════════════════════════
# --disable-bootstrap su trabajo ES descargar toolchains. Prohibido.
# MACH_BUILD_PYTHON_NATIVE_PACKAGE_SOURCE sin esto `mach` arma un virtualenv con `pip` y sale a
# =system la red. Con `system` usa lo que ya está.
# MOZBUILD_STATE_PATH por defecto escribe en `$HOME`, que en el sandbox no es
# suyo; se apunta al árbol de build.
# Los crates de Rust vienen VENDORIZADOS en el tarball (`third_party/rust`) ⇒ cargo no baja nada.
#
# ══ LO DEMÁS QUE SE APAGA ══════════════════════════════════════════════════════════════════════
# --disable-jemalloc musl trae su propio allocator; jemalloc encima es la receta del
# cuelgue clásico de Firefox en Alpine.
# (YA NO --without-wasm- RLBox ENCENDIDO desde 2026-09-06 (SDD 26, unidad 3.b). El flag que
# sandboxed-libraries) estaba acá apagaba la jaula wasm que envuelve a los parsers de
# fuentes y medios —graphite, ogg, expat, woff2—, o sea justo el
# código que come entrada NO CONFIABLE de la red. Estaba apagado por
# una razón honesta y ya vencida: «el SDK de WASI no está en ninguna
# cola». Ahora sí está, y son cuatro recetas nuestras:
# wasi-libc-headers → wasi-compiler-rt → wasi-libc → wasi-sdk
# Se reemplaza por `--with-wasi-sysroot`, que es LITERALMENTE lo que
# pasa Alpine (verificado contra su mozconfig el 2026-09-06), a la
# misma ruta donde instala nuestro `wasi-libc`.
# --disable-crashreporter manda telemetría a Mozilla; además pide breakpad.
# --disable-updater la distro actualiza por takana, no por un updater propio.
# --disable-tests no entran al artefacto.
# (SIN --enable-linker) Se deja que Firefox detecte solo. Con `zig-cc` esto era
# OBLIGATORIO —zig se anuncia como `zig ld 0.16.0` y el sondeo de
# `toolchain.configure` clasifica por esa cadena buscando
# «mold»/«GNU ld»/«GNU gold»/«LLD»— y **pedir el linker por nombre
# vuelve FATAL cualquier fallo del sondeo**. Con gcc el linker sí se
# anuncia como «GNU ld», así que el flag ya no haría daño; se deja
# fuera igual porque no aporta nada y una perilla menos es una
# diferencia menos entre esta receta y las de Waterfox y Zen.
# ══ ⚠ DEUDA ANOTADA Y NO BARRIDA: EL LANZADOR PIERDE AV1 ═══════════════════════════════════════
# El lanzador que esta receta escribe en `/out/usr/bin/firefox` pone `/usr/lib/firefox` UNA sola vez
# en `LD_LIBRARY_PATH`, o sea primero — y **Gecko se come el primer elemento al lanzar el proceso
# RDD**, el que decodifica vídeo. Consecuencia medida en `atuq`, que tenía el lanzador idéntico: el
# RDD no encuentra el ffvpx bundleado (`FFVPX: Link result: NoProvidedLib`), se cae al ffmpeg del
# sistema, y **AV1 no reproduce** —el decodificador `av1` de ffmpeg es sólo hwaccel— sin un error,
# sin un NEEDED faltante y sin una cadena ausente. El arreglo es poner el appdir DOS VECES; está
# hecho y probado en `recipes/atuq/bin/atuq`, con guardián en `scripts/test-atuq-codecs.sh`.
#
# **Acá NO se aplica todavía, a propósito.** El lanzador vive dentro de la fase `install` ⇒ tocarlo
# mueve el `ArtifactHash` y obliga a reconstruir firefox entero (LTO+PGO, ~4 h en el worker, OOM en
# gioser) y con él `atuq`. Y ningún perfil declara `firefox`: el navegador que se shipea es `atuq`,
# que ya está arreglado, así que hoy el bug es LATENTE. Se paga en el próximo re-hash de esta receta
# por cualquier otro motivo — que es cuando cuesta cero. Si alguien shipea `firefox` antes de eso,
# se paga ahí mismo. Ver SDD 26 §6.11.
# ⚠ EL JARLOG SE PROBÓ, FUNCIONA, Y ESTÁ APARCADO CON LA EVIDENCIA MEDIDA (2026-09-08).
# Se generó (`MOZ_JAR_LOG_FILE` en el entorno; NO hace falta la extensión `Quitter` de Mozilla, que
# el SDD daba por bloqueante), entró en el build, y se verificó del lado del artefacto: el `libxul`
# quedó IDÉNTICO byte a byte y sólo cambió el `omni.ja`. O sea que la función existe.
#
# Pero **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: BadZipFile
#
# Y eso ROMPE `atuq`, que reempaqueta el `omni.ja` con `zipfile` para su branding:
# `zipfile.BadZipFile: Bad magic number for central directory` en `rebrand.py`.
#
# La cuenta es asimétrica y por eso se aparca: el COSTE está MEDIDO —el navegador propio de la
# distro no se puede construir— y el BENEFICIO no. El banco corre con caché de página caliente, que
# es donde reordenar el `omni.ja` no ahorra ninguna lectura, y dio 0,1 %: por debajo del suelo de
# ruido del propio banco (~1 % entre corridas de días distintos). Cambiar algo que funciona por una
# ganancia que no se pudo medir, rompiendo algo que sí funcionaba, es mal negocio.
#
# 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**,
# así que retomarlo no exige volver a perfilar: es cambiar una línea y re-pinear el sha256.
#
name = "firefox"
version = "154.0"
license = "MPL-2.0"
[source]
# Tarball de RELEASE de Mozilla: fichero SUBIDO por upstream, sha256 estable y publicado en su
# SHA256SUMS. No es un `/archive/<tag>` de forja, que se genera al vuelo (ver `recipes/mbedtls.toml`).
tarball = "https://ftp.mozilla.org/pub/firefox/releases/154.0/source/firefox-154.0.source.tar.xz"
sha256 = "36cec5b3688a60f78a6d20dcaee15b598f84e03c66f6587056aead6cb498b99a"
# Los once de musl del APKBUILD de Alpine (community/firefox). NO se traen los suyos de ppc64le,
# loongarch ni Android: no aplican a x86_64 y cada parche que no hace falta es una forma más de que
# un rebase falle sin motivo.
patches = [
"firefox-patches/lfs64.patch",
"firefox-patches/time64.patch",
"firefox-patches/musl-no-linux-prctl.patch",
"firefox-patches/fix-fortify-system-wrappers.patch",
"firefox-patches/fix-rust-target.patch",
"firefox-patches/sandbox-sched_setscheduler.patch",
"firefox-patches/abseil-cpp.patch",
"firefox-patches/glean-stub.patch",
"firefox-patches/rust-lto-thin.patch",
"firefox-patches/wasip1.patch",
"firefox-patches/widevine.patch",
]
[build]
# ══ `clang` — Y POR QUÉ YA NO ES `gcc` (2026-09-05) ════════════════════════════════════════════
# HISTORIA: esta receta nació con `compiler = "gcc"` porque con zig-cc el configure de Firefox murió
# tres veces seguidas. Los tres muros eran REALES y siguen siéndolo para zig:
#
# 1. «Could not use lld as linker» — zig se identifica como `zig ld 0.16.0` y el sondeo de Mozilla
# clasifica por esa cadena buscando «LLD»/«GNU ld»/«GNU gold».
# 2. «Cannot find ar» — zig trae `ar` como SUBCOMANDO y `check_prog` busca un ejecutable.
# 3. «Firefox does not support linking statically with libstdc++» — `flags.configure:79` compila un
# C++ mínimo, lo pasa por `llvm-objdump --private-headers` y exige un `NEEDED` de la stdlib de
# C++. zig enlaza libc++ ESTÁTICA para musl, así que ese NEEDED no existe nunca.
#
# gcc satisfacía los tres, pero al precio de quedar FUERA de la cadena de optimización de Gecko:
# **PGO, LTO y BOLT son cadena de clang en Firefox** (`--enable-lto=cross` quiere clang+lld,
# `--enable-profile-use=cross` espera `-fprofile-instr-use`). Con gcc no hay perilla que encender.
# Medido contra el APKBUILD de `community/firefox` de aports —nuestro propio upstream, de donde
# salen los once parches de musl— y contra el PKGBUILD de Arch: **las dos hacen LTO y PGO**. No
# estábamos adelante de nadie; estábamos atrás, y la puerta era el compilador.
#
# clang RESUELVE LOS TRES MUROS SIN PERDER NADA:
# 1. `--enable-linker=lld` + `lld` en el lab (añadido a `bootstrap-devfs.sh` el 2026-09-05).
# 2. `AR=llvm-ar` lo pone takana solo con `compiler="clang"` (takana-build/src/lib.rs), y la suite
# `llvm-*` está expuesta en `/usr/bin` por el paso 3a-ter del bootstrap.
# 3. clang++ de Alpine usa la `libstdc++` COMPARTIDA de gcc ⇒ el `NEEDED` existe. La prueba no es
# teórica: Alpine construye este mismo Firefox con clang22 y **sin** `libcxx` en sus makedepends.
#
# El lab ya traía `clang22`/`llvm22` 22.1.8 (la MISMA major que usa Alpine: `_llvmver=22`), así que
# esto no costó una receta de LLVM: costó `apk add lld`.
#
# ⚠ EL PRECIO, ESCRITO: el toolchain del lab SÍ entra en `hash_inputs` desde el 2026-08-10
# (`takana-core/src/lab.rs`), pero **`lld` no casa ningún prefijo de `TOOLCHAIN_PREFIXES`** ⇒ su
# versión NO está en la huella. Eso es lo que permitió añadirlo sin re-hashear los 837 artefactos
# sellados —medido: la huella no se movió—, y a la vez deja un agujero que sólo expone a las recetas
# `compiler="clang"`, que hoy es ésta. Cerrarlo cuesta un re-hasheo del corpus entero.
compiler = "clang"
target = "x86_64-linux-musl"
link = "dynamic"
flags = []
# ══ `strip_debug`: SIN ESTO EL ARTEFACTO SERÍA INMANEJABLE ═════════════════════════════════════
# Medido en el `nodejs` que acaba de sellar sin strip: su binario 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. Firefox es varias veces V8, así que sin strip su artefacto entraría en varios GB, y
# el store no es sólo disco: se sincroniza hub↔worker y se respalda.
#
# Es además 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.
#
# Entra en `hash_inputs` A PROPÓSITO (cambia el contenido del artefacto), y usa `zig objcopy`, que
# siempre está en el sandbox ⇒ no agrega una dep de build.
strip_debug = true
# ══ `MOZ_BUILD_DATE`: SIN ESTO EL ARTEFACTO NO REPRODUCE ══════════════════════════════════════
# El `BuildID` de `application.ini` es, por defecto, la FECHA Y HORA del build. Medido en el
# artefacto sellado del 2026-09-05: `BuildID=20260905035306`. Dos construcciones de esta misma
# receta, con el mismo ArtifactHash, producían bytes distintos — justo el agujero que el invariante
# de reproducibilidad existe para cerrar, y que `build-state.json` no puede ver porque el hash es
# input-addressed y no se mueve por esto.
#
# El valor sale de `SOURCE_DATE_EPOCH`, que el sandbox de takana YA fija a 1 para todas las recetas.
# Es mejor que una constante escrita a mano: usa el mecanismo de hermeticidad que ya existe, en vez
# de inventar un segundo. Alpine hace exactamente lo mismo en su APKBUILD.
[build.phases]
configure = '''
export MOZBUILD_STATE_PATH="$PWD/.mozbuild"
export MACH_BUILD_PYTHON_NATIVE_PACKAGE_SOURCE=system
export MOZ_NOSPAM=1
# ── RUST_TARGET: lo EXIGE el parche de Alpine, no Firefox ─────────────────────────────────────
# `fix-rust-target.patch` REEMPLAZA la autodetección del triple de Rust por un
# `os.environ['RUST_TARGET']` pelado. Sin la variable, configure muere con
# `KeyError: 'RUST_TARGET'` — que se lee como un bug de Firefox y es en realidad el contrato del
# parche: el APKBUILD de Alpine lo exporta como `$CTARGET` y acá hay que hacer lo propio.
# El valor NO se adivina: sale de `ls .dev-fs/alpine/usr/lib/rustlib/` en el lab, que es
# `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 produciría un cross-compile silencioso).
export RUST_TARGET=x86_64-alpine-linux-musl
# ── `MOZ_REQUIRE_SIGNING=0`: SIN ESTO LA DISTRO NO PUEDE SHIPEAR NI UNA EXTENSIÓN PROPIA ──────
# Firefox exige que todo XPI esté firmado por Mozilla. La pref `xpinstall.signatures.required` NO
# alcanza: se ignora cuando la exigencia viene COMPILADA, y viene compilada porque el default de
# `MOZ_REQUIRE_SIGNING` sale del MILESTONE (154.0 es release), no del canal de actualización — el
# nuestro es `default`, y aun así estaba activa.
#
# Medido, no deducido: con el autoconfig ejecutándose de verdad (testigo `atuq.autoconfig.ok=true`
# en el perfil) y `lockPref("xpinstall.signatures.required", false)` puesta, la instalación por
# política seguía muriendo con `ERROR_SIGNEDSTATE_REQUIRED`.
#
# ⚠ EL PRECIO, ESCRITO: este Firefox —y por herencia `atuq`— deja de exigir la firma de Mozilla
# para CUALQUIER complemento, no sólo los nuestros. La alternativa real es firmar en AMO, que pide
# cuenta y revisión de Mozilla por cada versión: exactamente la dependencia externa que esta distro
# existe para no tener. Es la misma postura que ya tomaba `--with-unsigned-addon-scopes`, llevada
# hasta donde de verdad hace falta.
# ⚠ SE DESACTIVA CON EL VALOR VACÍO, NO CON CERO. `MOZ_REQUIRE_SIGNING=0` muere con
# «InvalidOptionError: MOZ_REQUIRE_SIGNING takes 0 values», porque es una bandera booleana y el `0`
# es un VALOR que no acepta. La semántica está en el propio parser de mozbuild
# (`python/mozbuild/mozbuild/configure/options.py`), y no se dedujo: se leyó del tarball —
# if values == ("",): return NegativeOptionValue() ⇒ VAR= (vacío) DESACTIVA
# if values == ("1",): return PositiveOptionValue() ⇒ VAR=1 activa
# El `export` con valor vacío es 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».
export MOZ_REQUIRE_SIGNING=
export MOZ_BUILD_DATE="$(date -u -d "@${SOURCE_DATE_EPOCH:-1}" +%Y%m%d%H%M%S)"
# ── LOS PARCHES ROMPEN EL CHECKSUM DE LOS CRATES VENDORIZADOS ─────────────────────────────────
# `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
# ninguna 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.
#
# Son los CINCO crates que tocan los parches, no uno: el error los va destapando de a uno y buscarlos
# por prueba y error costaría cinco vueltas de configure.
for c in audio_thread_priority cc zeitstempel wgpu-hal alsa; do
f="third_party/rust/$c/.cargo-checksum.json"
[ -f "$f" ] || { echo "FALTA $f — ¿upstream movió el crate?" >&2; exit 1; }
sed -i 's/\("files":{\)[^}]*/\1/' "$f"
done
cat > .mozconfig <<'MOZ'
ac_add_options --prefix=/usr
ac_add_options --enable-application=browser
ac_add_options --enable-default-toolkit=cairo-gtk3-wayland
ac_add_options --enable-release
ac_add_options --enable-optimize
ac_add_options --enable-hardening
# ── LA CADENA DE OPTIMIZACIÓN (2026-09-05) ────────────────────────────────────────────────────
# Las tres primeras son lo que Alpine y Arch ya hacían y nosotros no. `--enable-linker=lld` además
# resuelve el muro 1 del sondeo de linker (ver el bloque [build]).
ac_add_options --enable-linker=lld
ac_add_options --enable-lto=cross
ac_add_options --enable-packed-relative-relocs
# ── LO QUE HABILITA A `atuq` (SDD 26) ─────────────────────────────────────────────────────────
# Sin esto un Firefox de release rechaza las extensiones que la distro deja en
# `distribution/extensions/`. Es flag de CONFIGURE, o sea que NO se puede resolver en el overlay del
# artefacto derivado: la capacidad de `atuq` de shipear sus propias extensiones (el `sct` v1) se
# decide acá, en la base. Alpine pasa exactamente este flag y por el mismo motivo.
ac_add_options --with-unsigned-addon-scopes=app,system
ac_add_options --with-branding=browser/branding/unofficial
ac_add_options --with-libclang-path=/usr/lib
ac_add_options --disable-bootstrap
ac_add_options --disable-jemalloc
ac_add_options --disable-crashreporter
ac_add_options --disable-updater
ac_add_options --disable-tests
ac_add_options --disable-debug
ac_add_options --disable-debug-symbols
ac_add_options --disable-strip
ac_add_options --disable-install-strip
ac_add_options --disable-cargo-incremental
ac_add_options --enable-profile-use=cross
ac_add_options --with-pgo-profile-path=/usr/share/firefox-pgo/merged.profdata
ac_add_options --with-wasi-sysroot=/usr/share/wasi-sysroot
ac_add_options --enable-alsa
ac_add_options --enable-pulseaudio
ac_add_options --enable-dbus
ac_add_options --enable-ffmpeg
mk_add_options MOZ_OBJDIR=@TOPSRCDIR@/objdir
MOZ
# ── POR QUÉ AQUÍ NO HAY PGO TODAVÍA ───────────────────────────────────────────────────────────
# PGO es la otra mitad de la ganancia (LTO solo rinde menos que LTO+PGO) y NO entra en esta pasada
# por dos muros que las distros no tienen, porque no persiguen lo que nosotros perseguimos:
# 1. El perfil se junta CORRIENDO el navegador. Alpine y Arch lo hacen bajo `xvfb-run`; nosotros
# NO tenemos X11 en el corpus (la distro es Wayland-only) ⇒ el camino es un sway headless.
# 2. El `profdata` NO es determinista (los contadores dependen del timing) ⇒ si se generara aquí,
# cada build sellaría bytes distintos. La salida es generarlo UNA vez, sellarlo como artefacto
# propio y consumirlo por hash desde `[deps]`.
# Es su propia unidad de trabajo (SDD 26 §3.ter). Cuando llegue, además trae el `jarlog`, que ordena
# el `omni.ja` para el arranque — y eso condiciona cómo `atuq` puede re-empacarlo.
#
# ── ThinLTO Y LA MEMORIA ──────────────────────────────────────────────────────────────────────
# El enlace ThinLTO abre un trabajo por hilo y cada uno carga bitcode: sin capar, el link es lo
# primero que muere por OOM en una máquina de 16 GiB. Se capa con la MISMA cuenta que `-j` y por la
# misma razón que ella no es un número fijo: un literal ataría el ArtifactHash a la RAM de quien
# escribió la receta (ver la nota de la fase compile).
# ⚠ Si algún día dos máquinas con distinto `nproc` sellan bytes distintos, el primer sospechoso es
# este flag — ThinLTO se diseñó determinista respecto al número de trabajos, pero es una promesa
# de upstream, no algo que hayamos medido acá.
gib=$(awk "/MemTotal/{printf \"%d\", \$2/1024/1024}" /proc/meminfo)
jl=$(( gib / 3 )); [ "$jl" -lt 1 ] && jl=1
n=$(nproc); [ "$jl" -gt "$n" ] && jl=$n
export LDFLAGS="${LDFLAGS:-} -Wl,--thinlto-jobs=$jl"
echo "ThinLTO con --thinlto-jobs=$jl (MemTotal ${gib} GiB, nproc $n)"
./mach configure
'''
# `mach build` respeta -jN; la cuenta es la misma que en nodejs.toml y por el mismo motivo: un número
# fijo ataría el ArtifactHash a la RAM de quien escribió la receta.
# ⚠ OJO con el techo: dentro del sandbox `/proc/meminfo` NO muestra la memoria del contenedor sino la
# del host (medido en el LXC: 186 GiB en vez de 16), así que en un contenedor esta cuenta se degrada
# a `nproc`. Es un tope, no una garantía.
compile = '''
export RUST_TARGET=x86_64-alpine-linux-musl # ver la nota en la fase configure
# ⚠ `MOZ_REQUIRE_SIGNING` TAMBIÉN ACÁ, Y NO ES REDUNDANCIA. Las fases son shells distintos y
# `mach build` re-ejecuta el configure cuando lo cree necesario: exportarla sólo en la fase
# `configure` produjo un artefacto con `MOZ_REQUIRE_SIGNING: true` pese a que el configure la
# aceptó — 50 minutos de build para descubrirlo al abrir el navegador. Es exactamente la razón por
# la que `RUST_TARGET` se repite en las tres fases.
export MOZ_REQUIRE_SIGNING=
export MOZ_BUILD_DATE="$(date -u -d "@${SOURCE_DATE_EPOCH:-1}" +%Y%m%d%H%M%S)" # ídem: ver configure
export MOZBUILD_STATE_PATH="$PWD/.mozbuild"
export MACH_BUILD_PYTHON_NATIVE_PACKAGE_SOURCE=system
export MOZ_NOSPAM=1
gib=$(awk "/MemTotal/{printf \"%d\", \$2/1024/1024}" /proc/meminfo)
j=$(( gib / 3 )); [ "$j" -lt 1 ] && j=1
n=$(nproc); [ "$j" -gt "$n" ] && j=$n
echo "mach build -j$j (MemTotal ${gib} GiB, nproc $n)"
./mach build -j"$j"
'''
install = '''
export RUST_TARGET=x86_64-alpine-linux-musl # ver la nota en la fase configure
export MOZ_REQUIRE_SIGNING= # ídem
export MOZ_BUILD_DATE="$(date -u -d "@${SOURCE_DATE_EPOCH:-1}" +%Y%m%d%H%M%S)" # ídem
export MOZBUILD_STATE_PATH="$PWD/.mozbuild"
export MACH_BUILD_PYTHON_NATIVE_PACKAGE_SOURCE=system
DESTDIR=/out ./mach install
# ── GUARDIÁN: QUE LA BANDERA HAYA LLEGADO AL ARTEFACTO, NO AL CONFIGURE ───────────────────────
# `MOZ_REQUIRE_SIGNING` es una bandera que el configure acepta sin ruido y que aun así puede acabar
# en `true` dentro del artefacto (pasó: la exportábamos sólo en `configure`). El síntoma aparece
# lejísimos —al intentar instalar una extensión de la distro, en otra receta, tras 50 minutos de
# build— y no menciona la bandera por ningún lado. Se comprueba donde de verdad importa: en el
# `AppConstants` que va DENTRO de omni.ja.
python3 - <<'PY'
import sys, zipfile
z = zipfile.ZipFile("/out/usr/lib/firefox/omni.ja")
n = [x for x in z.namelist() if x.endswith("AppConstants.sys.mjs")]
if not n:
sys.exit("guardián: no encontré AppConstants.sys.mjs en omni.ja")
t = z.read(n[0]).decode("utf-8", "replace")
if "MOZ_REQUIRE_SIGNING: false" not in t:
sys.exit("guardián: MOZ_REQUIRE_SIGNING NO quedó en false — la distro no podría "
"instalar ninguna extensión propia. Revisá que la variable se exporte en TODAS "
"las fases (son shells distintos y mach re-ejecuta configure).")
print("guardián: MOZ_REQUIRE_SIGNING = false en el artefacto")
PY
# ── GUARDIÁN: QUE EL BuildID NO SEA UNA FECHA ────────────────────────────────────────────────
# Mismo razonamiento que el de arriba, y el mismo modo de fallo: si `MOZ_BUILD_DATE` no llega, el
# BuildID vuelve a ser la hora del build y el artefacto deja de reproducir SIN QUE NADA LO DIGA —
# el ArtifactHash es input-addressed y no se mueve por esto, así que `build-state.json` seguiría en
# verde. Se comprueba contra el valor que `SOURCE_DATE_EPOCH=1` obliga.
esperado="$(date -u -d "@${SOURCE_DATE_EPOCH:-1}" +%Y%m%d%H%M%S)"
real="$(sed -n 's/^BuildID=//p' /out/usr/lib/firefox/application.ini)"
if [ "$real" != "$esperado" ]; then
echo "guardián: BuildID=$real y esperaba $esperado — el artefacto NO reproduce." >&2
echo " Revisá que MOZ_BUILD_DATE se exporte en TODAS las fases." >&2
exit 1
fi
echo "guardián: BuildID = $real (determinista, de SOURCE_DATE_EPOCH)"
# ── GUARDIÁN: QUE RLBOX ESTÉ EN EL BINARIO, NO SÓLO EN EL CONFIGURE ───────────────────────────
# Tercera repetición de la misma lección, y por eso nace junto con la bandera en vez de 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 en ESTA receta (`MOZ_REQUIRE_SIGNING` y
# `MOZ_BUILD_DATE`), y acá el fallo sería el más caro de todos: un firefox que dice tener
# enjaulados los parsers de fuentes y medios —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.
#
# QUÉ SE MIRA. RLBox compila esas librerías a wasm y las traduce a C con wasm2c, que deja su
# rastro en los símbolos de `libxul.so`. El discriminante está MEDIDO, no supuesto: sobre el
# artefacto `b3:352d7880` (el último con RLBox APAGADO, 224 MB de libxul) los tres patrones dan
# CERO, así que cualquiera de ellos apareciendo significa que la jaula entró de verdad.
# 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.
X=/out/usr/lib/firefox/libxul.so
test -f "$X" || { echo "!! no hay libxul.so en el artefacto" >&2; exit 1; }
n_w2c=$(llvm-nm --defined-only "$X" 2>/dev/null | grep -c " w2c_" || true)
n_rlb=$(llvm-nm "$X" 2>/dev/null | grep -ci rlbox || true)
n_w2n=$(llvm-nm "$X" 2>/dev/null | grep -ci wasm2c || true)
if [ $((n_w2c + n_rlb + n_w2n)) -eq 0 ]; then
echo "!! RLBOX NO LLEGÓ AL BINARIO: libxul.so no tiene NI UN símbolo w2c_/rlbox/wasm2c." >&2
echo " El configure aceptó --with-wasi-sysroot pero el sandbox wasm no se construyó." >&2
echo " Mirá si las deps wasi-libc / wasi-compiler-rt / wasi-sdk llegaron al sandbox y si" >&2
echo " /usr/share/wasi-sysroot/lib/wasm32-wasip1/libc.a existe durante el build." >&2
exit 1
fi
# ── EL LANZADOR: SIN ESTO EL ARTEFACTO NO ARRANCA ────────────────────────────────────────────
# Medido el 2026-09-07 sobre el artefacto SELLADO: `firefox --version` moría con
#
# Error loading shared library libnspr4.so: No such file or directory
# Couldn't load XPCOM.
#
# `mach install` deja `/usr/bin/firefox` como un symlink pelado al binario, el binario NO trae
# `RUNPATH`, y el cierre no publica `/etc/ld-musl-x86_64.path`. Así que las librerías que viven
# JUNTO al binario en `/usr/lib/firefox` no las encuentra nadie. `atuq` funcionaba 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 la familia del 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, porque las librerías SÍ están en el
# artefacto y las cuenta como propias.
#
# El `LD_LIBRARY_PATH` es además lo que necesitan los procesos HIJOS: firefox lanza uno por pestaña.
rm -f /out/usr/bin/firefox
cat > /out/usr/bin/firefox <<'LANZA'
#!/bin/sh
LD_LIBRARY_PATH="/usr/lib/firefox${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
export LD_LIBRARY_PATH
exec /usr/lib/firefox/firefox "$@"
LANZA
chmod 755 /out/usr/bin/firefox
test -x /out/usr/bin/firefox || { echo "!! no se creó el lanzador" >&2; exit 1; }
echo "guardián: lanzador puesto (sin él el artefacto no arranca fuera del lab)"
echo "guardián: RLBox en el binario (w2c_=$n_w2c rlbox=$n_rlb wasm2c=$n_w2n; con RLBox apagado los tres son 0)"
'''
[deps]
# ⚠ DEP DE EJECUCIÓN, NO DE BUILD — Y NO ES DECORACIÓN (2026-09-06).
# Este artefacto declara `NEEDED libstdc++.so.6` / `libgcc_s.so.1`, y hasta hoy NADIE en el corpus
# las proveía: sólo existían en `.dev-fs/alpine`, o sea en el LAB. El binario sellaba, REPRODUCÍA
# bit a bit, pasaba todos los guardianes… y no arrancaba fuera del lab. Se descubrió corriendo
# `atuq` bajo un sway headless, no leyendo el grafo.
# `runtime` NO entra en el ArtifactHash (igual que `license`), así que declararlo NO re-hashea:
# lo que cambia es la CLAUSURA, que es lo que se hidrata en la imagen. Lo vigila
# `scripts/vigia-sonames.py`, que corre en el latido y deja `docs/state/sonames.txt`.
runtime = ["gcc-libs"]
build = [
"python3", "pkgconf", "nasm", "make", "cmake",
# la plataforma Gecko, compartida con los forks (SDD 26)
#
# ⚠ NI `clang18` NI `llvm18` — Y NO ES UN OLVIDO (2026-09-05) ─────────────────────────────────
# Estaban aquí cuando esta receta compilaba con gcc: `clang18` aportaba el `libclang.so` que
# bindgen carga en runtime y `llvm18` 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 `/usr/bin/llvm-ar` pasaba a ser el de LLVM 18
# mientras el compilador era clang 22. Con ThinLTO encendido los `.o` ya no son objetos sino
# BITCODE, y el bitcode no es compatible hacia atrás:
#
# /usr/bin/llvm-ar: error: …log.o: 'Unknown attribute kind (102)'
# (Producer: 'LLVM22.1.8' Reader: 'LLVM 18.1.8')
#
# El error nombra las DOS versiones, que es lo que lo hace diagnosticable de un vistazo. Es la
# misma forma que la lección de los headers UAPI: una dep tapa al lab y el fallo aparece lejos.
# El lab ya trae todo lo necesario en la versión correcta —`clang22-libclang` deja
# `/usr/lib/libclang.so` y el paso 3a-ter del bootstrap expone la suite `llvm-*` de llvm22 en
# `/usr/bin`—, así que la respuesta no es apuntar el AR a mano: es NO traer el 18.
# Es además lo que hace Alpine: clang22 + clang22-libclang, sin un segundo LLVM en la mesa.
"nodejs", "cbindgen",
# Las mismas variantes `-shared` que usa gtk3 (2026-09-05). No es simetría: declarar acá las
# ESTÁTICAS junto a un gtk3 que trae las compartidas son dos artefactos peleando por el mismo
# `lib/pkgconfig/pango.pc` y las mismas cabeceras, que es la otra forma conocida de este mismo
# fallo. Van las dos listas al mismo sitio o ninguna.
"gtk3", "atk", "gdk-pixbuf-shared", "pango-shared", "cairo-shared", "libepoxy",
"glib-shared", "pcre2-shared", "libffi-shared", "zlib-shared",
"harfbuzz-shared", "fribidi", "freetype-shared", "fontconfig-shared", "pixman",
"libpng-shared", "libjpeg-turbo-shared",
"wayland", "wayland-protocols", "libxkbcommon", "mesa", "libdrm",
"dbus", "pipewire", "pulseaudio", "alsa-lib",
"libxml2-shared", "linux-headers",
# ── LA CADENA WASM DE RLBOX (SDD 26 §3.b) ──────────────────────────────────────────────────────
# Van las TRES, y no es redundancia: las deps de takana NO son transitivas. En el `bwrap` sólo se
# apilan como overlay las que están escritas acá; declarar sólo `wasi-sdk` NO arrastra al
# `wasi-libc` que necesita, y el fallo sería el peor de los posibles — el sysroot no existiría y
# el error saldría a las horas, hablando de un header de wasm que no nombra a ninguna receta.
#
# wasi-libc el sysroot en sí (`/usr/share/wasi-sysroot`), que es lo que apunta el flag
# wasi-compiler-rt los builtins (`libclang_rt.builtins-wasm32.a`) SIN LOS CUALES NO ENLAZA
# nada a wasm; aterrizan en el resource dir del clang del lab
# wasi-sdk el `.cfg` de `/etc/clang22`, que hace el sysroot invisible para quien no
# pasa el flag. Con `--with-wasi-sysroot` explícito es cinturón Y tirantes:
# se declara igual porque es lo que hace Alpine y porque cuesta 28 K.
# wasi-libcxx libc++ y libc++abi para wasm32. NO es opcional: las librerías que RLBox
# enjaula no son todas C (graphite2 es C++) y sin esto el configure muere a
# los 6 segundos con «'cstring' file not found», culpando a los headers de
# WASI, que están perfectos. Es el hueco que este build encontró.
"wasi-libc", "wasi-libcxx", "wasi-compiler-rt", "wasi-sdk",
# El PERFIL de ejecución (SDD 26 §3.a). Es una medición congelada y pineada por sha256, no algo
# que se recalcule: ver la cabecera de `recipes/firefox-pgo-profile.toml` para por qué un perfil
# generado en cada build rompería la reproducibilidad en vez de darla.
#
# ⚠ 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. Ese fichero lo emite el
# `profileserver.py` de Mozilla usando su extensión `Quitter`, que nuestra corrida headless no
# tiene. O sea que hoy se gana la disposición de CÓDIGO y no el orden del `omni.ja`. Es su propia
# unidad de trabajo y está escrito para que nadie lo lea como «PGO completo».
"firefox-pgo-profile",
]