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
+19
View File
@@ -0,0 +1,19 @@
[package]
name = "takana-recover"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
description = "Mini-binario de auto-recuperación de upgrades al arranque (E4 #4b): el /sbin/init lo corre para completar/deshacer un apply interrumpido antes de incarnar."
# El PAQUETE se renombra, el BINARIO no: `/usr/sbin/hammer-recover` está copiado en
# sistemas ya instalados y `hammer-live-install.sh` hornea un hook de arranque que lo
# invoca por ese nombre. Renombrar el binario rompe máquinas instaladas. Etapa 6.
[[bin]]
name = "hammer-recover"
path = "src/main.rs"
[dependencies]
takana-upgrade.workspace = true
takana-journal.workspace = true
+73
View File
@@ -0,0 +1,73 @@
//! `hammer-recover` — auto-recuperación de upgrades al **arranque** (E4 / refinamiento #4b).
//!
//! El producto NO lleva el CLI `hammer` completo; este mini-binario (estático musl) es lo único que el
//! sistema instalado necesita para auto-sanar un upgrade interrumpido. El wrapper `/sbin/init` lo corre
//! **tras montar `/store` y `/var/lib/hammer`** y **antes** de incarnar arje-zero:
//!
//! - Sin intento pendiente ⇒ no-op (el caso normal).
//! - Con un `pending.json` (un `hammer upgrade apply` cortado por un apagón/reinicio): intenta
//! **completar** (roll-forward, re-ejecuta el plan idempotente + commitea); si no puede (p.ej. el árbol
//! ya no está en `/store`), **deshace** (roll-back) para dejar el FHS consistente. En cualquier caso el
//! sistema arranca en un estado coherente, no a medias.
//!
//! Encaja con el modelo de generaciones **in-place** (el FHS es la generación viva): no hay menú de
//! generaciones tipo NixOS que seleccionar, sino una reparación determinista del estado a medias.
//!
//! Paths del producto (overridables por entorno para tests):
//! `HAMMER_RECOVER_ROOT=/` `HAMMER_RECOVER_STORE=/store`
//! `HAMMER_RECOVER_STATE=/var/lib/hammer/upgrades` `HAMMER_RECOVER_JOURNAL=/var/lib/hammer/journal`
//! Flag: `--rollback` fuerza deshacer en vez de completar.
//!
//! NUNCA sale con código ≠0 por un fallo de recuperación: es un hook de arranque, no debe abortar el
//! boot. Reporta por consola y deja que arje-zero siga (con el estado más consistente que logró).
use std::path::PathBuf;
fn env_path(key: &str, default: &str) -> PathBuf {
std::env::var(key).map(PathBuf::from).unwrap_or_else(|_| PathBuf::from(default))
}
fn main() {
let force_rollback = std::env::args().skip(1).any(|a| a == "--rollback");
let root = env_path("HAMMER_RECOVER_ROOT", "/");
let store = env_path("HAMMER_RECOVER_STORE", "/store");
let state = env_path("HAMMER_RECOVER_STATE", "/var/lib/hammer/upgrades");
let journal_dir = env_path("HAMMER_RECOVER_JOURNAL", "/var/lib/hammer/journal");
match takana_upgrade::pending(&state) {
Ok(None) => {
println!("hammer-recover: sin upgrade interrumpido");
return;
}
Ok(Some(p)) => {
println!("hammer-recover: upgrade interrumpido (generación {}) — recuperando", p.id);
}
Err(e) => {
eprintln!("hammer-recover: no pude leer el estado de upgrades: {e}");
return;
}
}
let journal = takana_journal::Journal::open(&journal_dir).ok();
let j = journal.as_ref();
if force_rollback {
match takana_upgrade::recover(&store, &root, &state, j, true) {
Ok(r) => println!("hammer-recover: DESHECHO (generación {} descartada)", r.generation),
Err(e) => eprintln!("hammer-recover: rollback falló: {e}"),
}
return;
}
// Optimista: completar el upgrade. Si el roll-forward falla, deshacer ⇒ FHS consistente igual.
match takana_upgrade::recover(&store, &root, &state, j, false) {
Ok(r) => println!("hammer-recover: COMPLETADO (generación {})", r.generation),
Err(e) => {
eprintln!("hammer-recover: roll-forward falló ({e}); deshaciendo para dejar el FHS consistente");
match takana_upgrade::recover(&store, &root, &state, j, true) {
Ok(r) => println!("hammer-recover: DESHECHO tras fallo (generación {})", r.generation),
Err(e2) => eprintln!("hammer-recover: rollback de respaldo también falló: {e2}"),
}
}
}
}