El usuario decidió hacer «ahora» el re-hasheo masivo. Lo primero fue verificar la premisa, y no
se sostuvo: **el `-ffile-prefix-map` global no hace falta**. Se ahorran días de granja y un
re-hasheo de ~720 recetas.
EL PLAN HEREDADO decía «92 paquetes con no-determinismo probable, todos por rutas /src en
.debug_». Tres medidas, de fuerza creciente, lo desmontan:
(a) Las rutas `/src` son CONSTANTES: el lab bindea el árbol de fuentes en /src literal, así que
una ruta embebida en .debug_ es idéntica en cada rebuild y en cualquier máquina. Parece
sospechosa y es fija.
(b) Barrido de 195 artefactos buscando rutas que SÍ variarían (/home/<user>/, /opt/hammer/work,
/tmp/<aleatorio>): 15 las tienen, y ninguna es nuestra —
· /home/buildozer/aports/main/musl/src/musl-1.2.6 = la ruta de compilación DE ALPINE,
horneada en el musl prebuilt que inyectamos;
· /tmp/apiresponse.json = una cadena literal en el fuente de gron;
· /home/jimmac/, /home/sam/dev/ = rutas de los autores en los assets SVG de adwaita.
(c) LA PRUEBA QUE DECIDE: apartar el artefacto de wl-clipboard, reconstruir con deps cacheadas y
comparar con `hammer why-differs` → «24 entradas idénticas · 0 divergen · REPRODUCE».
La lección de método vale más que el ahorro: el no-determinismo se mide RECONSTRUYENDO Y
COMPARANDO, no buscando cadenas sospechosas. Un grep sobre binarios da candidatos, no veredictos
— misma clase de error que la regla del `.a` no-PIC (contaba reubicaciones de .debug_*) y que el
conteo de licencias con `grep -l license` (contaba comentarios). El proyecto YA tenía la
herramienta del veredicto; lo que faltaba era usarla antes de planificar.
QUEDA EL SPLIT DE DEBUG, que sí es real: el 79% del contenido binario del store son secciones
.debug_ ⇒ ~96 G de reserva y el espejo público de 126 G a ~30 G, que toca la decisión ya tomada
de servirlo desde el Storage Box.
🧨 Y APARECIÓ ALGO QUE CONDICIONA EL CÓMO: el entorno del lab NO ESTÁ HASHEADO. sandbox.rs fija
CC/-mcpu=baseline, SOURCE_DATE_EPOCH, TZ, LC_ALL, AR y codegen-units para TODOS los builds, y
ninguna entra en hash_inputs. ⇒ Cambiar el entorno del lab cambiaría lo que sale del build SIN
mover un solo hash: el store diría que todo está al día mientras los artefactos ya no
corresponden. En un sistema direccionado por contenido ese es el peor fallo posible: no falla,
miente. Hoy no muerde porque ese entorno no cambia nunca — y una campaña global es justo el
momento en que cambiaría.
Por eso el plan propone hacer el split en la fase `install` de cada receta (entra al hash por
diseño, auditable, re-hashea sólo lo que toca) y NO como flag del lab; más un ticket previo
chico: un `lab_version` en hash_inputs con el diseño de `zig_version` — sólo entra si está
fijado, así los hashes actuales no se mueven.
Cinco etapas con PUERTA cada una. La primera es verificación amplia de reproducibilidad (~30
recetas de clases distintas); si alguna diverge, ESO es el trabajo real y la campaña se pospone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
7.6 KiB
SDD 23 — Plan del re-hasheo masivo (y por qué es la mitad de lo que se creía)
Escrito 2026-08-08, tras la decisión del usuario de hacer «ahora, antes de seguir creciendo» el re-hasheo que llevaba tiempo pendiente.
La campaña se había planteado como dos trabajos que exigen reconstruir casi todo el corpus:
-ffile-prefix-mapglobal, para cerrar el «no-determinismo probable de 92 paquetes».- Separar la información de depuración en paquetes
-debug.
El primero se CANCELA: su premisa es falsa, y está medido. Queda el segundo, que es real.
1. 🛑 Lo primero fue verificar la premisa — y no se sostuvo
El plan heredado decía: «92 paquetes con no-determinismo probable, todos por rutas /src en
.debug_*; pendiente: -ffile-prefix-map global re-hashea ~720». Antes de comprometer días de
granja, se comprobó. Tres medidas, en orden de fuerza creciente:
(a) Las rutas /src son CONSTANTES, no variables. El lab bindea el árbol de fuentes en /src
—literal, siempre— así que una ruta /src/foo.c embebida en .debug_ es idéntica en cada rebuild y
en cualquier máquina. No es no-determinismo: es una ruta fija que parece sospechosa.
(b) Barrido de 195 artefactos buscando rutas que sí variarían (/home/<user>/,
/opt/hammer/work, /tmp/<aleatorio>): 15 las tienen (≈8%). Pero al mirarlas una a una,
ninguna es nuestra:
| ruta hallada | qué es realmente |
|---|---|
/home/buildozer/aports/main/musl/src/musl-1.2.6 |
la ruta de compilación de Alpine, horneada en el musl prebuilt que inyectamos |
/tmp/apiresponse.json |
una cadena literal en el código fuente de gron |
/home/jimmac/, /home/sam/dev/ |
rutas de los autores dentro de los assets SVG de adwaita |
Las tres son constantes que vienen de fuera y son idénticas en cada rebuild. Buscar rutas con
pinta de host no mide no-determinismo: mide qué escribieron terceros en sus fuentes. Es la misma
clase de error que la regla del .a no-PIC —grep R_X86_64_32 contaba reubicaciones de
.debug_*— y que el conteo de licencias con grep -l license, que contaba comentarios.
(c) La prueba que decide: reconstruir y comparar. Se apartó el artefacto de wl-clipboard, se
reconstruyó con las deps cacheadas y se comparó con hammer why-differs:
24 entradas idénticas · 0 divergen
✓ los dos árboles son idénticos: REPRODUCE
⇒ La reproducibilidad se sostiene. El -ffile-prefix-map global no hace falta. Se ahorran días
de granja y un re-hasheo de ~720 recetas.
La lección de método, que vale más que el ahorro: el no-determinismo se mide reconstruyendo y comparando, no buscando cadenas sospechosas. Un
grepsobre binarios genera candidatos, no veredictos. Y este proyecto ya tiene la herramienta del veredicto (why-differs); lo que faltaba era usarla antes de planificar.⚠️ Esto no prueba que las 1153 reproduzcan: prueba que la causa que se les atribuía no existe, y que el mecanismo funciona. La verificación amplia es el §4.
2. Lo que sí queda: separar la información de depuración
Medido sobre muestra amplia de .so/.a: el 79% del contenido binario del store son secciones
.debug_* (qtdeclarative solo llega al 87%). Consecuencias, las dos que importan:
- ~96 G de reserva de disco — el store de ~126 G serían ~30 G sin debug.
- El espejo público baja de 126 G a ~30 G, que es la diferencia entre alojar barato y caro, y toca la decisión ya tomada de servirlo desde el Storage Box.
Y no es sólo tamaño: separar el debug es lo que permite entregar los símbolos a quien depura sin imponérselos a quien sólo usa la distro, que es lo que hacen todas las distros serias.
El precio, dicho claro
strip posterior no sirve: rompería la verificación bit-a-bit, porque el ArtifactHash es de
ENTRADA (se calcula sin construir) y quedaría apuntando a un contenido que un rebuild ya no
reproduce. Hay que hacerlo dentro del build, y eso cambia la fase install ⇒ re-hashea las
recetas afectadas. Es el coste real y no hay atajo.
3. 🧨 El hallazgo que condiciona CÓMO se hace: el entorno del lab NO está hasheado
Al buscar dónde poner una flag global apareció algo que hay que decidir antes de tocar nada.
crates/hammer-build/src/sandbox.rs fija el entorno de TODOS los builds:
CC=hammer-zig-cc (con -mcpu=baseline) SOURCE_DATE_EPOCH=1 TZ=UTC LC_ALL=C
AR="zig ar" CARGO_BUILD_… codegen-units=1
Ninguna de esas entra en Recipe::hash_inputs, que es una lista blanca de source, compiler,
target, link, zig_version, patches, flags, phases y deps.
⇒ Cambiar el entorno del lab cambia lo que sale del build SIN cambiar un solo hash. El store diría que todo está al día mientras los artefactos ya no corresponden a su hash. Es el fallo más peligroso posible en un sistema direccionado por contenido: no falla, miente.
Hoy no muerde porque ese entorno no cambia nunca. Pero una campaña global es precisamente el momento en que cambiaría.
Las dos formas de hacerlo, y cuál propongo
- A — flag en el lab (una línea en
sandbox.rs, aplica a todo): barata de escribir y rompe el invariante por lo de arriba. Descartada, salvo que se añada primero unlab_versionahash_inputs(ver abajo). - B — en la fase
installde cada receta (strip --only-keep-debug+objcopy): explícita, entra al hash por diseño, re-hashea sólo lo que toca, y es auditable receta a receta. Propuesta. El coste es editar ~1100 recetas, pero es mecánico y verificable — exactamente el mismo patrón con el que hoy se poblaron 1074 licencias sin mover un hash.
Y un ticket previo, chico y de fondo: añadir a hash_inputs un lab_version que capture el
entorno del sandbox, con el mismo diseño que zig_version (sólo entra si está fijado, así los
hashes actuales no se mueven). Sin eso, cualquier cambio futuro del lab vuelve a ser invisible.
Es barato hoy y caro el día que haga falta.
4. El plan, por etapas y con puertas
Cada etapa tiene una puerta: si no pasa, se para. Una campaña de días sin puertas es una forma elegante de romper el corpus.
| # | etapa | puerta |
|---|---|---|
| 0 | lab_version en hash_inputs (opcional, no fijado) |
los 1153 hashes actuales no se mueven |
| 1 | Verificación AMPLIA de reproducibilidad: apartar y reconstruir una muestra de ~30 recetas de clases distintas (C, Rust, Go, meson, cmake) y comparar con why-differs |
0 divergencias; si alguna diverge, ESO es el trabajo real y esta campaña se pospone |
| 2 | Piloto del split de debug en 5 recetas representativas | los -debug salen, el binario sigue funcionando, y el rebuild reproduce bit a bit |
| 3 | Medir de verdad el ahorro sobre esas 5 y extrapolar | si el ahorro real se aleja mucho del 79% teórico, se replantea |
| 4 | Aplicar al corpus por tandas, con la granja | cada tanda: store-gc + why-differs sobre una muestra |
| 5 | Regenerar grafos, perfiles y el espejo | los perfiles siguen cerrando (escritorio-sway 121/121, KDE 162/162…) |
Orden respecto de lo demás: la etapa 1 conviene ANTES que nada, porque si la reproducibilidad
amplia no se sostiene, todo lo demás cambia de prioridad. Y es barata: apartar artefactos y
reconstruir con deps cacheadas, como se hizo con wl-clipboard.
5. Lo que este plan NO propone
- No tocar
-ffile-prefix-map: §1 lo cerró. - No strippear artefactos ya sellados: rompe la evidencia, que es el invariante del proyecto.
- No meter la flag en el lab sin
lab_version: §3. - No empezar por el corpus entero: §4 empieza por 5 recetas, y con razón.