# 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/` 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", ]