Files
hammer/docs/23-plan-rehasheo.md
T
sergioandClaude Opus 5 38f26eb3a0 docs: SDD 23 — plan del re-hasheo, y media campaña CANCELADA porque su premisa era falsa
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>
2026-08-07 23:42:22 -04:00

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:

  1. -ffile-prefix-map global, para cerrar el «no-determinismo probable de 92 paquetes».
  2. 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 grep sobre 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 installre-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 un lab_version a hash_inputs (ver abajo).
  • B — en la fase install de 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.