# 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/-/ 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](02-build-lab.md)). - **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//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: 1. `patchelf --set-interpreter /lib/ld-musl-x86_64.so.1 ` — loader estándar del sistema. 2. `patchelf --set-rpath /lib:/usr/lib ` — rutas clásicas. 3. 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 ```rust // 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; pub fn path_of(&self, h: &ArtifactHash) -> PathBuf; pub fn gc(&self, roots: &[ArtifactHash]) -> Result; } // hammer-build pub fn hydrate(h: &ArtifactHash, store: &Store, target_fhs: &Path, mode: LinkMode) -> Result>; ``` CLI: `hammer hydrate [--into /]` — proyecta el artefacto al FHS (real o de overlay).