1# SDD 03 — Hidratación y store
2
3La hidratación es el puente del sótano al piso de arriba: cómo un artefacto del store
4content-addressed termina siendo un `/bin/grep` normal, mutable, en un FHS clásico — sin
5arrastrar la ruta del store grabada a fuego, y sin el infierno de symlinks de Nix.
6
7## 1. El store
8
9```
10/store/<blake3>-<name>/ un artefacto sellado e inmutable
11 bin/grep
12 share/...
13```
14
15- **Inmutable y append-only.** Nada modifica un artefacto sellado. Reconstruir produce un
16 hash nuevo, una entrada nueva.
17- **Direccionado por contenido.** El prefijo es el `artifact_hash` (ver [SDD 02](02-build-lab.md)).
18- **Deduplicado.** Dos recetas que producen el mismo árbol comparten entrada.
19- **GC por alcanzabilidad.** Un artefacto es basura si ningún hardlink del FHS ni ningún pin
20 lo referencia. `hammer gc` libera lo inalcanzable.
21
22## 2. El problema que resolvemos
23
24Un binario compilado en un entorno funcional lleva grabado:
25
26- el **intérprete ELF** (el loader de la libc): p. ej. `/store/<hash>/lib/ld-musl-x86_64.so.1`,
27- el **RPATH** (dónde busca las `.so` dinámicas): rutas dentro del store.
28
29Si copias ese binario tal cual a `/bin`, falla: busca la libc en una ruta del store. Hay que
30**normalizar** las rutas al esquema clásico antes de que toque el FHS real.
31
32## 3. Estrategia primaria: enlazado estático (la vía limpia)
33
34Con musl, el camino de oro es **enlazar todo estáticamente** (`link = "static"`). El resultado
35es un único binario autosuficiente, sin `.so`, sin intérprete externo, sin RPATH que importe.
36
37- **Hidratar = hardlink directo** del binario del store a `/bin`/`/sbin`.
38- **Estructura familiar:** `/bin` real lleno de ejecutables normales; cero symlinks crípticos.
39- **Control total:** ¿reemplazar `ls` por una versión experimental compilada a mano? Copias el
40 binario encima y listo (ver §6, copy-on-write).
41
42Esta es la estrategia por defecto.
43
44## 4. Estrategia secundaria: purga de RPATH con `patchelf` (la vía dinámica)
45
46Cuando un paquete requiere enlazado dinámico (plugins, tooling específico), el lab compila
47contra el store, y la hidratación **reescribe el binario** antes de inyectarlo:
48
491. `patchelf --set-interpreter /lib/ld-musl-x86_64.so.1 <bin>` — loader estándar del sistema.
502. `patchelf --set-rpath /lib:/usr/lib <bin>` — rutas clásicas.
513. Hardlink/copia del binario normalizado y de sus `.so` a `/lib`.
52
53`patchelf` (irónicamente, creado por el equipo de Nix) es la herramienta exacta para esto.
54
55**Implementado** (`hammer-build::hydrate`, `LinkMode::Dynamic` + `DynamicSpec{interpreter,rpath}`):
56el walker detecta ELF por magic (`\x7fELF`) y, si el `DynamicSpec` trae interpreter y/o
57rpath, **copia** ese binario (no lo hardlinkea: patchelf lo reescribiría y mutaría el inode del
58store) a un `.hammer-tmp`, lo parchea con `patchelf --set-interpreter/--set-rpath` y lo renombra
59atómicamente. Los no-ELF (y los ELF cuando el spec está vacío) siguen hardlinkeándose como en
60estático. Si falta `patchelf` en el PATH se falla limpio **antes** de tocar el FHS
61(`ensure_patchelf`). `HydrateReport.patched` cuenta los binarios reescritos.
62
63## 5. El core dinámico curado (matiz honesto sobre el ABI de musl)
64
65La idea de "actualizar el sustrato atómicamente y que los binarios no se enteren" **sólo es
66segura si se versiona explícitamente.** musl **no garantiza estabilidad de ABI entre
67versiones**; confiar ciegamente en ello rompería binarios.
68
69Política de `hammer`:
70
71- El grueso del sistema se enlaza **estático** → inmune al problema.
72- Un subconjunto hiper-reducido de librerías core de actualización frecuente por seguridad
73 (p. ej. `openssl`, `zlib`) puede vivir **dinámico** en una ruta controlada `/lib/core/`,
74 **versionada** (`/lib/core/libssl.so.3`).
75- Las actualizaciones de ese core respetan el `soname`/versión. Si una actualización cambia el
76 ABI, **es un artefacto nuevo con soname nuevo**, no un reemplazo silencioso.
77- Para plugins reales, preferir `dlopen` versionado sobre fe en el ABI.
78
79Resultado: el superpoder de "reemplazar el suelo" existe, pero acotado y honesto, sin la
80promesa mágica que rompería en producción.
81
82## 6. Mutabilidad: copy-on-write sobre hardlinks
83
84El FHS está hidratado por **hardlinks** al store (no symlinks → rutas reales; no copias →
85dedup). El store es inmutable; el FHS es mutable. ¿Cómo conviven?
86
87- **Leer/ejecutar:** el hardlink apunta al inode del store. Cero copia.
88- **Escribir/pisar en caliente:** al reemplazar `/bin/grep`, **se rompe el hardlink** (se
89 escribe un inode nuevo). El artefacto del store queda intacto → base de rollback.
90- **Rollback:** re-hidratar desde el store restaura el hardlink original.
91
92Esto da exactamente lo buscado: "un `rm -rf` rompe cosas de verdad", pero siempre hay una base
93limpia reconstruible debajo.
94
95## 7. Interfaz
96
97```rust
98// hammer-core
99pub struct Store { root: PathBuf }
100impl Store {
101 pub fn has(&self, h: &ArtifactHash) -> bool;
102 pub fn seal(&self, out_dir: &Path, h: ArtifactHash) -> Result<PathBuf>;
103 pub fn path_of(&self, h: &ArtifactHash) -> PathBuf;
104 pub fn gc(&self, roots: &[ArtifactHash]) -> Result<GcReport>;
105}
106
107// hammer-build
108pub fn hydrate(h: &ArtifactHash, store: &Store, target_fhs: &Path, mode: LinkMode)
109 -> Result<Vec<HydratedFile>>;
110```
111
112CLI: `hammer hydrate <hash> [--into /]` — proyecta el artefacto al FHS (real o de overlay).
03-hydration.md
SDD 03 — Hidratación y store
La hidratación es el puente del sótano al piso de arriba: cómo un artefacto del store
content-addressed termina siendo un /bin/grep normal, mutable, en un FHS clásico — sin
arrastrar la ruta del store grabada a fuego, y sin el infierno de symlinks de Nix.
1. El store
/store/<blake3>-<name>/ un artefacto sellado e inmutable
bin/grep
share/...
- Inmutable y append-only. Nada modifica un artefacto sellado. Reconstruir produce un
hash nuevo, una entrada nueva.
- Direccionado por contenido. El prefijo es el
artifact_hash (ver SDD 02).
- Deduplicado. Dos recetas que producen el mismo árbol comparten entrada.
- GC por alcanzabilidad. Un artefacto es basura si ningún hardlink del FHS ni ningún pin
lo referencia.
hammer gc libera lo inalcanzable.
2. El problema que resolvemos
Un binario compilado en un entorno funcional lleva grabado:
- el intérprete ELF (el loader de la libc): p. ej.
/store/<hash>/lib/ld-musl-x86_64.so.1,
- el RPATH (dónde busca las
.so dinámicas): rutas dentro del store.
Si copias ese binario tal cual a /bin, falla: busca la libc en una ruta del store. Hay que
normalizar las rutas al esquema clásico antes de que toque el FHS real.
3. Estrategia primaria: enlazado estático (la vía limpia)
Con musl, el camino de oro es enlazar todo estáticamente (link = "static"). El resultado
es un único binario autosuficiente, sin .so, sin intérprete externo, sin RPATH que importe.
- Hidratar = hardlink directo del binario del store a
/bin//sbin.
- Estructura familiar:
/bin real lleno de ejecutables normales; cero symlinks crípticos.
- Control total: ¿reemplazar
ls por una versión experimental compilada a mano? Copias el
binario encima y listo (ver §6, copy-on-write).
Esta es la estrategia por defecto.
4. Estrategia secundaria: purga de RPATH con patchelf (la vía dinámica)
Cuando un paquete requiere enlazado dinámico (plugins, tooling específico), el lab compila
contra el store, y la hidratación reescribe el binario antes de inyectarlo:
patchelf --set-interpreter /lib/ld-musl-x86_64.so.1 <bin> — loader estándar del sistema.
patchelf --set-rpath /lib:/usr/lib <bin> — rutas clásicas.
- Hardlink/copia del binario normalizado y de sus
.so a /lib.
patchelf (irónicamente, creado por el equipo de Nix) es la herramienta exacta para esto.
Implementado (hammer-build::hydrate, LinkMode::Dynamic + DynamicSpec{interpreter,rpath}):
el walker detecta ELF por magic (\x7fELF) y, si el DynamicSpec trae interpreter y/o
rpath, copia ese binario (no lo hardlinkea: patchelf lo reescribiría y mutaría el inode del
store) a un .hammer-tmp, lo parchea con patchelf --set-interpreter/--set-rpath y lo renombra
atómicamente. Los no-ELF (y los ELF cuando el spec está vacío) siguen hardlinkeándose como en
estático. Si falta patchelf en el PATH se falla limpio antes de tocar el FHS
(ensure_patchelf). HydrateReport.patched cuenta los binarios reescritos.
5. El core dinámico curado (matiz honesto sobre el ABI de musl)
La idea de "actualizar el sustrato atómicamente y que los binarios no se enteren" sólo es
segura si se versiona explícitamente. musl no garantiza estabilidad de ABI entre
versiones; confiar ciegamente en ello rompería binarios.
Política de hammer:
- El grueso del sistema se enlaza estático → inmune al problema.
- Un subconjunto hiper-reducido de librerías core de actualización frecuente por seguridad
(p. ej.
openssl, zlib) puede vivir dinámico en una ruta controlada /lib/core/,
versionada (/lib/core/libssl.so.3).
- Las actualizaciones de ese core respetan el
soname/versión. Si una actualización cambia el
ABI, es un artefacto nuevo con soname nuevo, no un reemplazo silencioso.
- Para plugins reales, preferir
dlopen versionado sobre fe en el ABI.
Resultado: el superpoder de "reemplazar el suelo" existe, pero acotado y honesto, sin la
promesa mágica que rompería en producción.
6. Mutabilidad: copy-on-write sobre hardlinks
El FHS está hidratado por hardlinks al store (no symlinks → rutas reales; no copias →
dedup). El store es inmutable; el FHS es mutable. ¿Cómo conviven?
- Leer/ejecutar: el hardlink apunta al inode del store. Cero copia.
- Escribir/pisar en caliente: al reemplazar
/bin/grep, se rompe el hardlink (se
escribe un inode nuevo). El artefacto del store queda intacto → base de rollback.
- Rollback: re-hidratar desde el store restaura el hardlink original.
Esto da exactamente lo buscado: "un rm -rf rompe cosas de verdad", pero siempre hay una base
limpia reconstruible debajo.
7. Interfaz
// hammer-core
pub struct Store { root: PathBuf }
impl Store {
pub fn has(&self, h: &ArtifactHash) -> bool;
pub fn seal(&self, out_dir: &Path, h: ArtifactHash) -> Result<PathBuf>;
pub fn path_of(&self, h: &ArtifactHash) -> PathBuf;
pub fn gc(&self, roots: &[ArtifactHash]) -> Result<GcReport>;
}
// hammer-build
pub fn hydrate(h: &ArtifactHash, store: &Store, target_fhs: &Path, mode: LinkMode)
-> Result<Vec<HydratedFile>>;
CLI: hammer hydrate <hash> [--into /] — proyecta el artefacto al FHS (real o de overlay).