Files
takana/docs/24-arbol-de-fuentes.md
SergioandClaude Opus 5 008dd3925e renombre: los instaladores pasan a takana-* — y el peligro no era el fichero, era el PROTOCOLO
ADR 0016 los listaba entre los CONGELADOS; el usuario pidió descongelarlos al abrir el SDD 28. La
enmienda queda escrita en el propio ADR, que si no la etiqueta deja de describir el hecho.

`hammer-install.sh` → `takana-install.sh`, `hammer-live-install.sh` → `takana-live-install.sh`,
`hammer-banner.txt` → `takana-banner.txt`, `BRIEFING-hammer.md` → `BRIEFING-takana.md`.

**Lo que hacía caro esto no es el nombre del fichero.** El instalador se inyecta en el ISO como
`/usr/bin/hammer-install` y su éxito se detecta con un `grep` de `HAMMER-INSTALL-OK` desde TRES
scripts de prueba. Renombrar un solo lado los deja casando NADA — sin fallar —, que es literalmente
el modo en que `atribuir-fallos.py` quedó mudo cuando el renombre movió el target de `tracing`.
Se renombraron las dos puntas en el mismo commit (`/usr/bin/takana-install`,
`TAKANA-INSTALL-OK/FAIL`, `TAKANA_INSTALL_*`, `work/takana-install.img`, `/run/takana-install`),
se comprobó por `grep` que no quede ningún token viejo fuera del ADR, y —lo que decide— se CORRIÓ
`install-tui-test.sh`: 4/4 casos verdes.

Las tres `TAKANA_INSTALL_*` caen al nombre viejo (`${TAKANA_INSTALL_X:-${HAMMER_INSTALL_X:-}}`):
el llamador puede ser un ISO anterior al renombre. Misma convención que `TAKANA_ROOT_PW` unas
líneas más arriba en ese mismo script.

NO se tocó `/usr/sbin/hammer-recover` ni su hook de arranque —renombrarlo rompe máquinas YA
INSTALADAS, no el repo—: sobrevive intacto dentro del script renombrado, verificado por conteo
antes y después (8 ocurrencias). Tampoco `hammerd`, `hammer-edit` (su `name` está en la ruta del
store), `/var/lib/hammer`, `HAMMER_LIVE` ni los siete literales de hash.

De paso: `scripts/.hammer-banner.txt.kate-swp` era un swap de editor commiteado por error. Fuera.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
2026-09-09 22:54:13 +00:00

16 KiB
Raw Permalink Blame History

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 <nombre>-<sha16> para tarballs y <nombre>-<commit-completo> 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/<dep>/outputSome 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/<fuente_id>-<nombre>/     este SDD lo nombra como capa
       fuente_id = blake3( source_id ⧺ Σ blake3(patch_i) ⧺ marca_vendor )
       source_id cubre ambos modos: "git:<commit>" XOR "tarball:<sha256>"

BUILD (sandbox, N en paralelo, cero locks)
  bwrap monta /src = overlay( lower: fuente sellada, upper+work: work/ws/<uuid>/ )
  configure → compile → install → seal artefacto → destruir ws/<uuid>
  • 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 <rootfs> [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/<uuid>/{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/<fuente_id>). 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-3takana hash --check corpus completo: hash vigente idéntico pre/post migración.
  • IF-4kill -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 (R1R3, R6R7, 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/<uuid>/{upper,work} (R4R5), 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/<uuid> 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-4sources-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

  • D1go 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) FALLAPermission denied
echo y > /mnt/sub/nuevo.o (crear dentro de un subdir del lower) FALLAPermission 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:

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
<nombre>-<sha16> (tarball) / <nombre>-<commit> (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