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>
113 lines
5.3 KiB
Markdown
113 lines
5.3 KiB
Markdown
# 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](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/<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.
|
|
|
|
## 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<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).
|