firefox: BuildID determinista — se cierra la deuda de reproducibilidad, con guardián
Medido y probado hoy: `atuq` REPRODUCE bit a bit (`why-differs`: 85 entradas idénticas, 0 divergen), lo que valida el re-empaque determinista del omni.ja, el XPI, los iconos copiados y el BuildID derivado del contenido. El que NO reproducía era la BASE: el `BuildID` de firefox era la fecha y hora del build (`20260905035306` en el artefacto sellado), así que dos construcciones con el mismo ArtifactHash daban bytes distintos. Es el agujero exacto que el invariante de reproducibilidad existe para cerrar, y `build-state.json` no lo puede ver: el hash es input-addressed y no se mueve por esto. Verde y mintiendo. El valor sale de `SOURCE_DATE_EPOCH`, que el sandbox de hammer YA fija a 1 para todas las recetas — mejor que una constante escrita a mano, porque usa el mecanismo de hermeticidad que ya existe en vez de inventar un segundo. Alpine hace lo mismo en su APKBUILD. BuildID esperado: 19700101000001. Va en LAS TRES FASES, por la lección que costó 50 minutos esta misma tarde con MOZ_REQUIRE_SIGNING: son shells distintos y `mach build` re-ejecuta el configure cuando le parece. Y con su GUARDIÁN, hermano del de la firma: la fase install compara el BuildID del artefacto contra el que SOURCE_DATE_EPOCH obliga, y falla el build si no coinciden. Los dos fallos de esta familia —flag que el configure acepta y el artefacto ignora— son mudos y viajan lejos; la única defensa es comprobarlos donde importa, que es dentro del artefacto.
Semántico sayk'uy
Mana ima sutichaypas t'ikrakurqanchu: qillqalla, yachachiylla utaq willaylla t'ikrakurqan.
1 qillqakuna · +27 −10
- recipes/firefox.toml +27 −10
diff --git a/recipes/firefox.toml b/recipes/firefox.toml
index a46d7e04..4d16baf5 100644
--- a/recipes/firefox.toml
+++ b/recipes/firefox.toml
@@ -133,16 +133,16 @@ flags = []
# 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
-# ⚠ DEUDA MEDIDA EL 2026-09-05: ESTE ARTEFACTO NO REPRODUCE BIT A BIT ═══════════════════════════
-# `application.ini` del artefacto sellado trae `BuildID=20260905035306`: la FECHA Y HORA del build.
-# Dos construcciones de esta misma receta, con el mismo hash, producen bytes distintos — que es
-# exactamente el agujero que el invariante de reproducibilidad existe para cerrar, y no se ve en
-# `build-state.json` porque el ArtifactHash es input-addressed y no se mueve.
-# El arreglo es el que ya usa Alpine: exportar `MOZ_BUILD_DATE` con un valor determinista en las
-# fases (su APKBUILD lo deriva de `SOURCE_DATE_EPOCH`; acá el candidato natural es un valor fijo,
-# que al vivir en el texto de la fase entra en `hash_inputs` y queda declarado).
-# NO se aplica de paso: cambiar una fase re-hashea la receta y cuesta otro build de cuatro horas
-# más el re-sellado de `atuq`, que cuelga de ella. Es su propia unidad de trabajo.
+# ══ `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 = '''
@@ -183,6 +183,7 @@ export RUST_TARGET=x86_64-alpine-linux-musl
# 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
@@ -275,6 +276,7 @@ export RUST_TARGET=x86_64-alpine-linux-musl # ver la nota en la fase configure
# 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
@@ -287,6 +289,7 @@ echo "mach build -j$j (MemTotal ${gib} GiB, nproc $n)"
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
@@ -310,6 +313,20 @@ if "MOZ_REQUIRE_SIGNING: false" not in t:
"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)"
'''
[deps]