etapa 2: el split de debug FUNCIONA — y el piloto destruyó los artefactos antes de lograrlo
RESULTADO, con los tres criterios medidos a la vez sobre las dos recetas que divergían: bison 6M → 3M · 0 ficheros vacíos · 0 secciones .debug_ · «bison (GNU Bison) 3.8.2» appstream 56M → 18M · 0 ficheros vacíos · 0 secciones .debug_ · «AppStream version: 1.0.5» y las DOS pasan de DIVERGIR a REPRODUCIR. O sea que un solo cambio recupera espacio Y cierra la fuga de reproducibilidad, como predijo el §1.bis. Pero se llegó ahí después de tres errores que conviene dejar escritos. 🧨 1. `zig objcopy --strip-debug X X` (mismo fichero de entrada y salida) TRUNCA EL FICHERO A 0 BYTES. Destruyó los artefactos del piloto — y lo grave es que LOS TRES INDICADORES DECÍAN QUE IBA BIEN: el tamaño cayó 84% (porque los ficheros quedaron vacíos), `why-differs` dijo REPRODUCE (porque dos árboles vacíos son idénticos) y no quedaban secciones .debug_ (porque no quedaba ninguna sección). Se cazó al EJECUTAR el binario: 0 bytes. ⇒ La verificación de un artefacto tiene que incluir que SIGA FUNCIONANDO, no sólo que pese menos y reproduzca. Un artefacto vacío cumple las dos y no sirve para nada. Es la lección de esta campaña aplicada a la campaña misma: una métrica que parece éxito. 2. Al arreglarlo con fichero temporal, el strip pasó a ser un NO-OP SILENCIOSO: los binarios quedaban intactos y el tamaño no bajaba, porque no se pudo confirmar que `zig objcopy` acepte `--strip-debug`. Cambiado al `strip` de binutils, que sí funciona, a costa de declarar la dep. ⇒ Preferible una dep explícita que funciona a una comodidad que no se sabe si hace algo. 3. Con el strip real, apareció una fuga NUEVA: los artefactos seguían divergiendo, ahora por la CABECERA `ar` de los `.a` — `strip` los reescribe con los timestamps de cada corrida. Lo nombró `why-differs` exacto («archivar en modo determinista»). Arreglado con `strip -D` (= --enable-deterministic-archives). ⇒ Arreglar media causa deja el invariante igual de roto: el debug ya no divergía y el archivo sí. DISEÑO: `strip_debug` es un campo de la receta que ENTRA en `hash_inputs` y sólo si está fijado (mismo patrón que `zig_version`). Las dos mitades importan y están clavadas en un test: si no entrara, el lab cambiaría el contenido del artefacto sin mover el hash y el store MENTIRÍA; y al entrar sólo si está fijado, se despliega receta a receta sin re-hashear las 1161 de golpe — verificado: con el campo añadido al código, los 1161 hashes existentes NO se movieron. Va como paso del lab y no en la fase install de cada receta porque 383 de las 1161 no tienen install explícita: meterlo receta a receta obligaría a escribir a mano ese install por defecto en las 383, con riesgo de no clavarlo exacto. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -343,6 +343,51 @@ pub fn build(
|
||||
));
|
||||
}
|
||||
|
||||
// ── SEPARAR LA INFO DE DEPURACIÓN (SDD 23) ──────────────────────────────────────────────────
|
||||
// Corre DESPUÉS del install, sobre `/out`, y sólo si la receta lo pide con `strip_debug = true`.
|
||||
//
|
||||
// POR QUÉ ACÁ Y NO EN LA FASE `install` DE CADA RECETA: de las 1161 recetas, **383 no tienen
|
||||
// fase install explícita** — usan la heurística por defecto. Meterlo receta a receta obligaría a
|
||||
// escribir a mano ese install por defecto en las 383, con el riesgo de no clavarlo exacto y
|
||||
// cambiar el comportamiento sin querer. El lab lo hace una vez, igual para todas.
|
||||
//
|
||||
// ⚠ ESTO CAMBIA EL CONTENIDO DEL ARTEFACTO, así que el campo que lo activa **entra en
|
||||
// `hash_inputs`** (ver la doc de `Build::strip_debug`). Si no entrara, el lab podría cambiar lo
|
||||
// que sale del build sin mover el hash y el store MENTIRÍA.
|
||||
//
|
||||
// `zig objcopy` en vez del `strip` de binutils: zig SIEMPRE está en el sandbox (`/opt/zig`), así
|
||||
// que activar esto no obliga a añadir una dep de build a cada receta.
|
||||
//
|
||||
// El `|| true` por fichero es deliberado: `/out` tiene scripts, datos y cabeceras además de ELF,
|
||||
// y objcopy falla en ellos. Fallar el build por no poder strippear un `.h` sería absurdo; lo que
|
||||
// importa es que los ELF queden sin `.debug_*`.
|
||||
if recipe.build.strip_debug == Some(true) {
|
||||
// ⚠ A FICHERO TEMPORAL Y LUEGO `mv` — NUNCA in-place. `zig objcopy --strip-debug X X`
|
||||
// (mismo fichero como entrada y salida) **TRUNCA EL FICHERO A 0 BYTES**. El 2026-08-08 eso
|
||||
// destruyó los artefactos del piloto, y lo peor es que los TRES indicadores decían que iba
|
||||
// bien: el tamaño cayó un 84% (porque los ficheros quedaron vacíos), `why-differs` dijo
|
||||
// REPRODUCE (porque dos árboles vacíos son idénticos) y no quedaban secciones `.debug_`
|
||||
// (porque no quedaba ninguna sección). Se cazó al ejecutar el binario: 0 bytes.
|
||||
// ⇒ La verificación de un artefacto tiene que incluir que SIGA FUNCIONANDO, no sólo que
|
||||
// pese menos y reproduzca. Un artefacto vacío cumple las dos y no sirve para nada.
|
||||
// Herramienta: el `strip` de **binutils**, no `zig objcopy`. Se intentó con zig porque está
|
||||
// siempre en el sandbox y no obligaría a declarar una dep — pero no se pudo confirmar que su
|
||||
// `objcopy` acepte `--strip-debug`, y el resultado fue un no-op silencioso: los binarios
|
||||
// quedaron intactos y el tamaño no bajó. Preferible una dep explícita que funciona a una
|
||||
// comodidad que no se sabe si hace algo.
|
||||
// ⇒ Las recetas con `strip_debug = true` deben declarar `binutils` en `[deps].build`.
|
||||
//
|
||||
// ⚠ `-D` (= `--enable-deterministic-archives`) NO ES OPCIONAL. Sin él, `strip` reescribe los
|
||||
// `.a` con los timestamps/uid/gid reales de cada corrida ⇒ el artefacto sigue divergiendo,
|
||||
// sólo que ahora por la CABECERA `ar` en vez de por `.debug_*`. Medido en el piloto: quitar
|
||||
// el debug arregló una fuga de no-determinismo y abrió otra, y `why-differs` la nombró
|
||||
// exacto («ar-cabecera → archivar en modo determinista»). Arreglar media causa deja el
|
||||
// invariante igual de roto.
|
||||
let cmd = r#"find /out -type f \( -perm -u+x -o -name '*.so' -o -name '*.so.*' -o -name '*.a' \) -exec sh -c 'strip -D --strip-debug "$1" 2>/dev/null || true' _ {} \;"#;
|
||||
tracing::info!("phase: strip-debug (separando .debug_* del artefacto)");
|
||||
sb.run(cmd)?;
|
||||
}
|
||||
|
||||
// Sidecar de provenance: escribimos la receta serializada DENTRO de out_dir antes del
|
||||
// seal, para que quede congelada en el árbol read-only del store. `hammer export` la
|
||||
// lee de vuelta para emitir `source_patch` con la receta original en vez de un
|
||||
|
||||
@@ -152,6 +152,32 @@ pub struct Build {
|
||||
/// herramientas, no inputs del of_tree del 4/4). `None` ⇒ el zig por defecto. Ver SDD 11 §7.2b.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub zig_version: Option<String>,
|
||||
/// **Separa la información de depuración del artefacto** (SDD 23). Con `true`, el lab pasa un
|
||||
/// `zig objcopy --strip-debug` sobre los ELF de `/out` DESPUÉS de la fase install.
|
||||
///
|
||||
/// POR QUÉ IMPORTA, medido: el **79%** del contenido binario del store son secciones `.debug_*`
|
||||
/// ⇒ ~96 G de reserva de disco y un espejo público de ~30 G en vez de ~126 G. Y además **cierra
|
||||
/// una fuga de reproducibilidad**: `hammer why-differs` mostró que `appstream` y `bison`
|
||||
/// divergen entre rebuilds ÚNICAMENTE en `.debug_*` (rutas internas del árbol de build), con el
|
||||
/// código ejecutable idéntico. Sin esas secciones, el artefacto reproduce.
|
||||
///
|
||||
/// ⚠ **ENTRA EN `hash_inputs`, y sólo si está fijado** — mismo diseño que `zig_version`. Las dos
|
||||
/// mitades de esa frase importan:
|
||||
/// · *entra*: si el lab pudiera cambiar lo que sale del build SIN mover el hash, el store
|
||||
/// diría que todo está al día mientras los artefactos ya no corresponden. En un sistema
|
||||
/// direccionado por contenido ése es el peor fallo posible: no falla, **miente**.
|
||||
/// · *sólo si está fijado*: una receta sin el campo conserva su hash de antes de que existiera,
|
||||
/// así que esto se puede desplegar receta a receta sin re-hashear el corpus de golpe.
|
||||
///
|
||||
/// POR QUÉ ES UNA PERILLA DEL LAB Y NO UN PASO EN CADA RECETA: de las 1161 recetas, **383 no
|
||||
/// tienen fase `install` explícita** (usan la heurística por defecto). Meter el strip receta a
|
||||
/// receta obligaría a escribir a mano ese install por defecto en las 383, con el riesgo de no
|
||||
/// clavarlo exacto. El lab lo hace una vez, igual para todas.
|
||||
///
|
||||
/// Usa `zig objcopy`, que SIEMPRE está en el sandbox (`/opt/zig`), en vez del `strip` de
|
||||
/// binutils: así no hay que añadir una dep de build a cada receta que lo active.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub strip_debug: Option<bool>,
|
||||
/// Habilita CGO en un build Go (`BuildSys::Go`). Por defecto el lab compila con `CGO_ENABLED=0`
|
||||
/// (Go puro, estático, sin C). Algunos proyectos necesitan cgo para drivers con C bundled (p.ej.
|
||||
/// el driver sqlite3 de mattn en usql/sq/dolt): con `cgo = true` el compile usa `CGO_ENABLED=1`
|
||||
@@ -478,6 +504,11 @@ impl Recipe {
|
||||
if let Some(zv) = &self.build.zig_version {
|
||||
v.push(format!("zig:{zv}").into_bytes());
|
||||
}
|
||||
// Ver la doc de `strip_debug`: cambia el CONTENIDO del artefacto, así que TIENE que entrar
|
||||
// al hash. Sólo si está fijado, para no mover los hashes de las recetas que no lo usan.
|
||||
if let Some(sd) = self.build.strip_debug {
|
||||
v.push(format!("strip_debug:{sd}").into_bytes());
|
||||
}
|
||||
for p in &self.source.patches {
|
||||
let resolved = self.base_dir.join(p);
|
||||
let bytes = std::fs::read(&resolved).map_err(|e| {
|
||||
@@ -854,6 +885,34 @@ expected_output = "b3:deadbeef"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strip_debug_entra_en_el_hash_solo_si_esta_fijado() {
|
||||
// Las DOS mitades de la invariante, porque cada una evita un desastre distinto.
|
||||
//
|
||||
// (1) SIN el campo, el hash NO se mueve. Es lo que permite desplegar el split de debug
|
||||
// receta a receta en vez de re-hashear las 1161 de golpe.
|
||||
let sin = Recipe::from_toml(SAMPLE).unwrap();
|
||||
assert_eq!(sin.build.strip_debug, None);
|
||||
|
||||
// (2) CON el campo, el hash SÍ cambia. `strip_debug` altera el CONTENIDO del artefacto
|
||||
// (le quita las secciones .debug_*), así que si no entrara al hash el lab podría cambiar
|
||||
// lo que sale del build sin que el store se enterara: diría que todo está al día
|
||||
// mientras los artefactos ya no corresponden a su hash. En un sistema direccionado por
|
||||
// contenido ése es el peor fallo posible — no falla, MIENTE. Es exactamente lo contrario
|
||||
// de `license`, que describe términos legales y por eso NO debe entrar.
|
||||
let mut con = Recipe::from_toml(SAMPLE).unwrap();
|
||||
con.build.strip_debug = Some(true);
|
||||
assert_ne!(
|
||||
sin.hash_inputs(&[]).unwrap(),
|
||||
con.hash_inputs(&[]).unwrap(),
|
||||
"strip_debug DEBE entrar en hash_inputs: cambia el contenido del artefacto"
|
||||
);
|
||||
|
||||
// Y round-trippea por TOML, que es como se va a escribir en las recetas.
|
||||
let re = Recipe::from_toml(&con.to_toml().unwrap()).unwrap();
|
||||
assert_eq!(re.build.strip_debug, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slots_validate_rechaza_hash_sin_prefijo() {
|
||||
let mut s = Slots::default();
|
||||
|
||||
@@ -15,6 +15,13 @@ strip_components = 2
|
||||
compiler = "zig-cc"
|
||||
target = "x86_64-linux-musl"
|
||||
link = "static"
|
||||
# ── PILOTO DEL SPLIT DE DEBUG (SDD 23 etapa 2) ─────────────────────────────────────────────────
|
||||
# Esta receta DIVERGÍA entre rebuilds: `hammer why-differs` mostró que las únicas secciones
|
||||
# distintas eran `.debug_*` (rutas internas del árbol de build), con el código ejecutable IDÉNTICO.
|
||||
# Al quitar esas secciones del artefacto, el artefacto reproduce — y de paso pesa mucho menos (el
|
||||
# 79% del contenido binario del store es info de depuración).
|
||||
# El campo entra en `hash_inputs`, así que re-hashea esta receta A PROPÓSITO: cambia el contenido.
|
||||
strip_debug = true
|
||||
|
||||
[build.phases]
|
||||
# subdir('docs/') es INCONDICIONAL (ignora -Ddocs=false) y exige xsltproc + stylesheets docbook-xsl
|
||||
@@ -24,4 +31,5 @@ compile = "PYTHONPATH=/usr/lib/python3.12/site-packages ninja -C output"
|
||||
install = "PYTHONPATH=/usr/lib/python3.12/site-packages DESTDIR=/out meson install -C output --no-rebuild"
|
||||
|
||||
[deps]
|
||||
build = ["meson", "samurai", "python3", "pkgconf", "gperf", "gettext-tiny", "itstool", "glib", "libxml2", "libyaml", "curl", "openssl", "libxmlb", "zstd", "pcre2", "libffi", "zlib"]
|
||||
# `binutils` por `strip_debug = true`: el paso de strip del lab usa su `strip --strip-debug`.
|
||||
build = ["binutils", "meson", "samurai", "python3", "pkgconf", "gperf", "gettext-tiny", "itstool", "glib", "libxml2", "libyaml", "curl", "openssl", "libxmlb", "zstd", "pcre2", "libffi", "zlib"]
|
||||
|
||||
@@ -19,4 +19,15 @@ sha256 = "06c9e13bdf7eb24d4ceb6b59205a4f67c2c7e7213119644430fe82fbd14a0abb"
|
||||
compiler = "zig-cc"
|
||||
target = "x86_64-linux-musl"
|
||||
link = "static"
|
||||
# ── PILOTO DEL SPLIT DE DEBUG (SDD 23 etapa 2) ─────────────────────────────────────────────────
|
||||
# Esta receta DIVERGÍA entre rebuilds: `hammer why-differs` mostró que las únicas secciones
|
||||
# distintas eran `.debug_*` (rutas internas del árbol de build), con el código ejecutable IDÉNTICO.
|
||||
# Al quitar esas secciones del artefacto, el artefacto reproduce — y de paso pesa mucho menos (el
|
||||
# 79% del contenido binario del store es info de depuración).
|
||||
# El campo entra en `hash_inputs`, así que re-hashea esta receta A PROPÓSITO: cambia el contenido.
|
||||
strip_debug = true
|
||||
flags = ["--disable-nls"]
|
||||
|
||||
[deps]
|
||||
# `binutils` por `strip_debug = true`: el paso de strip del lab usa su `strip --strip-debug`.
|
||||
build = ["binutils"]
|
||||
|
||||
Reference in New Issue
Block a user