Files
takana/recipes/firefox.toml
T
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

266 lines
18 KiB
TOML

# 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.
# --without-wasm-sandboxed- exige el SDK de WASI, que no está en ninguna cola. Apaga el
# libraries sandbox wasm de algunas librerías de medios, no el sandbox del
# proceso de contenido.
# --disable-crashreporter manda telemetría a Mozilla; además pide breakpad.
# --disable-updater la distro actualiza por hammer, 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.
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 hammer solo con `compiler="clang"` (hammer-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
# (`hammer-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
[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
# ── 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 --without-wasm-sandboxed-libraries
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
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 MOZBUILD_STATE_PATH="$PWD/.mozbuild"
export MACH_BUILD_PYTHON_NATIVE_PACKAGE_SOURCE=system
DESTDIR=/out ./mach install
'''
[deps]
build = [
"python3", "pkgconf", "nasm", "make", "cmake",
# la plataforma Gecko, compartida con Waterfox y Zen
"nodejs", "cbindgen", "clang18", "llvm18",
"gtk3", "atk", "gdk-pixbuf", "pango", "cairo", "libepoxy",
"glib-shared", "pcre2-shared", "libffi-shared", "zlib-shared",
"harfbuzz", "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",
]