# SDD 24 — El árbol de fuentes: caché sellada + workspace efímero (v2) > **Resuelve ADR 0012.** Estado propuesto: ACEPTADO al cerrar F2.5. > **v2, 2026-08-28:** incorpora la verificación contra el código (§8): R1 e IF-1/IF-2 corregidos > por medición, segunda carrera nombrada, Q1 cerrada por precedente, tensión NG-4/Q3 resuelta. --- ## 0. El problema, medido — son DOS carreras `work/sources/…` cumple dos papeles que se contradicen bajo concurrencia: 1. **Caché direccionada por contenido**: dos builds de la misma dep resuelven al MISMO directorio. (El nombre es `-` para tarballs y `-` para git — `fetch.rs:234` / `fetch.rs:73`.) 2. **Workspace mutable de build**: patches, `cargo vendor`, `go mod vendor` (minutos, decenas de GB), y los residuos que las recetas dejan EN EL ÁRBOL — `.zwrap/cc` lo escriben **131 recetas** *(medido)*, casi todas de la cola KDE. Las dos carreras concretas: - **Carrera A — fetch vs build**: un `remove_dir_all` de re-materialización barre un árbol que un `bwrap` está compilando. Medida a escala: al invalidar `libdrm`, ~93/205 recetas KDE murieron con `/src/.zwrap/cc is not a full path…`. - **Carrera B — build vs build**: dos recetas DISTINTAS que comparten una dep transitiva no sellada la construyen en el MISMO `sources//output` ⇒ `Some other Meson process is already using this build directory`. Documentada en `build-farm.sh:82-99`; hoy se concilia con un **reintento serial** (Fase 1b) que existe sólo por esto. Un lock por árbol (opción A del ADR) NO la resuelve sin serializar. Mitigación vigente: `flock` global + `JOBS=1` ⇒ la granja entera corre serializada. **La tesis:** los dos papeles se **separan por capa**, con el corte puesto para que lo caro (vendor) caiga del lado inmutable. Es el mismo mecanismo que el sandbox ya usa para las build-deps (`--overlay-src` RO apiladas), extendido a `/src`. Mata las dos carreras: A porque lo sellado es inmutable y la re-materialización es imposible (mismo `fuente_id` ⇒ ya existe); B porque cada constructor tiene su propio `output/` en su upper privado. --- ## 1. Diseño ### 1.1 Las dos capas ``` MATERIALIZACIÓN (host, una vez por fuente_id, lock corto por-hash) fetch (red) → patch → vendor (cargo/go) ← YA ocurre todo en host hoy (lib.rs:224,248,258); → sellar árbol en work/sources-cas/-/ este SDD lo nombra como capa fuente_id = blake3( source_id ⧺ Σ blake3(patch_i) ⧺ marca_vendor ) source_id cubre ambos modos: "git:" XOR "tarball:" BUILD (sandbox, N en paralelo, cero locks) bwrap monta /src = overlay( lower: fuente sellada, upper+work: work/ws// ) configure → compile → install → seal artefacto → destruir ws/ ``` - **El vendoreo se paga una vez** y queda del lado inmutable: `marca_vendor` deriva de los lockfiles que ya viven en el `source_id` ⇒ la fuente sellada es determinista dado el pin. - **`.zwrap` y todo residuo viven en el upper privado.** Las 131 recetas quedan estructuralmente a salvo. - **La fase `patch` NO migra: ya corre en host** desde el día 1. Este SDD nombra como contrato lo que hoy es de facto. La tabla de fases del **SDD 02 §4 está desactualizada** (dice "patch en sandbox") y corregirla es entregable de F1. ### 1.2 Gotchas de bwrap verificados (citar al implementar) - `--overlay` exige ≥1 `--overlay-src` previo, y el acumulador **se consume por operación**. Como `sandbox.rs` ya emite `--overlay-src [deps…] --tmp-overlay /`, el grupo de `/src` va **después** y no debe filtrar srcs al overlay del root. Orden de argumentos, no diseño. **Verificado end-to-end** (§8.2): la composición real de los dos grupos funciona y no filtra. - El límite de una página del `lowerdir` (`sandbox.rs:347-357`, el pre-merge `cp -al`) **no aplica**: `/src` es un mount aparte con su propia lowerdir. - `--overlay RWSRC WORKDIR DEST`: exige `upper` **y** `work` vacíos en el mismo filesystem. ### 1.3 Por qué no las otras opciones del ADR - **(A)** lock por árbol durante todo el build: serializa el diamante qtbase y **no resuelve la carrera B**. - **(B)** árbol privado por constructor: paga el vendor completo en cada build. - **(C)** caché inmutable + copia: paga la copia por build. Este diseño es (C) con overlay en vez de copia y el vendor movido al lado sellado. --- ## 2. Reglas - **R1** — La fuente sellada es inmutable y direccionada: `fuente_id = blake3(source_id ⧺ Σ blake3(patch) ⧺ marca_vendor)`. Vive en `work/sources-cas/`. **La inmutabilidad la da el mount, no los bits de modo**: a través del overlay es estructuralmente imposible escribir el lower. Los modos del árbol se dejan **tal como los trae upstream** — sellarlos a `a-w` los hereda el copy-up y **rompe el build** (medido: el sandbox corre uid 1001 sin `CAP_DAC_OVERRIDE`; EACCES al reescribir un fichero existente y al crear dentro de un subdir; el fallo es parcial-por-profundidad porque el modo del upper root gana en la raíz). Blindaje contra procesos del host: proteger el **directorio contenedor** `sources-cas/`, nunca el árbol. La detección de corrupción es `blake3_arbol` (R10). - **R2** — Sellado atómico: materializar en `tmp/`, escribir marker, `rename`. Jamás visible un árbol parcial bajo su nombre final. - **R3** — Marker `.sellada` = `{fuente_id, blake3_arbol, ts}`. Directorio sin marker = no sellado (lección del vacío-como-cache-hit: *un ausente falla ruidosamente; un vacío llega hasta el final diciendo que todo fue bien* — CLAUDE.md regla 3). - **R4** — Nada escribe en una fuente sellada. El build la recibe sólo como lower del overlay. - **R5** — Workspace efímero por constructor: `work/ws//{upper,work}` (los dos directorios que `--overlay` exige, mismo filesystem), destruido tras el seal. `--keep-ws` lo conserva. - **R6** — Lock **sólo** durante la materialización, por `fuente_id` (`flock work/locks/`). El segundo llegador espera y encuentra sellado. Los builds no toman locks. - **R7** — Fallo o kill durante materialización ⇒ sólo queda `tmp/`; la corrida siguiente lo barre y re-materializa. Nunca se sella parcial. - **R8** — La adopción **no mueve ningún `artifact_hash`**: `fuente_id` no entra en `hash_inputs`. Verificación pre-merge: `takana hash --check` sobre el corpus, cero movidos. - **R9** — GC de `sources-cas` por alcanzabilidad desde recetas activas; lo no alcanzable es podable (regenerable desde el mirror ADR 0013). Medir por contenido, nunca con `du`. - **R10** — Re-materializar un `fuente_id` existente debe reproducir el `blake3_arbol` del marker o fallar ruidoso (detecta vendor no determinista o mirror envenenado). - **R11** — El `flock` global (CLAUDE.md regla 1) y el `JOBS=1` se retiran **sólo** tras F2.5 verde, en el mismo commit. ## 3. Invariantes (mecánicamente verificables) - **IF-1** — Tras N builds concurrentes sobre la misma fuente, el `blake3_arbol` del árbol sellado casa con el del marker. Verificable **sin fanotify** (que exige `CAP_SYS_ADMIN` y complica la granja): hashear antes/después es más barato y más fuerte. - **IF-2** — Canario deliberado (estilo D9 de harkaq): un build de test **escribe** en `/src` (debe FUNCIONAR, vía copy-up al upper) y el árbol sellado **sigue idéntico** por `blake3_arbol`. Si el hash se movió, alguien montó el lower como bind RW. No se cree: se gana. - **IF-3** — `takana hash --check` corpus completo: hash vigente idéntico pre/post migración. - **IF-4** — `kill -9` en medio de la materialización ⇒ no existe marker; la corrida siguiente sella limpio y `blake3_arbol` casa con una materialización sin interrupción. - **IF-5** — Artefacto sobre overlay ≡ bit-a-bit al del modelo viejo, **una receta por clase y por build-system**: autotools-en-raíz (zlib), **meson y cmake (escriben en subdirs — la clase que un spike sólo-zlib dejaría sin cubrir)**, cargo, go. - **IF-6** — Tras F3, `work/sources/` no existe, la **Fase 1b de `build-farm.sh` está borrada**, y ningún script referencia el esquema viejo — verificado reconstruyendo, no con `grep` (en este repo `grep` sobre ficheros con comentarios densos no es una medición). ## 4. Fases - **F0 — gate residual**: el mecanismo overlay ya está verde (verificado 2026-08-28 con bwrap 0.11.2: lower legible, escritura al upper, lower intacto, y la composición con el `--tmp-overlay /` del root — §8). Queda **sólo IF-5**, y con el corpus de clases completo — el caso que muerde es meson/cmake en subdirs, no zlib. - **F1 — materialización**: `fetch.rs` produce fuentes selladas (R1–R3, R6–R7, R10). El build sigue en el modelo viejo (puente). **Incluye corregir la tabla de fases del SDD 02 §4.** - **F2 — build sobre overlay**: `/src` = lower sellado + `ws//{upper,work}` (R4–R5), respetando §1.2. Detrás de `SOURCES_CAS=1`, **inerte sin él**. IF-3 + IF-5. - **F2.5 — vertical slice**: worker real con `JOBS=4` drena una onda KDE que atraviesa el diamante qtbase. Todo `✓ REPRODUCIBLE`, IF-1 e IF-2 corridos. **Gate del ADR:** 0012 pasa a ACEPTADO aquí o el diseño vuelve. - **F3 — migración**: borrar `work/sources/`; **borrar la Fase 1b de `build-farm.sh`** (entregable medible: la carrera B ya no existe); **reescribir — no re-apuntar — el watchdog de disco de `farm-worker-loop.sh:43`** (queda más simple: lo sellado es inmutable, cada `ws/` tiene un solo dueño); actualizar `reconstruir-corpus.sh:206`; retirar flock global + `JOBS=1` (R11); GC (R9) al latido. - **F4 — cosecha**: medir el speedup real de la campaña de reconstrucción con JOBS=N. El número cierra el informe del ADR. ## 5. No-objetivos - **NG-1** — No cambia el sandbox de build ni sus fases internas. - **NG-2** — No toca `hash_inputs` ni re-hashea nada (R8 lo verifica). - **NG-3** — No resuelve la red del vendor de Go en host (→ D1). - **NG-4** — `sources-cas` NO es procedencia ni parte del store: es caché **podable**. Q3 cita `fuente_id` en el log de transparencia, y eso es compatible **porque la cita es a un hash cuya materialización es regenerable desde el mirror (ADR 0013)**: podar no rompe la cita, la vuelve un puntero frío. Lo que NUNCA se cita es una ruta de `sources-cas`. - **NG-5** — No introduce dependencia de reflink ni de overlayfs del host: el mecanismo es el `--overlay` de bwrap ya en uso para deps. ## 6. Deudas nombradas - **D1** — `go mod vendor` sigue en host con red. Acotado a materialización; R10 lo hace detectable. Cierre futuro: materialización en sandbox con red controlada. - **D2** — Confirmar en F2.5 que los kernels propios de granja (SDD 22) montan el `--overlay` de bwrap con lower en `sources-cas`. Fallback: copia completa — correcta, lenta, **ruidosa en el log**, nunca silenciosa. - **D3** — `--keep-ws` acumula workspaces; podarlos en el GC de R9. - **D4** — El snapshot golden debe hornear `sources-cas` o los primeros builds pagan materialización fría. **Choca con el test de `takana-bootstrap` (`lib.rs:2659`) que afirma que `work/sources/` no viaja en el builder rootfs**: la decisión (¿el builder hereda `sources-cas`?) se toma en F2.5 con el dato del slice, y si es sí, ese test se actualiza a propósito — no se deja fallar por sorpresa. ## 7. Preguntas — estado - **Q1 — CERRADA por precedente**: fetch+patch+vendor corren en host desde el día 1 (`lib.rs:224,248,258`). El riesgo ya se corre hoy; este SDD sólo lo pone bajo contrato (R10 lo hace además detectable). No es una apuesta nueva. - **Q2 — abierta**: retención de `sources-cas` con vendors grandes: ¿tope GB con evicción LRU o sólo alcanzabilidad? Decide el operador con el dato de F4. - **Q3 — DECIDIDA: sí** — `fuente_id` entra en `bootstrap.json` como campo opcional. Ver NG-4 para por qué no contradice la podabilidad. --- *Cruces: ADR 0012 (resuelve) · ADR 0013 (regenerabilidad que sostiene NG-4/Q3) · SDD 02 §4 (overlay-src de deps: el mecanismo reutilizado; tabla de fases a corregir en F1) · SDD 16 (patrón "inerte sin flag"; canario D9 → IF-2) · CLAUDE.md regla 3 (marker contra vacíos) · CLAUDE.md regla 1 (retiro del flock en R11) · `build-farm.sh` Fase 1b (carrera B; se borra en F3) · `docs/BRIEFING-takana.md` §14.4 (los modos de fallo estructurales que R3 e IF-6 citan).* --- # 8. Registro de evidencia — mediciones contra el código (2026-08-28) > Las correcciones que salieron de aquí **ya están incorporadas en el cuerpo de la v2**. Esta > sección queda como registro reproducible: qué se midió, con qué comando y con qué salida, para > que nadie tenga que volver a descubrirlo. Método del SDD 22: un diseño entrante se contesta con > evidencia, y si un dato de takana lo contradice, manda takana. ## 8.1 El `chmod a-w` recursivo rompe el build (origen de R1) El sandbox corre como **uid 1001 sin `CAP_DAC_OVERRIDE`** (`id` dentro de bwrap lo confirma) y **overlayfs preserva el modo del lower al hacer copy-up**. Con el árbol sellado a `a-w` (dirs 0555, ficheros 0444), sobre patrones reales de fase: | patrón de build | resultado | |---|---| | `echo >> /mnt/configure` (reescribir fichero existente) | **FALLA** — `Permission denied` | | `echo y > /mnt/sub/nuevo.o` (crear dentro de un subdir del lower) | **FALLA** — `Permission denied` | | `sed -i` en la **raíz** del overlay | OK | | `mkdir /mnt/output` (patrón meson) en la **raíz** | OK | Las dos últimas pasan porque el upper root ya existe con modo 0755 y su modo gana sobre el del lower root ⇒ **el fallo es parcial y por profundidad**: funciona arriba y falla adentro. Un spike sobre `zlib` (autotools que configura en la raíz) habría pasado en verde y el diseño habría reventado después en meson/cmake/cargo. Es la forma del fallo *sellado ≠ arranca*. De ahí R1 (inmutabilidad por mount, modos upstream) e IF-5 (una receta por build-system, no sólo zlib). ## 8.2 La composición real con el sandbox actual funciona (origen de §1.2 y del gate de F0) Reproduciendo el orden de argumentos que `sandbox.rs` ya emite, más el grupo de `/src`: ```sh bwrap --unshare-all \ --overlay-src .dev-fs/alpine --tmp-overlay / \ --overlay-src $S/lower --overlay $S/upper $S/work /src \ --proc /proc --dev /dev -- /bin/sh -c '…' ``` ``` root = rootfs alpine: bin dev etc home lib media mnt musl256 opt proc root run sbi /src = fuente sellada: SOY-LA-FUENTE-SELLADA escritura en SUBDIR de /src: OK ← /src/sub/output/build.ninja (el caso meson) raiz sigue escribible (tmp-overlay): OK lower intacto: marca.txt, sub/f.c ← el árbol sellado no se tocó upper del /src: sub/output/build.ninja ← el residuo quedó aislado ``` Cuatro hechos de una sola corrida: (1) el acumulador de `--overlay-src` **se consume por operación y no filtra** — el root sigue siendo el rootfs Alpine y `/src` la fuente sellada; (2) el `--tmp-overlay /` del root sigue funcionando junto al `/src`; (3) con modos upstream, **escribir en un subdirectorio de `/src` funciona** — exactamente el caso que §8.1 rompía; (4) el lower queda byte-intacto y el residuo aislado en el upper. ⇒ El gate de *mecanismo* de F0 está verde. Lo que queda de F0 es IF-5 (bit-a-bit real). ## 8.3 Referencias de código verificadas | Afirmación del SDD | Dónde | Estado | |---|---|---| | `-` (tarball) / `-` (git) | `fetch.rs:234` / `fetch.rs:73` | ✅ | | fetch+patch+vendor corren en host, antes de bwrap | `lib.rs:224,248,258` | ✅ (cierra Q1) | | Límite de página del `lowerdir` y pre-merge `cp -al` | `sandbox.rs:347-357` | ✅ | | Carrera B y su reintento serial (Fase 1b) | `build-farm.sh:82-99` | ✅ | | Watchdog acoplado al esquema de rutas | `farm-worker-loop.sh:43` | ✅ | | Purga del árbol viejo | `reconstruir-corpus.sh:206` | ✅ | | `work/sources/` no viaja en el builder rootfs (test) | `takana-bootstrap/src/lib.rs:2659` | ✅ (origen de D4) | | `.zwrap` escrito en el árbol de fuentes | 131 recetas *(medido)* | ✅ | | `--overlay RWSRC WORKDIR DEST` disponible | bubblewrap 0.11.2 | ✅ | | La tabla de fases dice "patch en sandbox" | `docs/02-build-lab.md` §4 | ⚠️ desactualizada — F1 |