takana etapa 4: los 10 crates de librería y el CLI pasan a takana-*

hammer-{core,build,bootstrap,overlay,journal,mirror,upgrade,agent,recover,cli}
→ takana-*, con sus deps de workspace, sus identificadores en el fuente y las
referencias -p de los scripts.

VERIFICADO que no mueve nada del corpus: `takana hash recipes/zlib.toml`
devuelve b3:dc363f26… , idéntico a antes del renombre. Los nombres de crate no
entran en hash_inputs, pero eso se comprueba, no se supone. 600 tests en verde.

DOS BINARIOS SE CONGELAN, y no por prolijidad:

- `hammerd` — paquete Y binario. Es componente de Stage 1 de la distro (musl,
  busybox, hammerd, arje-zero), lo supervisa arje-zero en el sistema arrancado,
  `PRESEED=hammerd` lo nombra en selfhost-verify y sus bytes anclan el baseline
  of_tree. El nombre del crate va en los símbolos ⇒ renombrarlo mueve los bytes.

- `hammer-recover` — el PAQUETE se renombra a takana-recover, el BINARIO no.
  hammer-live-install.sh lo copia a /usr/sbin/hammer-recover en sistemas ya
  instalados y hornea un hook de arranque que lo invoca por ese nombre:
  renombrarlo rompe máquinas instaladas, no el repo.

Consecuencia que hay que anotar igual: al renombrar hammer-core, los bytes de
hammerd cambian de todos modos porque linkea contra un crate con otro nombre.
El baseline of_tree del selfhost hay que rehacerlo — es efecto de la etapa 4,
no de un cambio de hammerd.

Las referencias en comentarios de recetas y docs (rutas hammer-core/src/…)
quedan para la etapa 5: son texto, no mueven hash.
This commit is contained in:
Sergio
2026-09-09 18:46:41 +00:00
parent d47cafa05d
commit 24bcf1783c
105 changed files with 943 additions and 940 deletions
@@ -0,0 +1,76 @@
//! La hidratación no escribe fuera de su root, aunque el FHS ya traiga un symlink que salga.
//!
//! EL FALLO (medido 2026-08-30, antes de `no_escapa`): hidratar es `target_fhs.join(rel)` y
//! escribir por ruta. Un artefacto que trae `usr/share/pkg → /algún/lado` deja ese symlink puesto
//! —los symlinks se replican literales, y así debe ser— y la hidratación SIGUIENTE escribía
//! `usr/share/pkg/archivo` **fuera del root**, devolviendo `Ok(1)`. Con `hammer hydrate --into`
//! sobre una imagen, eso es escribir en el sistema anfitrión diciendo que todo fue bien.
//!
//! El corpus no lo disparaba (de 160 symlinks absolutos ninguno apunta a un directorio, y de
//! ~42 000 relativos ninguno sale de su artefacto), así que esto es una mina desactivada, no un
//! incendio apagado. Por eso el test: lo que no dispara nadie tampoco lo nota nadie.
use std::path::Path;
use takana_build::hydrate::hydrate;
use takana_core::LinkMode;
/// Artefacto que trae `usr/share/pkg` como symlink a `destino`.
fn artefacto_con_symlink(dir: &Path, destino: &Path) {
std::fs::create_dir_all(dir.join("usr/share")).unwrap();
std::os::unix::fs::symlink(destino, dir.join("usr/share/pkg")).unwrap();
}
/// Artefacto que trae un fichero DEBAJO de esa misma ruta.
fn artefacto_con_fichero(dir: &Path) {
std::fs::create_dir_all(dir.join("usr/share/pkg")).unwrap();
std::fs::write(dir.join("usr/share/pkg/archivo"), b"contenido").unwrap();
}
#[test]
fn un_symlink_del_fhs_que_sale_del_root_no_deja_escribir() {
let fuera = tempfile::tempdir().unwrap();
let fhs = tempfile::tempdir().unwrap();
let a = tempfile::tempdir().unwrap();
let b = tempfile::tempdir().unwrap();
artefacto_con_symlink(a.path(), fuera.path());
hydrate(a.path(), fhs.path(), LinkMode::Static, None).unwrap();
artefacto_con_fichero(b.path());
let r = hydrate(b.path(), fhs.path(), LinkMode::Static, None);
assert!(r.is_err(), "hidratar a través del symlink tiene que FALLAR, no devolver Ok");
let msg = format!("{}", r.unwrap_err());
assert!(msg.contains("se saldría del root"), "el error tiene que decir qué pasó: {msg}");
assert!(
!fuera.path().join("archivo").exists(),
"ESCAPÓ: el fichero se escribió fuera del target_fhs"
);
}
/// El contrapeso: un symlink de directorio que se queda DENTRO tiene que seguir funcionando.
/// Es el caso de usr-merge (`/lib → usr/lib`) y el de los temas de iconos (`22@2x → 22`), o sea
/// la mayoría de los symlinks de directorio que hay en el corpus. Un guardián que los rompiera
/// sería peor que el agujero que tapa.
#[test]
fn un_symlink_que_se_queda_dentro_sigue_hidratando() {
let fhs = tempfile::tempdir().unwrap();
let a = tempfile::tempdir().unwrap();
let b = tempfile::tempdir().unwrap();
// `usr/share/pkg → ../otro` (relativo y dentro), con el destino existiendo.
std::fs::create_dir_all(a.path().join("usr/otro")).unwrap();
std::fs::write(a.path().join("usr/otro/.marca"), b"x").unwrap();
std::fs::create_dir_all(a.path().join("usr/share")).unwrap();
std::os::unix::fs::symlink("../otro", a.path().join("usr/share/pkg")).unwrap();
hydrate(a.path(), fhs.path(), LinkMode::Static, None).unwrap();
artefacto_con_fichero(b.path());
let r = hydrate(b.path(), fhs.path(), LinkMode::Static, None);
assert!(r.is_ok(), "un symlink que no sale del root no puede bloquear la hidratación: {r:?}");
assert!(
fhs.path().join("usr/otro/archivo").exists(),
"el fichero tiene que aterrizar donde apunta el symlink, dentro del root"
);
}