From 3171bc66994e21b5433a4dbd364a4cdf12e447cc Mon Sep 17 00:00:00 2001 From: Sergio Date: Mon, 7 Sep 2026 00:32:10 +0000 Subject: [PATCH] =?UTF-8?q?recetas:=20apuntar=20al=20vig=C3=ADa=20que=20EX?= =?UTF-8?q?ISTE,=20no=20al=20que=20borr=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- recipes/firefox-instrumentado.toml | 406 +++++++++++++++++++++++ recipes/firefox.toml | 2 +- recipes/gcc-libs.toml | 2 +- recipes/incoming-gnome/librsvg.toml | 2 +- recipes/incoming-gnome/spidermonkey.toml | 2 +- recipes/mesa-llvmpipe.toml | 2 +- recipes/waterfox.toml | 2 +- 7 files changed, 412 insertions(+), 6 deletions(-) create mode 100644 recipes/firefox-instrumentado.toml diff --git a/recipes/firefox-instrumentado.toml b/recipes/firefox-instrumentado.toml new file mode 100644 index 00000000..65e4da6e --- /dev/null +++ b/recipes/firefox-instrumentado.toml @@ -0,0 +1,406 @@ +# firefox-instrumentado — el firefox de al lado, pero COMPILADO PARA MEDIRSE. Andamiaje del PGO +# (SDD 26, unidad 3.a): existe sólo para correrlo una vez y quedarse con el perfil que emite. +# +# ══ QUÉ ES Y QUÉ NO ════════════════════════════════════════════════════════════════════════════ +# Es `recipes/firefox.toml` con UNA opción de más, `--enable-profile-generate=cross`, que hace que +# clang instrumente cada rama. El binario resultante es LENTO y NO se distribuye: se ejecuta una +# vez bajo un sway headless, escupe `.profraw`, y de ahí sale el `merged.profdata` que el firefox +# de verdad consume con `--enable-profile-use=cross`. +# +# ⚠ ES UNA COPIA DE `firefox.toml` Y ESO TIENE UN PRECIO CONOCIDO. Dos ficheros que describen el +# mismo build divergen sin que nada lo diga, y un perfil recogido de un binario que ya no se parece +# al que se optimiza vale menos que ninguno. **Si se toca `firefox.toml` —deps, parches, mozconfig— +# hay que tocar esto.** Se generó derivándolo del original, no escribiéndolo a mano, justamente para +# que naciera igual; mantenerlo igual es responsabilidad de quien edite. +# +# ⚠ POR QUÉ EL PERFIL NO SE GENERA DENTRO DE HAMMER. Correr el navegador exige un compositor, y +# meter sway en el sandbox pediría ~11 librerías más sobre las ~35 de firefox — cerca del límite de +# 4 KB del `lowerdir=` de bwrap que ya tumbó a kio con 52 deps. Pero el argumento de fondo no es el +# coste: **el perfil NO ES DETERMINISTA** (los contadores dependen del timing), así que meterlo en un +# build hermético no compra reproducibilidad, sólo la finge. El paso no determinista vive FUERA y su +# salida entra como fuente pineada, que es exactamente lo que el ADR 0013 hace con las fuentes. + +name = "firefox-instrumentado" +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 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 +# ══ `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 hammer 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-profile-generate=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 --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 LA INSTRUMENTACIÓN HAYA LLEGADO AL BINARIO ───────────────────────────────── +# Misma lección que RLBox: `--enable-profile-generate=cross` es una opción que el configure acepta +# sin ruido, y un binario SIN instrumentar corre perfecto y no emite ni un `.profraw`. El fallo se +# descubriría al final de la corrida de perfilado, con las manos vacías y sin nada que lo explique. +# clang deja símbolos `__llvm_profile_*` en todo binario instrumentado; sin instrumentar son CERO. +X=/out/usr/lib/firefox/libxul.so +n=$(llvm-nm --defined-only "$X" 2>/dev/null | grep -c " __llvm_profile_") +test "$n" -gt 0 || { + echo "!! el binario NO está instrumentado: 0 símbolos __llvm_profile_* en libxul.so" >&2 + echo " el configure aceptó --enable-profile-generate=cross y no hizo nada." >&2 + exit 1 +} +echo "guardián: instrumentado ($n símbolos __llvm_profile_*; sin instrumentar serían 0)" + +# ── 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 +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 hammer 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", + # ⚠ SÓLO EL INSTRUMENTADO LA NECESITA. `-fprofile-generate` enlaza contra la runtime de perfilado + # de compiler-rt —la que escribe los `.profraw`— y el LAB NO TRAE NINGUNA runtime de compiler-rt: + # su `lib/` del resource dir ni existe. Sin esto el build muere en el minuto 38 con + # `ld.lld: cannot open .../libclang_rt.profile.a`. El firefox de producción usa + # `-fprofile-use`, que NO necesita la runtime, así que esta dep no va allá. + "compiler-rt-profile", +] diff --git a/recipes/firefox.toml b/recipes/firefox.toml index 9d8b213b..f0646b1e 100644 --- a/recipes/firefox.toml +++ b/recipes/firefox.toml @@ -373,7 +373,7 @@ echo "guardián: RLBox en el binario (w2c_=$n_w2c rlbox=$n_rlb wasm2c=$n_w2n; co # `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/audit-needed.sh`. +# `scripts/vigia-sonames.py`, que corre en el latido y deja `docs/state/sonames.txt`. runtime = ["gcc-libs"] build = [ "python3", "pkgconf", "nasm", "make", "cmake", diff --git a/recipes/gcc-libs.toml b/recipes/gcc-libs.toml index 6a07f181..b544a402 100644 --- a/recipes/gcc-libs.toml +++ b/recipes/gcc-libs.toml @@ -8,7 +8,7 @@ # NEEDED libstdc++.so.6 NEEDED libgcc_s.so.1 NEEDED libc.so # # y las dos primeras no existían en el store ni en ningún cierre: sólo en `.dev-fs/alpine`, o sea en -# el LAB. `scripts/audit-needed.sh` midió el alcance: 6 recetas colgantes —`firefox`, `atuq`, +# el LAB. `scripts/vigia-sonames.py` —que ya existía y nadie corría— medía el alcance: 6 recetas colgantes —`firefox`, `atuq`, # `waterfox`, `librsvg`, `spidermonkey`, `mesa-llvmpipe`— y SÓLO ESTOS DOS sonames faltando en todo # el corpus. Como `firefox` y `atuq` son raíces de los CUATRO perfiles de escritorio, las cuatro # imágenes llevaban un navegador que no podía arrancar. diff --git a/recipes/incoming-gnome/librsvg.toml b/recipes/incoming-gnome/librsvg.toml index 982ab9d9..581ad29c 100644 --- a/recipes/incoming-gnome/librsvg.toml +++ b/recipes/incoming-gnome/librsvg.toml @@ -83,6 +83,6 @@ install = "CARGO_NET_OFFLINE=true DESTDIR=/out make install" # `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/audit-needed.sh`. +# `scripts/vigia-sonames.py`, que corre en el latido y deja `docs/state/sonames.txt`. runtime = ["gcc-libs"] build = ["make", "pkgconf", "python3", "py3-setuptools", "gobject-introspection", "gi-foreign-girs", "glib", "glib-introspected", "gdk-pixbuf", "cairo-shared", "pango", "harfbuzz", "fribidi", "freetype-shared", "fontconfig-shared", "libxml2-shared", "libpng-shared", "pixman", "expat", "libffi", "pcre2", "zlib-shared"] diff --git a/recipes/incoming-gnome/spidermonkey.toml b/recipes/incoming-gnome/spidermonkey.toml index 420a00da..031943c6 100644 --- a/recipes/incoming-gnome/spidermonkey.toml +++ b/recipes/incoming-gnome/spidermonkey.toml @@ -52,6 +52,6 @@ install = "export MOZBUILD_STATE_PATH=/src/.mozbuild MOZ_NOSPAM=1 && cd js/src # `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/audit-needed.sh`. +# `scripts/vigia-sonames.py`, que corre en el latido y deja `docs/state/sonames.txt`. runtime = ["gcc-libs"] build = ["nspr", "nasm", "cbindgen", "python3", "make", "zlib", "pkgconf"] diff --git a/recipes/mesa-llvmpipe.toml b/recipes/mesa-llvmpipe.toml index a9ae5d2f..bf485b44 100644 --- a/recipes/mesa-llvmpipe.toml +++ b/recipes/mesa-llvmpipe.toml @@ -34,7 +34,7 @@ install = "DESTDIR=/out ninja -C output install" # `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/audit-needed.sh`. +# `scripts/vigia-sonames.py`, que corre en el latido y deja `docs/state/sonames.txt`. runtime = ["gcc-libs"] # llvm18: el libLLVM hammer-built (soberano) que llvmpipe JIT-linkea. hammer lo materializa en el # sandbox en /usr ⇒ /usr/bin/llvm-config es el de LLVM 18 (no el LLVM 22 de Alpine, que mesa 24.0.9 diff --git a/recipes/waterfox.toml b/recipes/waterfox.toml index bd51b52c..1a41b9fe 100644 --- a/recipes/waterfox.toml +++ b/recipes/waterfox.toml @@ -263,7 +263,7 @@ echo "guardián: BuildID = $real (determinista, de SOURCE_DATE_EPOCH)" # `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/audit-needed.sh`. +# `scripts/vigia-sonames.py`, que corre en el latido y deja `docs/state/sonames.txt`. runtime = ["gcc-libs"] build = [ "python3", "pkgconf", "nasm", "make", "cmake",