tawasuyu

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1182Shell 87Rust 63Markdown 541592 Definitions

⎇ 80319d9bab35edd2ab1055efc4dc43d45f18eeb1 docs/23-plan-rehasheo.md Markdown · 353 lines · 18.6 KB History raw

granja: el worker NO TIENE Rust ni Go — el 76% del catálogo sólo construye en el hub dff840966 · sergio ·

1# SDD 23 — Plan del re-hasheo masivo (corregido: la etapa 1 desmintió mi primera conclusión)
2
3Escrito 2026-08-08, tras la decisión del usuario de hacer «ahora, antes de seguir creciendo» el
4re-hasheo que llevaba tiempo pendiente.
5
6La campaña se había planteado como **dos** trabajos que exigen reconstruir casi todo el corpus:
7
81. `-ffile-prefix-map` global, para cerrar el «no-determinismo probable de 92 paquetes».
92. Separar la información de depuración en paquetes `-debug`.
10
11**Los dos son reales.** La primera versión de este documento decía que el primero se cancelaba
12«porque su premisa es falsa» — **me equivoqué, y la etapa 1 lo demostró en veinte minutos**. La
13corrección está en el §1.bis, y es el mejor argumento a favor de tener puertas.
14
15---
16
17## 1. Lo primero fue verificar la premisa (y me pasé de confiado — ver §1.bis)
18
19El plan heredado decía: *«92 paquetes con no-determinismo probable, todos por rutas `/src` en
20`.debug_*`; pendiente: `-ffile-prefix-map` global re-hashea ~720»*. Antes de comprometer días de
21granja, se comprobó. Tres medidas, en orden de fuerza creciente:
22
23**(a) Las rutas `/src` son CONSTANTES, no variables.** El lab bindea el árbol de fuentes en `/src`
24—literal, siempre— así que una ruta `/src/foo.c` embebida en `.debug_` es idéntica en cada rebuild y
25en cualquier máquina. No es no-determinismo: es una ruta fija que *parece* sospechosa.
26
27**(b) Barrido de 195 artefactos buscando rutas que sí variarían** (`/home/<user>/`,
28`/opt/hammer/work`, `/tmp/<aleatorio>`): **15 las tienen (≈8%)**. Pero al mirarlas una a una,
29ninguna es nuestra:
30
31| ruta hallada | qué es realmente |
32|---|---|
33| `/home/buildozer/aports/main/musl/src/musl-1.2.6` | la ruta de compilación **de Alpine**, horneada en el `musl` prebuilt que inyectamos |
34| `/tmp/apiresponse.json` | una **cadena literal** en el código fuente de `gron` |
35| `/home/jimmac/`, `/home/sam/dev/` | rutas de los autores dentro de los **assets SVG** de adwaita |
36
37Las tres son constantes que vienen de fuera y son idénticas en cada rebuild. **Buscar rutas con
38pinta de host no mide no-determinismo: mide qué escribieron terceros en sus fuentes.** Es la misma
39clase de error que la regla del `.a` no-PIC —`grep R_X86_64_32` contaba reubicaciones de
40`.debug_*`— y que el conteo de licencias con `grep -l license`, que contaba comentarios.
41
42**(c) La prueba que decide: reconstruir y comparar.** Se apartó el artefacto de `wl-clipboard`, se
43reconstruyó con las deps cacheadas y se comparó con `hammer why-differs`:
44
45```
46   24 entradas idénticas · 0 divergen
47   ✓ los dos árboles son idénticos: REPRODUCE
48```
49
50⇒ Conclusión que saqué entonces: «la reproducibilidad se sostiene, el `-ffile-prefix-map` no hace
51falta». **Era falsa, y duró lo que tardó en correrse la etapa 1.** Ver §1.bis.
52
53> **La lección de método, que vale más que el ahorro:** el no-determinismo se mide **reconstruyendo y
54> comparando**, no buscando cadenas sospechosas. Un `grep` sobre binarios genera candidatos, no
55> veredictos. Y este proyecto ya tiene la herramienta del veredicto (`why-differs`); lo que faltaba
56> era usarla antes de planificar.
57>
58> ⚠️ Esto **no** prueba que las 1153 reproduzcan — sólo que UNA lo hace. Yo lo leí como si probara
59> más de lo que probaba, que es exactamente el error contra el que este mismo documento advierte
60> dos párrafos más arriba.
61
62---
63
64## 1.bis 🔴 CORRECCIÓN: sí hay no-determinismo, y el `-ffile-prefix-map` SÍ hace falta
65
66La etapa 1 (verificación amplia, `scripts/verificar-repro.sh`) se corrió sobre una muestra por clase
67de build. Resultado sobre las que pudieron reconstruirse:
68
69| receta | veredicto |
70|---|---|
71| `binutils` | ✓ REPRODUCE |
72| `appstream` | ✗ **DIVERGE** |
73| `bison` | ✗ **DIVERGE** |
74| `wl-clipboard` (§1c) | ✓ REPRODUCE |
75
76Y la causa, idéntica en las dos que divergen, dicha por `why-differs`:
77
78```
79difieren [.debug_aranges, .debug_info, .debug_pubnames, .debug_pubtypes, .debug_str]
80 — sólo info de depuración (el código ejecutable es idéntico)
81 · .debug_str sólo en B: /src/output/meson-private
82```
83
84**Dónde me equivoqué, exactamente:** el barrido de §1b buscaba rutas *del host* (`/home/<user>/`,
85`/tmp/<aleatorio>`) y no encontró ninguna nuestra — cierto. Pero el no-determinismo no venía de una
86ruta del host sino de **rutas internas al árbol de build** (`/src/output/meson-private`) que **varían
87entre corridas** aunque `/src` sea constante. Buscar la forma equivocada de ruta y no hallarla no
88prueba que no haya otra. Y una sola muestra que reproduce (§1c) no es una muestra.
89
90⇒ La lección del §1 sigue siendo válida y encima se refuerza: **el veredicto es reconstruir y
91comparar**. Sólo que esta vez el equivocado fui yo, y lo que me salvó fue haber puesto la puerta
92antes de la campaña en vez de después.
93
94### 🎁 Y de ahí sale la mejor noticia del plan
95Las dos mitades **son el mismo trabajo**. La divergencia vive ENTERA en secciones `.debug_*` y el
96código ejecutable es idéntico ⇒ **separar el debug del artefacto principal hace que el artefacto
97principal reproduzca**, sin necesidad de tocar `-ffile-prefix-map` en 720 recetas.
98
99Queda por decidir qué se hace con el paquete `-debug` en sí (su contenido seguiría siendo
100no-determinista sin `-ffile-prefix-map`), pero eso es un problema mucho más chico: afecta a un
101artefacto secundario que nadie instala por defecto, y puede resolverse después o a la vez.
102
103---
104
105## 2. Lo que sí queda: separar la información de depuración
106
107Medido sobre muestra amplia de `.so`/`.a`: **el 79% del contenido binario del store son secciones
108`.debug_*`** (qtdeclarative solo llega al 87%). Consecuencias, las dos que importan:
109
110- **~96 G de reserva de disco** — el store de ~126 G serían ~30 G sin debug.
111- **El espejo público baja de 126 G a ~30 G**, que es la diferencia entre alojar barato y caro, y
112  toca la decisión ya tomada de servirlo desde el Storage Box.
113
114Y no es sólo tamaño: separar el debug es lo que permite **entregar los símbolos a quien depura sin
115imponérselos a quien sólo usa la distro**, que es lo que hacen todas las distros serias.
116
117### El precio, dicho claro
118`strip` posterior **no sirve**: rompería la verificación bit-a-bit, porque el `ArtifactHash` es de
119ENTRADA (se calcula sin construir) y quedaría apuntando a un contenido que un rebuild ya no
120reproduce. Hay que hacerlo **dentro del build**, y eso cambia la fase `install` ⇒ **re-hashea las
121recetas afectadas**. Es el coste real y no hay atajo.
122
123---
124
125## 3. 🧨 El hallazgo que condiciona CÓMO se hace: el entorno del lab NO está hasheado
126
127Al buscar dónde poner una flag global apareció algo que hay que decidir antes de tocar nada.
128
129`crates/hammer-build/src/sandbox.rs` fija el entorno de TODOS los builds:
130
131```
132CC=hammer-zig-cc (con -mcpu=baseline)   SOURCE_DATE_EPOCH=1   TZ=UTC   LC_ALL=C
133AR="zig ar"   CARGO_BUILD_… codegen-units=1
134```
135
136**Ninguna de esas entra en `Recipe::hash_inputs`**, que es una lista blanca de source, compiler,
137target, link, `zig_version`, patches, flags, phases y deps.
138
139⇒ **Cambiar el entorno del lab cambia lo que sale del build SIN cambiar un solo hash.** El store
140diría que todo está al día mientras los artefactos ya no corresponden a su hash. Es el fallo más
141peligroso posible en un sistema direccionado por contenido: no falla, miente.
142
143Hoy no muerde porque ese entorno no cambia nunca. Pero una campaña global es precisamente el momento
144en que cambiaría.
145
146### Las dos formas de hacerlo, y cuál propongo
147- **A — flag en el lab** (una línea en `sandbox.rs`, aplica a todo): barata de escribir y
148  **rompe el invariante** por lo de arriba. **Descartada**, salvo que se añada primero un
149  `lab_version` a `hash_inputs` (ver abajo).
150- **B — en la fase `install` de cada receta** (`strip --only-keep-debug` + `objcopy`): explícita,
151  entra al hash por diseño, re-hashea sólo lo que toca, y es auditable receta a receta.
152  **Propuesta.** El coste es editar ~1100 recetas, pero es mecánico y verificable — exactamente el
153  mismo patrón con el que hoy se poblaron 1074 licencias sin mover un hash.
154
155**Y un ticket previo, chico y de fondo:** añadir a `hash_inputs` un `lab_version` que capture el
156entorno del sandbox, con el mismo diseño que `zig_version` (**sólo entra si está fijado**, así los
157hashes actuales no se mueven). Sin eso, cualquier cambio futuro del lab vuelve a ser invisible.
158Es barato hoy y caro el día que haga falta.
159
160---
161
162## 4. El plan, por etapas y con puertas
163
164Cada etapa tiene una **puerta**: si no pasa, se para. Una campaña de días sin puertas es una forma
165elegante de romper el corpus.
166
167| # | etapa | puerta |
168|---|---|---|
169| 0 | `lab_version` en `hash_inputs` (opcional, no fijado) | los 1153 hashes actuales **no se mueven** |
170| 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 |
171| 2 | Piloto del split de debug en **5 recetas** representativas | los `-debug` salen, el binario sigue funcionando, y el rebuild reproduce bit a bit |
172| 3 | Medir de verdad el ahorro y extrapolar — ✅ **HECHO, ver §6** | el ahorro real es **~60%**, no 79%: se replanteó |
173| 4 | Aplicar al corpus por tandas, con la granja | cada tanda: `store-gc` + `why-differs` sobre una muestra |
174| 5 | Regenerar grafos, perfiles y el espejo | los perfiles siguen cerrando (`escritorio-sway` 121/121, KDE 162/162…) |
175
176**Orden respecto de lo demás**: la etapa 1 conviene ANTES que nada, porque si la reproducibilidad
177amplia no se sostiene, todo lo demás cambia de prioridad. Y es barata: apartar artefactos y
178reconstruir con deps cacheadas, como se hizo con `wl-clipboard`.
179
180---
181
182## 5. Lo que este plan NO propone
183
184- **No** tocar `-ffile-prefix-map`: §1 lo cerró.
185- **No** strippear artefactos ya sellados: rompe la evidencia, que es el invariante del proyecto.
186- **No** meter la flag en el lab sin `lab_version`: §3.
187- **No** empezar por el corpus entero: §4 empieza por 5 recetas, y con razón.
188
189
190---
191
192## 6. Etapa 3 — el ahorro real es ~60%, no 79%, y las cifras publicadas estaban infladas
193
194El «79%» que este documento y los SDD 19/20 venían citando era **el 79% del CONTENIDO BINARIO**,
195medido sobre una muestra de `.so`/`.a`. Es cierto y es la cifra equivocada para planificar, porque
196un artefacto no es sólo binarios: trae cabeceras, datos, iconos, locales, `.pc` y documentación,
197que no encogen.
198
199**Medido con `scripts/medir-debug.sh`** (suma los tamaños reales de las secciones `.debug_*` vía
200`readelf -S` y los compara con el tamaño del árbol, sin reconstruir nada):
201
202| muestra | artefactos | volumen | debug / artefacto |
203|---|---:|---:|---:|
204| pequeña | 40 | 876 MB | 27% |
205| media (truncada a 60 ficheros/artefacto) | 100 | 2 955 MB | 38% |
206| **grande, sin truncar** | **250** | **8 874 MB** | **60%** |
207
208La varianza es alta porque unos pocos artefactos grandes dominan el total. **La muestra grande es la
209que manda** (cubre ~7% del store), y encaja con lo único que está medido de verdad —el piloto de la
210etapa 2, donde se reconstruyó y se pesó:
211
212- `bison` 6 M → 3 M = **−50%**
213- `appstream` 56 M → 18 M = **−68%**
214
215Los dos **encierran** el 60% de la muestra grande. Ésa es la validación: el modelo predice lo que el
216rebuild real produjo.
217
218### Las cifras corregidas
219Con el store en **127 G** y un ahorro de ~60%:
220
221| | se venía diciendo | medido |
222|---|---|---|
223| reserva de disco | ~96 G | **~76 G** |
224| store tras el split | ~30 G | **~51 G** |
225| espejo público | ~30 G | **~51 G** |
226
227Sigue siendo el mayor ahorro disponible en el proyecto —76 G no es poco— pero **no es lo prometido**,
228y la diferencia importa para dimensionar el alojamiento del espejo, que es una decisión con factura.
229
230> **La lección:** una cifra medida sobre una cosa (contenido binario) se citó durante semanas como si
231> midiera otra (tamaño de artefacto), y llegó a tres documentos. El número no era falso; **la unidad
232> sí**. Cuando un número vaya a decidir un gasto, conviene volver a la medición original y comprobar
233> QUÉ estaba midiendo.
234
235
236---
237
238## 7. 🔴 SEGUNDA CORRECCIÓN: lo que medía el test era DERIVA, no no-determinismo
239
240El §1.bis dijo «sí hay no-determinismo» al ver que `appstream` y `bison` divergían. **También estaba
241mal**, y por un fallo de diseño del propio test.
242
243`scripts/verificar-repro.sh` aparta el artefacto **guardado en el store** y lo compara con una
244reconstrucción de hoy. Pero ese artefacto guardado puede tener meses: se construyó con **otro estado
245del lab** (otro zig, otras flags, otro entorno). Así que el test conflaba dos cosas muy distintas:
246
247- **no-determinismo** — mismas entradas, mismo lab, salidas distintas. Es lo que rompe el invariante.
248- **deriva** — el artefacto viejo no es lo que el lab de HOY produce. No es no-determinismo: es que
249  el mundo se movió.
250
251**La prueba que las separa: construir DOS VECES HOY.** Y se hizo, sin querer al principio: al volver
252a correr el verificador sobre recetas ya reconstruidas, todas pasaron a REPRODUCIR.
253
254| receta | 1ª corrida (viejo vs hoy) | 2ª corrida (hoy vs hoy) |
255|---|---|---|
256| `anew` | ✗ diverge | ✓ **REPRODUCE** |
257| `gron` | ✗ diverge | ✓ **REPRODUCE** |
258| `age` | ✗ diverge (¡y le faltaba `age-inspect`!) | ✓ **REPRODUCE** |
259
260⇒ **El corpus es determinista hoy.** Lo que hay es deriva contra artefactos viejos.
261
262### Qué significa para la campaña, sin adornos
263- **El argumento de reproducibilidad para el split de debug se cae.** El split se justifica por
264  ESPACIO (~60%, §6), que sigue siendo real y grande. Nada más.
265- **Pero la deriva es un problema por derecho propio, y mayor**: el store contiene artefactos que el
266  lab de hoy **no reproduciría**. Mientras eso siga así, la frase «nuestros artefactos son
267  verificables por terceros» es falsa para una parte del corpus — un tercero que reconstruya no
268  obtendrá lo publicado. `age` es el caso feo: la reconstrucción **ni siquiera instala los mismos
269  ficheros** (falta `age-inspect`), o sea que la deriva no es cosmética.
270- ⇒ La reconstrucción masiva **sigue valiendo la pena**, pero por una razón distinta de la que este
271  documento decía: no para *arreglar* el determinismo, sino para **poner el store al día con el lab**
272  y que la promesa de reproducibilidad sea cierta sobre todo el corpus.
273
274### La lección, que es la misma tres veces
275Este documento se equivocó tres veces seguidas y las tres se corrigieron midiendo:
2761. «el `-ffile-prefix-map` no hace falta» — falso, por leer una sola muestra;
2772. «sí hay no-determinismo» — falso, por un test que confundía deriva con no-determinismo;
2783. y de paso, el «79%» que medía contenido binario y se citaba como tamaño de artefacto.
279
280**Un test hay que diseñarlo contra la pregunta, no contra lo que es fácil de comparar.** Comparar
281con lo que hay en el store es cómodo; comparar dos builds de hoy es lo que contesta.
282
283---
284
285## 8. Etapa 4 en marcha — y la lección que faltaba en el plan: **el orden importa más que la tanda**
286
287La primera tanda activó `strip_debug` en `expat`, `zstd` y `ncurses`. Son **bibliotecas base**, así
288que re-hashearon en cascada **54 recetas**, incluida la cadena `wlroots`/`sway` sellada la noche
289anterior. No se pierde nada —los artefactos viejos siguen en el store y en el respaldo, y las recetas
290están en git— pero el perfil `escritorio-sway` deja de cerrar 121/121 hasta reconstruirlas.
291
292⇒ **La tanda correcta no es «las 25 siguientes por orden alfabético», sino las HOJAS del grafo
293primero, o un nivel entero de una vez con la granja arriba.** Tocar tres bibliotecas base costó 54
294reconstrucciones; tocar cincuenta hojas no habría costado ninguna extra. `scripts/desplegar-strip.sh`
295toma las candidatas en orden alfabético y eso hay que cambiarlo: debe ordenar por profundidad en el
296grafo, de hoja a raíz.
297
298### Operación: dos cosas que cuestan una corrida cada una
299- **Un drenaje largo NO puede vivir dentro del `ssh`.** La primera pasada se cortó a las 14 de 54 al
300  caerse la sesión. Va con `nohup` en el worker, siempre.
301- **La flota efímera reusa IPs** ⇒ la clave de host cambia y cada conexión grita MITM. Se limpia con
302  `ssh-keygen -R <ip>`, pero con workers que van y vienen esto se repetirá: conviene que `farm-up` lo
303  haga solo al crear la máquina.
304
305### Fallos de la primera pasada — ninguno nuevo, y uno que ya estaba avisado
306| receta | causa |
307|---|---|
308| `dwarves` | el muro de libdw/musl, ya documentado como frente propio |
309| `dbus` | «Unhandled python exception» de meson |
310| `adwaita-hello` | **error 39 (Directory not empty)** — la carrera del árbol de fuentes del **ADR 0012** |
311
312El último merece subrayado: **ADR 0012 lleva meses «pendiente sin decidir» y acaba de morder**. Dos
313builds de la misma dep se pisan el árbol de `work/sources` y lo dejan a medias. Una campaña de
314reconstrucción masiva es exactamente el escenario que lo dispara, así que decidirlo deja de ser
315opcional si la etapa 4 va a correr en paralelo.
316
317---
318
319## 9. 🧱 EL BLOQUEANTE ESTRUCTURAL DE LA GRANJA: el worker no tiene Rust ni Go
320
321Medido el 2026-08-08 al lanzar COSMIC:
322
323```
324worker:  cargo AUSENTE · rustc AUSENTE · go AUSENTE
325hub:     cargo ✓ · rustc ✓ · go ✓   (~/.cargo/bin)
326```
327
328`hammer` invoca **`cargo vendor` en el HOST**, no dentro del sandbox hermético — el vendoreo pasa
329antes de entrar a la caja. El worker no trae ningún toolchain de Rust ni Go, así que **la granja no
330puede construir NINGUNA receta Rust o Go**:
331
332- **COSMIC entero** (todos los `cosmic-*` son Rust) — de ahí que 30 raíces fallaran de golpe con
333  `spawn cargo vendor: No such file`;
334- las **228 recetas Rust** y las **362 Go** del corpus.
335
336O sea que **el 76% del catálogo sólo puede construirse en el hub**, que es una sola máquina — justo
337lo que el respaldo del 2026-08-07 intentaba dejar de asumir.
338
339### Lo que esto explica hacia atrás
340Todos los «no construyó» de Go/Rust en el worker durante esta campaña —`amp`, `anew`, `apko`,
341`atlas`, `bom`, `broot`, `cargo-audit`, `cargo-hack`, `git-absorb`— eran **la misma causa**, y yo la
342atribuí primero a «necesitan red para sus módulos». La red no tenía nada que ver: **falta el
343binario**.
344
345### Y no rompe la hermeticidad arreglarlo
346`cargo`/`go` aquí son **herramientas de FETCH**, del mismo orden que `git` para clonar un repo: se
347usan para traer y fijar las deps ANTES del build, no dentro del sandbox. Instalarlos en la imagen
348golden del worker no relaja el aislamiento del build — el build sigue ocurriendo en la caja con el
349árbol ya vendoreado. Es exactamente lo contrario del caso «no engordar el rootfs del worker», que
350habla del rootfs DEL SANDBOX.
351
352⇒ **Es una decisión de aprovisionamiento, no de arquitectura**, y desbloquea tres cuartas partes del
353catálogo para la granja.

23-plan-rehasheo.md

SDD 23 — Plan del re-hasheo masivo (corregido: la etapa 1 desmintió mi primera conclusión)

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 me pasé de confiado — ver §1.bis)

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 halladaqué es realmente
/home/buildozer/aports/main/musl/src/musl-1.2.6la ruta de compilación de Alpine, horneada en el musl prebuilt que inyectamos
/tmp/apiresponse.jsonuna 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:

recetaveredicto
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/<user>/, /tmp/<aleatorio>) 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:

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

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.

#etapapuerta
0lab_version en hash_inputs (opcional, no fijado)los 1153 hashes actuales no se mueven
1Verificación AMPLIA de reproducibilidad: apartar y reconstruir una muestra de ~30 recetas de clases distintas (C, Rust, Go, meson, cmake) y comparar con why-differs0 divergencias; si alguna diverge, ESO es el trabajo real y esta campaña se pospone
2Piloto del split de debug en 5 recetas representativaslos -debug salen, el binario sigue funcionando, y el rebuild reproduce bit a bit
3Medir de verdad el ahorro y extrapolar — ✅ HECHO, ver §6el ahorro real es ~60%, no 79%: se replanteó
4Aplicar al corpus por tandas, con la granjacada tanda: store-gc + why-differs sobre una muestra
5Regenerar grafos, perfiles y el espejolos 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


6. Etapa 3 — el ahorro real es ~60%, no 79%, y las cifras publicadas estaban infladas

El «79%» que este documento y los SDD 19/20 venían citando era el 79% del CONTENIDO BINARIO, medido sobre una muestra de .so/.a. Es cierto y es la cifra equivocada para planificar, porque un artefacto no es sólo binarios: trae cabeceras, datos, iconos, locales, .pc y documentación, que no encogen.

Medido con scripts/medir-debug.sh (suma los tamaños reales de las secciones .debug_* vía readelf -S y los compara con el tamaño del árbol, sin reconstruir nada):

muestraartefactosvolumendebug / artefacto
pequeña40876 MB27%
media (truncada a 60 ficheros/artefacto)1002 955 MB38%
grande, sin truncar2508 874 MB60%

La varianza es alta porque unos pocos artefactos grandes dominan el total. La muestra grande es la que manda (cubre ~7% del store), y encaja con lo único que está medido de verdad —el piloto de la etapa 2, donde se reconstruyó y se pesó:

Los dos encierran el 60% de la muestra grande. Ésa es la validación: el modelo predice lo que el rebuild real produjo.

Las cifras corregidas

Con el store en 127 G y un ahorro de ~60%:

se venía diciendomedido
reserva de disco~96 G~76 G
store tras el split~30 G~51 G
espejo público~30 G~51 G

Sigue siendo el mayor ahorro disponible en el proyecto —76 G no es poco— pero no es lo prometido, y la diferencia importa para dimensionar el alojamiento del espejo, que es una decisión con factura.

La lección: una cifra medida sobre una cosa (contenido binario) se citó durante semanas como si midiera otra (tamaño de artefacto), y llegó a tres documentos. El número no era falso; la unidad sí. Cuando un número vaya a decidir un gasto, conviene volver a la medición original y comprobar QUÉ estaba midiendo.


7. 🔴 SEGUNDA CORRECCIÓN: lo que medía el test era DERIVA, no no-determinismo

El §1.bis dijo «sí hay no-determinismo» al ver que appstream y bison divergían. También estaba mal, y por un fallo de diseño del propio test.

scripts/verificar-repro.sh aparta el artefacto guardado en el store y lo compara con una reconstrucción de hoy. Pero ese artefacto guardado puede tener meses: se construyó con otro estado del lab (otro zig, otras flags, otro entorno). Así que el test conflaba dos cosas muy distintas:

La prueba que las separa: construir DOS VECES HOY. Y se hizo, sin querer al principio: al volver a correr el verificador sobre recetas ya reconstruidas, todas pasaron a REPRODUCIR.

receta1ª corrida (viejo vs hoy)2ª corrida (hoy vs hoy)
anew✗ diverge✓ REPRODUCE
gron✗ diverge✓ REPRODUCE
age✗ diverge (¡y le faltaba age-inspect!)✓ REPRODUCE

⇒ El corpus es determinista hoy. Lo que hay es deriva contra artefactos viejos.

Qué significa para la campaña, sin adornos

La lección, que es la misma tres veces

Este documento se equivocó tres veces seguidas y las tres se corrigieron midiendo:

  1. «el -ffile-prefix-map no hace falta» — falso, por leer una sola muestra;
  2. «sí hay no-determinismo» — falso, por un test que confundía deriva con no-determinismo;
  3. y de paso, el «79%» que medía contenido binario y se citaba como tamaño de artefacto.

Un test hay que diseñarlo contra la pregunta, no contra lo que es fácil de comparar. Comparar con lo que hay en el store es cómodo; comparar dos builds de hoy es lo que contesta.


8. Etapa 4 en marcha — y la lección que faltaba en el plan: el orden importa más que la tanda

La primera tanda activó strip_debug en expat, zstd y ncurses. Son bibliotecas base, así que re-hashearon en cascada 54 recetas, incluida la cadena wlroots/sway sellada la noche anterior. No se pierde nada —los artefactos viejos siguen en el store y en el respaldo, y las recetas están en git— pero el perfil escritorio-sway deja de cerrar 121/121 hasta reconstruirlas.

⇒ La tanda correcta no es «las 25 siguientes por orden alfabético», sino las HOJAS del grafo primero, o un nivel entero de una vez con la granja arriba. Tocar tres bibliotecas base costó 54 reconstrucciones; tocar cincuenta hojas no habría costado ninguna extra. scripts/desplegar-strip.sh toma las candidatas en orden alfabético y eso hay que cambiarlo: debe ordenar por profundidad en el grafo, de hoja a raíz.

Operación: dos cosas que cuestan una corrida cada una

Fallos de la primera pasada — ninguno nuevo, y uno que ya estaba avisado

recetacausa
dwarvesel muro de libdw/musl, ya documentado como frente propio
dbus«Unhandled python exception» de meson
adwaita-helloerror 39 (Directory not empty) — la carrera del árbol de fuentes del ADR 0012

El último merece subrayado: ADR 0012 lleva meses «pendiente sin decidir» y acaba de morder. Dos builds de la misma dep se pisan el árbol de work/sources y lo dejan a medias. Una campaña de reconstrucción masiva es exactamente el escenario que lo dispara, así que decidirlo deja de ser opcional si la etapa 4 va a correr en paralelo.


9. 🧱 EL BLOQUEANTE ESTRUCTURAL DE LA GRANJA: el worker no tiene Rust ni Go

Medido el 2026-08-08 al lanzar COSMIC:

worker:  cargo AUSENTE · rustc AUSENTE · go AUSENTE
hub:     cargo ✓ · rustc ✓ · go ✓   (~/.cargo/bin)

hammer invoca cargo vendor en el HOST, no dentro del sandbox hermético — el vendoreo pasa antes de entrar a la caja. El worker no trae ningún toolchain de Rust ni Go, así que la granja no puede construir NINGUNA receta Rust o Go:

O sea que el 76% del catálogo sólo puede construirse en el hub, que es una sola máquina — justo lo que el respaldo del 2026-08-07 intentaba dejar de asumir.

Lo que esto explica hacia atrás

Todos los «no construyó» de Go/Rust en el worker durante esta campaña —amp, anew, apko, atlas, bom, broot, cargo-audit, cargo-hack, git-absorb— eran la misma causa, y yo la atribuí primero a «necesitan red para sus módulos». La red no tenía nada que ver: falta el binario.

Y no rompe la hermeticidad arreglarlo

cargo/go aquí son herramientas de FETCH, del mismo orden que git para clonar un repo: se usan para traer y fijar las deps ANTES del build, no dentro del sandbox. Instalarlos en la imagen golden del worker no relaja el aislamiento del build — el build sigue ocurriendo en la caja con el árbol ya vendoreado. Es exactamente lo contrario del caso «no engordar el rootfs del worker», que habla del rootfs DEL SANDBOX.

⇒ Es una decisión de aprovisionamiento, no de arquitectura, y desbloquea tres cuartas partes del catálogo para la granja.