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
16 KiB
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:
- 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.) - 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/cclo escriben 131 recetas (medido), casi todas de la cola KDE.
Las dos carreras concretas:
- Carrera A — fetch vs build: un
remove_dir_allde re-materialización barre un árbol que unbwrapestá compilando. Medida a escala: al invalidarlibdrm, ~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>/output⇒Some other Meson process is already using this build directory. Documentada enbuild-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_vendorderiva de los lockfiles que ya viven en elsource_id⇒ la fuente sellada es determinista dado el pin. .zwrapy todo residuo viven en el upper privado. Las 131 recetas quedan estructuralmente a salvo.- La fase
patchNO 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)
--overlayexige ≥1--overlay-srcprevio, y el acumulador se consume por operación. Comosandbox.rsya emite--overlay-src <rootfs> [deps…] --tmp-overlay /, el grupo de/srcva 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-mergecp -al) no aplica:/srces un mount aparte con su propia lowerdir. --overlay RWSRC WORKDIR DEST: exigeupperyworkvací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 enwork/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 aa-wlos hereda el copy-up y rompe el build (medido: el sandbox corre uid 1001 sinCAP_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 contenedorsources-cas/, nunca el árbol. La detección de corrupción esblake3_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--overlayexige, mismo filesystem), destruido tras el seal.--keep-wslo 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_idno entra enhash_inputs. Verificación pre-merge:takana hash --checksobre el corpus, cero movidos. - R9 — GC de
sources-caspor alcanzabilidad desde recetas activas; lo no alcanzable es podable (regenerable desde el mirror ADR 0013). Medir por contenido, nunca condu. - R10 — Re-materializar un
fuente_idexistente debe reproducir elblake3_arboldel marker o fallar ruidoso (detecta vendor no determinista o mirror envenenado). - R11 — El
flockglobal (CLAUDE.md regla 1) y elJOBS=1se 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_arboldel árbol sellado casa con el del marker. Verificable sin fanotify (que exigeCAP_SYS_ADMINy 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 porblake3_arbol. Si el hash se movió, alguien montó el lower como bind RW. No se cree: se gana. - IF-3 —
takana hash --checkcorpus completo: hash vigente idéntico pre/post migración. - IF-4 —
kill -9en medio de la materialización ⇒ no existe marker; la corrida siguiente sella limpio yblake3_arbolcasa 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 debuild-farm.shestá borrada, y ningún script referencia el esquema viejo — verificado reconstruyendo, no congrep(en este repogrepsobre 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.rsproduce 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/<uuid>/{upper,work}(R4–R5), respetando §1.2. Detrás deSOURCES_CAS=1, inerte sin él. IF-3 + IF-5. - F2.5 — vertical slice: worker real con
JOBS=4drena 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 debuild-farm.sh(entregable medible: la carrera B ya no existe); reescribir — no re-apuntar — el watchdog de disco defarm-worker-loop.sh:43(queda más simple: lo sellado es inmutable, cadaws/<uuid>tiene un solo dueño); actualizarreconstruir-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_inputsni re-hashea nada (R8 lo verifica). - NG-3 — No resuelve la red del vendor de Go en host (→ D1).
- NG-4 —
sources-casNO es procedencia ni parte del store: es caché podable. Q3 citafuente_iden 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 desources-cas. - NG-5 — No introduce dependencia de reflink ni de overlayfs del host: el mecanismo es el
--overlayde bwrap ya en uso para deps.
6. Deudas nombradas
- D1 —
go mod vendorsigue 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
--overlayde bwrap con lower ensources-cas. Fallback: copia completa — correcta, lenta, ruidosa en el log, nunca silenciosa. - D3 —
--keep-wsacumula workspaces; podarlos en el GC de R9. - D4 — El snapshot golden debe hornear
sources-caso los primeros builds pagan materialización fría. Choca con el test detakana-bootstrap(lib.rs:2659) que afirma quework/sources/no viaja en el builder rootfs: la decisión (¿el builder heredasources-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-cascon vendors grandes: ¿tope GB con evicción LRU o sólo alcanzabilidad? Decide el operador con el dato de F4. - Q3 — DECIDIDA: sí —
fuente_identra enbootstrap.jsoncomo 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:
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 |