Files
hammer/docs/03-hydration.md
SergioandClaude Opus 4.8 f6b337f6cb feat(hydrate): hidratación dinámica real con patchelf (LinkMode::Dynamic)
Deja de ser un error "pendiente": hydrate(.., DynamicSpec{interpreter,rpath})
detecta ELF por magic, copia los que necesitan parcheo (no hardlink: patchelf
mutaría el store) y aplica --set-interpreter/--set-rpath atómicamente
(copy→patch→rename); no-ELF y spec vacío siguen hardlinkeando. ensure_patchelf
falla limpio antes de tocar el FHS si falta la herramienta. HydrateReport.patched
cuenta los reescritos. Callers (bus/cli/bootstrap/e2e) pasan None=estático.
4 tests nuevos (incl. camino de error sin patchelf y real gated). Doc §4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 03:23:12 +00:00

5.3 KiB

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:

  1. patchelf --set-interpreter /lib/ld-musl-x86_64.so.1 <bin> — loader estándar del sistema.
  2. patchelf --set-rpath /lib:/usr/lib <bin> — 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.

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).