# 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`. **Los dos son reales.** La primera versión de este documento decía que el primero se cancelaba «porque su premisa es falsa» — **me equivoqué, y la etapa 1 lo demostró en veinte minutos**. La corrección está en el §1.bis, y es el mejor argumento a favor de tener puertas. --- ## 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//`, `/opt/hammer/work`, `/tmp/`): **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 ``` ⇒ Conclusión que saqué entonces: «la reproducibilidad se sostiene, el `-ffile-prefix-map` no hace falta». **Era falsa, y duró lo que tardó en correrse la etapa 1.** Ver §1.bis. > **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 — sólo que UNA lo hace. Yo lo leí como si probara > más de lo que probaba, que es exactamente el error contra el que este mismo documento advierte > dos párrafos más arriba. --- ## 1.bis 🔴 CORRECCIÓN: sí hay no-determinismo, y el `-ffile-prefix-map` SÍ hace falta La etapa 1 (verificación amplia, `scripts/verificar-repro.sh`) se corrió sobre una muestra por clase de build. Resultado sobre las que pudieron reconstruirse: | receta | veredicto | |---|---| | `binutils` | ✓ REPRODUCE | | `appstream` | ✗ **DIVERGE** | | `bison` | ✗ **DIVERGE** | | `wl-clipboard` (§1c) | ✓ REPRODUCE | Y la causa, idéntica en las dos que divergen, dicha por `why-differs`: ``` difieren [.debug_aranges, .debug_info, .debug_pubnames, .debug_pubtypes, .debug_str] — sólo info de depuración (el código ejecutable es idéntico) · .debug_str sólo en B: /src/output/meson-private ``` **Dónde me equivoqué, exactamente:** el barrido de §1b buscaba rutas *del host* (`/home//`, `/tmp/`) y no encontró ninguna nuestra — cierto. Pero el no-determinismo no venía de una ruta del host sino de **rutas internas al árbol de build** (`/src/output/meson-private`) que **varían entre corridas** aunque `/src` sea constante. Buscar la forma equivocada de ruta y no hallarla no prueba que no haya otra. Y una sola muestra que reproduce (§1c) no es una muestra. ⇒ La lección del §1 sigue siendo válida y encima se refuerza: **el veredicto es reconstruir y comparar**. Sólo que esta vez el equivocado fui yo, y lo que me salvó fue haber puesto la puerta antes de la campaña en vez de después. ### 🎁 Y de ahí sale la mejor noticia del plan Las dos mitades **son el mismo trabajo**. La divergencia vive ENTERA en secciones `.debug_*` y el código ejecutable es idéntico ⇒ **separar el debug del artefacto principal hace que el artefacto principal reproduzca**, sin necesidad de tocar `-ffile-prefix-map` en 720 recetas. Queda por decidir qué se hace con el paquete `-debug` en sí (su contenido seguiría siendo no-determinista sin `-ffile-prefix-map`), pero eso es un problema mucho más chico: afecta a un artefacto secundario que nadie instala por defecto, y puede resolverse después o a la vez. --- ## 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 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.