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
Generated
+162 -162
View File
@@ -483,179 +483,21 @@ dependencies = [
"wasip3", "wasip3",
] ]
[[package]]
name = "hammer-agent"
version = "0.0.1"
dependencies = [
"anyhow",
"base64",
"hammer-core",
"hammer-journal",
"hammer-overlay",
"serde",
"serde_json",
"serde_yaml",
"tempfile",
"thiserror",
"tracing",
"ureq",
]
[[package]]
name = "hammer-bootstrap"
version = "0.0.1"
dependencies = [
"anyhow",
"hammer-build",
"hammer-core",
"hex",
"serde",
"serde_json",
"sha2",
"tempfile",
"thiserror",
"tracing",
]
[[package]]
name = "hammer-build"
version = "0.0.1"
dependencies = [
"anyhow",
"hammer-core",
"hex",
"libc",
"serde_json",
"sha2",
"tempfile",
"thiserror",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "hammer-cli"
version = "0.0.1"
dependencies = [
"anyhow",
"base64",
"clap",
"hammer-agent",
"hammer-bootstrap",
"hammer-build",
"hammer-core",
"hammer-journal",
"hammer-mirror",
"hammer-overlay",
"hammer-upgrade",
"hex",
"nix",
"serde",
"serde_json",
"sha2",
"tempfile",
"toml",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "hammer-core"
version = "0.0.1"
dependencies = [
"anyhow",
"base64",
"blake3",
"ed25519-dalek",
"getrandom 0.2.17",
"serde",
"serde_json",
"serde_yaml",
"tempfile",
"thiserror",
"toml",
]
[[package]]
name = "hammer-journal"
version = "0.0.1"
dependencies = [
"anyhow",
"blake3",
"hammer-core",
"serde",
"serde_json",
"tempfile",
"thiserror",
"tracing",
]
[[package]]
name = "hammer-mirror"
version = "0.0.1"
dependencies = [
"anyhow",
"hammer-core",
"serde",
"serde_json",
"tempfile",
"thiserror",
"tracing",
]
[[package]]
name = "hammer-overlay"
version = "0.0.1"
dependencies = [
"anyhow",
"hammer-core",
"hammer-journal",
"libc",
"serde",
"serde_json",
"tempfile",
"thiserror",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "hammer-recover"
version = "0.0.1"
dependencies = [
"hammer-journal",
"hammer-upgrade",
]
[[package]]
name = "hammer-upgrade"
version = "0.0.1"
dependencies = [
"anyhow",
"hammer-core",
"hammer-journal",
"serde",
"serde_json",
"tempfile",
"thiserror",
"tracing",
]
[[package]] [[package]]
name = "hammerd" name = "hammerd"
version = "0.0.1" version = "0.0.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
"hammer-build",
"hammer-core",
"hammer-journal",
"hammer-overlay",
"libc", "libc",
"nix", "nix",
"postcard", "postcard",
"serde", "serde",
"serde_json", "serde_json",
"takana-build",
"takana-core",
"takana-journal",
"takana-overlay",
"tempfile", "tempfile",
"thiserror", "thiserror",
"tracing", "tracing",
@@ -1381,6 +1223,164 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "takana-agent"
version = "0.0.1"
dependencies = [
"anyhow",
"base64",
"serde",
"serde_json",
"serde_yaml",
"takana-core",
"takana-journal",
"takana-overlay",
"tempfile",
"thiserror",
"tracing",
"ureq",
]
[[package]]
name = "takana-bootstrap"
version = "0.0.1"
dependencies = [
"anyhow",
"hex",
"serde",
"serde_json",
"sha2",
"takana-build",
"takana-core",
"tempfile",
"thiserror",
"tracing",
]
[[package]]
name = "takana-build"
version = "0.0.1"
dependencies = [
"anyhow",
"hex",
"libc",
"serde_json",
"sha2",
"takana-core",
"tempfile",
"thiserror",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "takana-cli"
version = "0.0.1"
dependencies = [
"anyhow",
"base64",
"clap",
"hex",
"nix",
"serde",
"serde_json",
"sha2",
"takana-agent",
"takana-bootstrap",
"takana-build",
"takana-core",
"takana-journal",
"takana-mirror",
"takana-overlay",
"takana-upgrade",
"tempfile",
"toml",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "takana-core"
version = "0.0.1"
dependencies = [
"anyhow",
"base64",
"blake3",
"ed25519-dalek",
"getrandom 0.2.17",
"serde",
"serde_json",
"serde_yaml",
"tempfile",
"thiserror",
"toml",
]
[[package]]
name = "takana-journal"
version = "0.0.1"
dependencies = [
"anyhow",
"blake3",
"serde",
"serde_json",
"takana-core",
"tempfile",
"thiserror",
"tracing",
]
[[package]]
name = "takana-mirror"
version = "0.0.1"
dependencies = [
"anyhow",
"serde",
"serde_json",
"takana-core",
"tempfile",
"thiserror",
"tracing",
]
[[package]]
name = "takana-overlay"
version = "0.0.1"
dependencies = [
"anyhow",
"libc",
"serde",
"serde_json",
"takana-core",
"takana-journal",
"tempfile",
"thiserror",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "takana-recover"
version = "0.0.1"
dependencies = [
"takana-journal",
"takana-upgrade",
]
[[package]]
name = "takana-upgrade"
version = "0.0.1"
dependencies = [
"anyhow",
"serde",
"serde_json",
"takana-core",
"takana-journal",
"tempfile",
"thiserror",
"tracing",
]
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.27.0" version = "3.27.0"
+18 -18
View File
@@ -1,16 +1,16 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = [ members = [
"crates/hammer-core", "crates/takana-core",
"crates/hammer-build", "crates/takana-build",
"crates/hammer-bootstrap", "crates/takana-bootstrap",
"crates/hammer-overlay", "crates/takana-overlay",
"crates/hammer-journal", "crates/takana-journal",
"crates/hammer-mirror", "crates/takana-mirror",
"crates/hammer-upgrade", "crates/takana-upgrade",
"crates/hammer-recover", "crates/takana-recover",
"crates/hammer-agent", "crates/takana-agent",
"crates/hammer-cli", "crates/takana-cli",
"crates/hammerd", "crates/hammerd",
"crates/netup", "crates/netup",
] ]
@@ -23,14 +23,14 @@ authors = ["sergio <gerencia@jlsoltech.com>"]
repository = "https://gitea.gioser.net/sergio/hammer" repository = "https://gitea.gioser.net/sergio/hammer"
[workspace.dependencies] [workspace.dependencies]
hammer-core = { path = "crates/hammer-core" } takana-core = { path = "crates/takana-core" }
hammer-build = { path = "crates/hammer-build" } takana-build = { path = "crates/takana-build" }
hammer-bootstrap = { path = "crates/hammer-bootstrap" } takana-bootstrap = { path = "crates/takana-bootstrap" }
hammer-overlay = { path = "crates/hammer-overlay" } takana-overlay = { path = "crates/takana-overlay" }
hammer-journal = { path = "crates/hammer-journal" } takana-journal = { path = "crates/takana-journal" }
hammer-mirror = { path = "crates/hammer-mirror" } takana-mirror = { path = "crates/takana-mirror" }
hammer-upgrade = { path = "crates/hammer-upgrade" } takana-upgrade = { path = "crates/takana-upgrade" }
hammer-agent = { path = "crates/hammer-agent" } takana-agent = { path = "crates/takana-agent" }
# shared third-party deps, pinned at the workspace level # shared third-party deps, pinned at the workspace level
anyhow = "1" anyhow = "1"
+4 -4
View File
@@ -12,10 +12,10 @@ name = "hammerd"
path = "src/main.rs" path = "src/main.rs"
[dependencies] [dependencies]
hammer-core.workspace = true takana-core.workspace = true
hammer-build.workspace = true takana-build.workspace = true
hammer-journal.workspace = true takana-journal.workspace = true
hammer-overlay.workspace = true takana-overlay.workspace = true
anyhow.workspace = true anyhow.workspace = true
clap.workspace = true clap.workspace = true
thiserror.workspace = true thiserror.workspace = true
+26 -26
View File
@@ -25,7 +25,7 @@ use std::thread;
use nix::sys::socket::{getsockopt, sockopt::PeerCredentials}; use nix::sys::socket::{getsockopt, sockopt::PeerCredentials};
use crate::events::EventBus; use crate::events::EventBus;
use hammer_core::proto::{ use takana_core::proto::{
Cap, Command, Event, EvidenceCheckVerdict, EvidenceVerdict, Peer, RecipeInline, Cap, Command, Event, EvidenceCheckVerdict, EvidenceVerdict, Peer, RecipeInline,
PROTOCOL_VERSION, PROTOCOL_VERSION,
}; };
@@ -49,7 +49,7 @@ pub fn default_policy() -> CapsPolicy {
/// Política derivada de un fichero `agent-caps.toml` (SDD 07 §4). El peer no elige sus caps; /// Política derivada de un fichero `agent-caps.toml` (SDD 07 §4). El peer no elige sus caps;
/// las resuelve la config por `(uid, gid)`. /// las resuelve la config por `(uid, gid)`.
pub fn policy_from_config(cfg: hammer_core::AgentCapsConfig) -> CapsPolicy { pub fn policy_from_config(cfg: takana_core::AgentCapsConfig) -> CapsPolicy {
Arc::new(move |peer: &Peer| cfg.caps_for(peer.uid, peer.gid)) Arc::new(move |peer: &Peer| cfg.caps_for(peer.uid, peer.gid))
} }
@@ -259,7 +259,7 @@ fn dispatch(
.ok(); .ok();
} }
Command::Inject { artifact, target, overlay: _ } => { Command::Inject { artifact, target, overlay: _ } => {
let store = match hammer_core::Store::open(&ctx.store_root) { let store = match takana_core::Store::open(&ctx.store_root) {
Ok(s) => s, Ok(s) => s,
Err(e) => { Err(e) => {
let _ = tx.send(Event::Error { let _ = tx.send(Event::Error {
@@ -279,10 +279,10 @@ fn dispatch(
return; return;
} }
}; };
let report = match hammer_build::run_hydrate( let report = match takana_build::run_hydrate(
&artifact_dir, &artifact_dir,
Path::new(&target), Path::new(&target),
hammer_core::LinkMode::Static, takana_core::LinkMode::Static,
None, None,
) { ) {
Ok(r) => r, Ok(r) => r,
@@ -333,7 +333,7 @@ fn dispatch(
// El daemon evalúa con el `fs_root` del sistema host (None ⇒ rutas // El daemon evalúa con el `fs_root` del sistema host (None ⇒ rutas
// absolutas tal y como vienen). Si el .swm necesitara otra base, el // absolutas tal y como vienen). Si el .swm necesitara otra base, el
// cliente puede evaluar localmente con un EvalContext distinto. // cliente puede evaluar localmente con un EvalContext distinto.
hammer_core::query::eval_str(e, &hammer_core::query::EvalContext::new()) takana_core::query::eval_str(e, &takana_core::query::EvalContext::new())
} }
other => { other => {
let _ = tx.send(Event::Error { let _ = tx.send(Event::Error {
@@ -363,7 +363,7 @@ fn dispatch(
fn run_compile(recipe: RecipeInline, store_root: PathBuf, tx: Sender<Event>) { fn run_compile(recipe: RecipeInline, store_root: PathBuf, tx: Sender<Event>) {
let name = recipe.name.clone(); let name = recipe.name.clone();
let mutation = hammer_core::swm::Mutation::SourcePatch { let mutation = takana_core::swm::Mutation::SourcePatch {
repo: recipe.repo, repo: recipe.repo,
commit: recipe.commit, commit: recipe.commit,
tarball: recipe.tarball, tarball: recipe.tarball,
@@ -371,7 +371,7 @@ fn run_compile(recipe: RecipeInline, store_root: PathBuf, tx: Sender<Event>) {
strip_components: None, strip_components: None,
patch: recipe.patch, patch: recipe.patch,
patch_url: None, patch_url: None,
build: hammer_core::swm::SwmBuild { build: takana_core::swm::SwmBuild {
compiler: recipe.compiler, compiler: recipe.compiler,
target: recipe.target, target: recipe.target,
link: recipe.link, link: recipe.link,
@@ -389,7 +389,7 @@ fn run_compile(recipe: RecipeInline, store_root: PathBuf, tx: Sender<Event>) {
// de compatibilidad vive en `install` (CLI) contra el estado instalado. Vacío aquí. // de compatibilidad vive en `install` (CLI) contra el estado instalado. Vacío aquí.
slots: Default::default(), slots: Default::default(),
}; };
let store = match hammer_core::Store::open(&store_root) { let store = match takana_core::Store::open(&store_root) {
Ok(s) => s, Ok(s) => s,
Err(e) => { Err(e) => {
let _ = tx.send(Event::BuildFailed { let _ = tx.send(Event::BuildFailed {
@@ -400,8 +400,8 @@ fn run_compile(recipe: RecipeInline, store_root: PathBuf, tx: Sender<Event>) {
return; return;
} }
}; };
let cfg = hammer_build::BuildConfig::from_env_or_defaults(store.root()); let cfg = takana_build::BuildConfig::from_env_or_defaults(store.root());
match hammer_build::build_source_patch(&mutation, &cfg, &store, None, None) { match takana_build::build_source_patch(&mutation, &cfg, &store, None, None) {
Ok(h) => { Ok(h) => {
// [H1c] El artefacto está sellado; ahora EJECUTAMOS la evidencia declarada y // [H1c] El artefacto está sellado; ahora EJECUTAMOS la evidencia declarada y
// adjuntamos el veredicto. El gate ("no proponer" si falla) vive en el orquestador // adjuntamos el veredicto. El gate ("no proponer" si falla) vive en el orquestador
@@ -417,7 +417,7 @@ fn run_compile(recipe: RecipeInline, store_root: PathBuf, tx: Sender<Event>) {
Err(e) => { Err(e) => {
// Si el fallo trae la cola del log del lab, la separamos de la razón para que la // Si el fallo trae la cola del log del lab, la separamos de la razón para que la
// IA pueda leer el error real del compilador, no sólo el mensaje de Rust. // IA pueda leer el error real del compilador, no sólo el mensaje de Rust.
let (reason, log_tail) = match hammer_build::sandbox::BuildFailure::from_error(&e) { let (reason, log_tail) = match takana_build::sandbox::BuildFailure::from_error(&e) {
Some(bf) => (bf.reason.clone(), bf.log_tail.clone()), Some(bf) => (bf.reason.clone(), bf.log_tail.clone()),
None => (e.to_string(), None), None => (e.to_string(), None),
}; };
@@ -437,13 +437,13 @@ fn run_compile(recipe: RecipeInline, store_root: PathBuf, tx: Sender<Event>) {
/// - `Some(all_passed=false, …)` sintético si la evidencia ni siquiera pudo ejecutarse (falta una /// - `Some(all_passed=false, …)` sintético si la evidencia ni siquiera pudo ejecutarse (falta una
/// build-dep, error de sandbox…): eso también debe bloquear la propuesta, no pasar en silencio. /// build-dep, error de sandbox…): eso también debe bloquear la propuesta, no pasar en silencio.
fn evidence_verdict( fn evidence_verdict(
mutation: &hammer_core::swm::Mutation, mutation: &takana_core::swm::Mutation,
cfg: &hammer_build::BuildConfig, cfg: &takana_build::BuildConfig,
store: &hammer_core::Store, store: &takana_core::Store,
) -> Option<EvidenceVerdict> { ) -> Option<EvidenceVerdict> {
let has_evidence = matches!( let has_evidence = matches!(
mutation, mutation,
hammer_core::swm::Mutation::SourcePatch { evidence, .. } if !evidence.is_empty() takana_core::swm::Mutation::SourcePatch { evidence, .. } if !evidence.is_empty()
); );
if !has_evidence { if !has_evidence {
return None; return None;
@@ -456,7 +456,7 @@ fn evidence_verdict(
all_passed: false, all_passed: false,
max_level: None, max_level: None,
checks: vec![EvidenceCheckVerdict { checks: vec![EvidenceCheckVerdict {
kind: hammer_core::EvidenceKind::CmdExit, kind: takana_core::EvidenceKind::CmdExit,
cmd: "<runner>".into(), cmd: "<runner>".into(),
passed: false, passed: false,
detail: format!("la evidencia no pudo ejecutarse: {e}"), detail: format!("la evidencia no pudo ejecutarse: {e}"),
@@ -467,14 +467,14 @@ fn evidence_verdict(
} }
fn run_evidence_inner( fn run_evidence_inner(
mutation: &hammer_core::swm::Mutation, mutation: &takana_core::swm::Mutation,
cfg: &hammer_build::BuildConfig, cfg: &takana_build::BuildConfig,
store: &hammer_core::Store, store: &takana_core::Store,
) -> hammer_core::Result<EvidenceVerdict> { ) -> takana_core::Result<EvidenceVerdict> {
let catalog_dir = hammer_build::swm_bridge::catalog_dir_for(cfg, None); let catalog_dir = takana_build::swm_bridge::catalog_dir_for(cfg, None);
std::fs::create_dir_all(&catalog_dir)?; std::fs::create_dir_all(&catalog_dir)?;
let recipe = hammer_build::swm_bridge::recipe_from_source_patch(mutation, None, &catalog_dir)?; let recipe = takana_build::swm_bridge::recipe_from_source_patch(mutation, None, &catalog_dir)?;
let report = hammer_build::run_evidence(&recipe, cfg, store)?; let report = takana_build::run_evidence(&recipe, cfg, store)?;
Ok(EvidenceVerdict { Ok(EvidenceVerdict {
all_passed: report.all_passed(), all_passed: report.all_passed(),
max_level: report.max_level_passed(), max_level: report.max_level_passed(),
@@ -510,7 +510,7 @@ fn query_file(path: &str) -> serde_json::Value {
} }
fn query_artifact(store_root: &Path, hash: &str) -> serde_json::Value { fn query_artifact(store_root: &Path, hash: &str) -> serde_json::Value {
let store = match hammer_core::Store::open(store_root) { let store = match takana_core::Store::open(store_root) {
Ok(s) => s, Ok(s) => s,
Err(e) => return serde_json::json!({"error": e.to_string()}), Err(e) => return serde_json::json!({"error": e.to_string()}),
}; };
@@ -552,7 +552,7 @@ mod tests {
#[test] #[test]
fn config_policy_resolves_by_uid_gid() { fn config_policy_resolves_by_uid_gid() {
let cfg = hammer_core::AgentCapsConfig::from_toml( let cfg = takana_core::AgentCapsConfig::from_toml(
r#" r#"
default = ["query"] default = ["query"]
[[rule]] [[rule]]
+1 -1
View File
@@ -28,7 +28,7 @@
use std::sync::mpsc::Receiver; use std::sync::mpsc::Receiver;
use hammer_core::proto::Event; use takana_core::proto::Event;
use crate::events::EventBus; use crate::events::EventBus;
+2 -2
View File
@@ -12,7 +12,7 @@
use std::sync::mpsc::{self, Receiver, Sender}; use std::sync::mpsc::{self, Receiver, Sender};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use hammer_core::proto::Event; use takana_core::proto::Event;
#[derive(Clone)] #[derive(Clone)]
pub struct EventBus { pub struct EventBus {
@@ -58,7 +58,7 @@ impl EventBus {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use hammer_core::proto::Event; use takana_core::proto::Event;
#[test] #[test]
fn publish_reaches_all_subs() { fn publish_reaches_all_subs() {
+3 -3
View File
@@ -33,7 +33,7 @@ struct Args {
journal: String, journal: String,
/// Directorio raíz de overlays (para que el watcher pueda filtrar eventos bajo overlays /// Directorio raíz de overlays (para que el watcher pueda filtrar eventos bajo overlays
/// activos). Default coincide con `hammer try`. /// activos). Default coincide con `hammer try`.
#[arg(long, default_value = hammer_overlay::DEFAULT_STATE_ROOT)] #[arg(long, default_value = takana_overlay::DEFAULT_STATE_ROOT)]
overlay_state_root: String, overlay_state_root: String,
/// Directorios extra a vigilar; si vacío, usa los defaults del FHS (SDD 05 §2). /// Directorios extra a vigilar; si vacío, usa los defaults del FHS (SDD 05 §2).
#[arg(long = "watch")] #[arg(long = "watch")]
@@ -97,7 +97,7 @@ fn main() -> anyhow::Result<()> {
// Watcher fanotify: opcional. Si falla (no caps), el daemon sigue con el bus solo. // Watcher fanotify: opcional. Si falla (no caps), el daemon sigue con el bus solo.
if !args.no_watcher { if !args.no_watcher {
let journal = hammer_journal::Journal::open(&args.journal)?; let journal = takana_journal::Journal::open(&args.journal)?;
let mut dirs = watcher::default_watch_dirs(); let mut dirs = watcher::default_watch_dirs();
dirs.extend(args.extra_watch.clone()); dirs.extend(args.extra_watch.clone());
let overlay_root = PathBuf::from(&args.overlay_state_root); let overlay_root = PathBuf::from(&args.overlay_state_root);
@@ -132,7 +132,7 @@ fn main() -> anyhow::Result<()> {
// Bus de agente: bloqueante en este thread. Si falla, fin del daemon. // Bus de agente: bloqueante en este thread. Si falla, fin del daemon.
// Política de caps: del fichero si existe, si no la heurística por-UID por defecto. // Política de caps: del fichero si existe, si no la heurística por-UID por defecto.
let policy = match hammer_core::AgentCapsConfig::load(&args.agent_caps) { let policy = match takana_core::AgentCapsConfig::load(&args.agent_caps) {
Ok(Some(cfg)) => { Ok(Some(cfg)) => {
tracing::info!(path = %args.agent_caps.display(), rules = cfg.rule.len(), tracing::info!(path = %args.agent_caps.display(), rules = cfg.rule.len(),
"bus: política de caps cargada de fichero"); "bus: política de caps cargada de fichero");
+8 -8
View File
@@ -6,7 +6,7 @@
//! * Marcamos como `FAN_MARK_ADD | FAN_MARK_ONLYDIR` cada directorio vigilado. La cobertura //! * Marcamos como `FAN_MARK_ADD | FAN_MARK_ONLYDIR` cada directorio vigilado. La cobertura
//! por defecto (ver `default_watch_dirs`) cubre el FHS clásico de userland. //! por defecto (ver `default_watch_dirs`) cubre el FHS clásico de userland.
//! * Filtro mínimo: ignoramos eventos cuya ruta resuelta cae dentro de un overlay activo //! * Filtro mínimo: ignoramos eventos cuya ruta resuelta cae dentro de un overlay activo
//! (consultamos `hammer_overlay::status` en cada batch — para Fase 3 el coste es //! (consultamos `takana_overlay::status` en cada batch — para Fase 3 el coste es
//! despreciable; cuando los overlays se vuelvan numerosos cachearemos los paths). //! despreciable; cuando los overlays se vuelvan numerosos cachearemos los paths).
//! * Cada evento se materializa como `MutationEvent` con `source = External` y la `op` //! * Cada evento se materializa como `MutationEvent` con `source = External` y la `op`
//! se deriva (`classify_op`): si el fd resuelve a un path borrado (`" (deleted)"`) es //! se deriva (`classify_op`): si el fd resuelve a un path borrado (`" (deleted)"`) es
@@ -27,7 +27,7 @@ use std::os::fd::AsFd;
use std::os::unix::ffi::OsStringExt; use std::os::unix::ffi::OsStringExt;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use hammer_journal::{Actor, Journal, MutationEvent, MutationOp, Source}; use takana_journal::{Actor, Journal, MutationEvent, MutationOp, Source};
use nix::fcntl::AT_FDCWD; use nix::fcntl::AT_FDCWD;
use nix::sys::fanotify::{EventFFlags, Fanotify, InitFlags, MarkFlags, MaskFlags}; use nix::sys::fanotify::{EventFFlags, Fanotify, InitFlags, MarkFlags, MaskFlags};
@@ -48,7 +48,7 @@ pub enum Error {
#[error("read_events: {0}")] #[error("read_events: {0}")]
Read(nix::errno::Errno), Read(nix::errno::Errno),
#[error("journal: {0}")] #[error("journal: {0}")]
Journal(#[from] hammer_journal::Error), Journal(#[from] takana_journal::Error),
#[error("io: {0}")] #[error("io: {0}")]
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
} }
@@ -160,7 +160,7 @@ impl Watcher {
let content_hash = if deleted { let content_hash = if deleted {
None None
} else { } else {
match hammer_journal::hash_file(&path) { match takana_journal::hash_file(&path) {
Ok(h) => h, Ok(h) => h,
Err(e) => { Err(e) => {
tracing::warn!(path = %path.display(), error = %e, "watcher: no pude hashear contenido"); tracing::warn!(path = %path.display(), error = %e, "watcher: no pude hashear contenido");
@@ -170,7 +170,7 @@ impl Watcher {
}; };
let op = self.classify_op(&path, deleted)?; let op = self.classify_op(&path, deleted)?;
let me = MutationEvent { let me = MutationEvent {
ts: hammer_journal::now_rfc3339(), ts: takana_journal::now_rfc3339(),
op, op,
path, path,
by: Actor { by: Actor {
@@ -188,7 +188,7 @@ impl Watcher {
} }
recorded += 1; recorded += 1;
if let Some(bus) = &self.events { if let Some(bus) = &self.events {
bus.publish(&hammer_core::proto::Event::Modified { bus.publish(&takana_core::proto::Event::Modified {
path: me.path.display().to_string(), path: me.path.display().to_string(),
op: me.op.as_str().to_string(), op: me.op.as_str().to_string(),
ts: me.ts.clone(), ts: me.ts.clone(),
@@ -216,7 +216,7 @@ impl Watcher {
let Some(root) = &self.overlay_state_root else { let Some(root) = &self.overlay_state_root else {
return Vec::new(); return Vec::new();
}; };
match hammer_overlay::status(root) { match takana_overlay::status(root) {
Ok(overlays) => overlays Ok(overlays) => overlays
.into_iter() .into_iter()
.flat_map(|o| o.mounts.into_iter().map(|m| m.target)) .flat_map(|o| o.mounts.into_iter().map(|m| m.target))
@@ -301,7 +301,7 @@ mod tests {
fn me(op: MutationOp, path: &str, hash: Option<&str>) -> MutationEvent { fn me(op: MutationOp, path: &str, hash: Option<&str>) -> MutationEvent {
MutationEvent { MutationEvent {
ts: hammer_journal::now_rfc3339(), ts: takana_journal::now_rfc3339(),
op, op,
path: PathBuf::from(path), path: PathBuf::from(path),
by: Actor { source: Source::External, pid: Some(1), uid: Some(0) }, by: Actor { source: Source::External, pid: Some(1), uid: Some(0) },
+1 -1
View File
@@ -24,7 +24,7 @@ use std::path::PathBuf;
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
use hammer_core::proto::Event; use takana_core::proto::Event;
use serde::Serialize; use serde::Serialize;
use ulid::Ulid; use ulid::Ulid;
+1 -1
View File
@@ -23,7 +23,7 @@ use std::sync::Arc;
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
use hammer_core::proto::{Cap, Command, Event, Peer, RecipeInline}; use takana_core::proto::{Cap, Command, Event, Peer, RecipeInline};
/// Helper: lanza un bus en background y devuelve la ruta del socket. Limpia al final del /// Helper: lanza un bus en background y devuelve la ruta del socket. Limpia al final del
/// test gracias al `tempfile::TempDir` que se le pasa al caller. /// test gracias al `tempfile::TempDir` que se le pasa al caller.
@@ -1,5 +1,5 @@
[package] [package]
name = "hammer-agent" name = "takana-agent"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
@@ -14,8 +14,8 @@ default = []
llm-claude = ["dep:ureq"] llm-claude = ["dep:ureq"]
[dependencies] [dependencies]
hammer-core.workspace = true takana-core.workspace = true
hammer-overlay.workspace = true takana-overlay.workspace = true
anyhow.workspace = true anyhow.workspace = true
thiserror.workspace = true thiserror.workspace = true
serde.workspace = true serde.workspace = true
@@ -26,5 +26,5 @@ ureq = { version = "2", optional = true, default-features = false, features = ["
[dev-dependencies] [dev-dependencies]
tempfile.workspace = true tempfile.workspace = true
hammer-journal.workspace = true takana-journal.workspace = true
base64.workspace = true base64.workspace = true
@@ -17,7 +17,7 @@ use std::sync::{Arc, Condvar, Mutex};
use std::thread; use std::thread;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use hammer_core::proto::{Cap, Command, Event, Peer, RecipeInline}; use takana_core::proto::{Cap, Command, Event, Peer, RecipeInline};
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum ClientError { pub enum ClientError {
@@ -49,7 +49,7 @@ pub struct Welcome {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct CompileOutcome { pub struct CompileOutcome {
pub artifact: String, pub artifact: String,
pub verdict: Option<hammer_core::proto::EvidenceVerdict>, pub verdict: Option<takana_core::proto::EvidenceVerdict>,
} }
/// Estado compartido entre el reader thread y el caller. /// Estado compartido entre el reader thread y el caller.
@@ -111,7 +111,7 @@ impl AgentClient {
}, },
}; };
me.send(&Command::Hello { me.send(&Command::Hello {
ver: hammer_core::proto::PROTOCOL_VERSION, ver: takana_core::proto::PROTOCOL_VERSION,
client: client.to_string(), client: client.to_string(),
})?; })?;
let ev = me.recv_pending(Duration::from_secs(5))?; let ev = me.recv_pending(Duration::from_secs(5))?;
@@ -182,7 +182,7 @@ impl AgentClient {
} }
/// Evalúa una expresión del mini-lenguaje (`bin:grep`, `pin:musl`, …) en el daemon. /// Evalúa una expresión del mini-lenguaje (`bin:grep`, `pin:musl`, …) en el daemon.
/// El JSON devuelto sigue el schema de `hammer_core::query::eval`. /// El JSON devuelto sigue el schema de `takana_core::query::eval`.
pub fn query_expr(&mut self, expr: &str, timeout: Duration) -> Result<serde_json::Value> { pub fn query_expr(&mut self, expr: &str, timeout: Duration) -> Result<serde_json::Value> {
self.send(&Command::Query { self.send(&Command::Query {
what: "expr".into(), what: "expr".into(),
@@ -38,11 +38,11 @@ pub enum Error {
#[error("io: {0}")] #[error("io: {0}")]
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
#[error("overlay: {0}")] #[error("overlay: {0}")]
Overlay(#[from] hammer_overlay::Error), Overlay(#[from] takana_overlay::Error),
#[error("apply: {0}")] #[error("apply: {0}")]
Apply(#[from] hammer_core::apply::ApplyError), Apply(#[from] takana_core::apply::ApplyError),
#[error("core: {0}")] #[error("core: {0}")]
Core(#[from] hammer_core::Error), Core(#[from] takana_core::Error),
} }
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
@@ -7,7 +7,7 @@
//! schema → swm.verify_schema() //! schema → swm.verify_schema()
//! base → swm.verify_base(local) → debe ser Ok //! base → swm.verify_base(local) → debe ser Ok
//! build → para cada source_patch: AgentClient.compile() vía el bus (o saltar si --no-bus) //! build → para cada source_patch: AgentClient.compile() vía el bus (o saltar si --no-bus)
//! try → hammer_overlay::try_overlay() (o usar --prefix) //! try → takana_overlay::try_overlay() (o usar --prefix)
//! apply → hammer-core::apply para config_edit/file_drop + hydrate para source_patch //! apply → hammer-core::apply para config_edit/file_drop + hydrate para source_patch
//! verify → spot-checks post-condición sobre disco //! verify → spot-checks post-condición sobre disco
//! propose → devuelve Proposal con overlay_id + log de checks //! propose → devuelve Proposal con overlay_id + log de checks
@@ -18,11 +18,11 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::time::Duration; use std::time::Duration;
use hammer_core::apply::{apply_config_edit, apply_file_drop, rebase_path}; use takana_core::apply::{apply_config_edit, apply_file_drop, rebase_path};
use hammer_core::proto::RecipeInline; use takana_core::proto::RecipeInline;
use hammer_core::swm::{Mutation, Swm}; use takana_core::swm::{Mutation, Swm};
use hammer_core::BaseRef; use takana_core::BaseRef;
use hammer_overlay::OverlayId; use takana_overlay::OverlayId;
use crate::client::AgentClient; use crate::client::AgentClient;
use crate::translator::{IntentTranslator, SystemContext}; use crate::translator::{IntentTranslator, SystemContext};
@@ -79,7 +79,7 @@ impl<T: IntentTranslator> Orchestrator<T> {
// 3. BASE // 3. BASE
match swm.verify_base(&self.base) { match swm.verify_base(&self.base) {
hammer_core::BaseCompat::Ok => {} takana_core::BaseCompat::Ok => {}
other => { other => {
return Err(Error::Orchestrate(format!( return Err(Error::Orchestrate(format!(
"base incompatible: {other:?}" "base incompatible: {other:?}"
@@ -91,7 +91,7 @@ impl<T: IntentTranslator> Orchestrator<T> {
let (overlay_id, prefix): (Option<OverlayId>, Option<PathBuf>) = match &self.apply { let (overlay_id, prefix): (Option<OverlayId>, Option<PathBuf>) = match &self.apply {
ApplyTarget::Overlay { state_root } => { ApplyTarget::Overlay { state_root } => {
std::fs::create_dir_all(state_root)?; std::fs::create_dir_all(state_root)?;
let id = hammer_overlay::try_overlay(&[], state_root)?; let id = takana_overlay::try_overlay(&[], state_root)?;
(Some(id), None) (Some(id), None)
} }
ApplyTarget::Prefix { prefix } => { ApplyTarget::Prefix { prefix } => {
@@ -147,8 +147,8 @@ impl<T: IntentTranslator> Orchestrator<T> {
} }
Mutation::InitRule { action, service, command } => { Mutation::InitRule { action, service, command } => {
let rules_dir = let rules_dir =
rebase_path(hammer_core::apply::INIT_RULES_DIR, prefix.as_deref()); rebase_path(takana_core::apply::INIT_RULES_DIR, prefix.as_deref());
let p = hammer_core::apply::apply_init_rule( let p = takana_core::apply::apply_init_rule(
&rules_dir, action, service, command, &rules_dir, action, service, command,
) )
.map_err(|e| Error::Orchestrate(format!("init_rule '{action} {service}': {e}")))?; .map_err(|e| Error::Orchestrate(format!("init_rule '{action} {service}': {e}")))?;
@@ -246,7 +246,7 @@ impl<T: IntentTranslator> Orchestrator<T> {
// Hidratamos localmente desde el store (sin pasar por el bus, evita un // Hidratamos localmente desde el store (sin pasar por el bus, evita un
// segundo Inject que duplicaría trabajo). // segundo Inject que duplicaría trabajo).
let store = hammer_core::Store::open(store)?; let store = takana_core::Store::open(store)?;
let artifact_dir = store let artifact_dir = store
.find_by_hash(&artifact) .find_by_hash(&artifact)
.map_err(|e| Error::Orchestrate(format!("find_by_hash: {e}")))?; .map_err(|e| Error::Orchestrate(format!("find_by_hash: {e}")))?;
@@ -340,13 +340,13 @@ fn wait_for_crash(policy: &RepairPolicy) -> Option<CrashInfo> {
let deadline = std::time::Instant::now() + policy.crash_window; let deadline = std::time::Instant::now() + policy.crash_window;
// Drenamos primero la cola asíncrona ya acumulada (eventos llegados durante el `run`). // Drenamos primero la cola asíncrona ya acumulada (eventos llegados durante el `run`).
for ev in client.drain_async() { for ev in client.drain_async() {
if let hammer_core::proto::Event::Crashed { service, code } = ev { if let takana_core::proto::Event::Crashed { service, code } = ev {
return Some(CrashInfo { service, code }); return Some(CrashInfo { service, code });
} }
} }
while let Some(remaining) = deadline.checked_duration_since(std::time::Instant::now()) { while let Some(remaining) = deadline.checked_duration_since(std::time::Instant::now()) {
match client.next_async(remaining) { match client.next_async(remaining) {
Ok(hammer_core::proto::Event::Crashed { service, code }) => { Ok(takana_core::proto::Event::Crashed { service, code }) => {
return Some(CrashInfo { service, code }); return Some(CrashInfo { service, code });
} }
Ok(_other) => continue, Ok(_other) => continue,
@@ -554,7 +554,7 @@ impl Default for RepairPolicy {
mod tests { mod tests {
use super::*; use super::*;
use crate::translator::MockTranslator; use crate::translator::MockTranslator;
use hammer_core::swm::{Base, Mutation}; use takana_core::swm::{Base, Mutation};
use std::collections::BTreeMap; use std::collections::BTreeMap;
fn baseref() -> BaseRef { fn baseref() -> BaseRef {
@@ -626,7 +626,7 @@ mod tests {
strip_components: None, strip_components: None,
patch: None, patch: None,
patch_url: None, patch_url: None,
build: hammer_core::swm::SwmBuild { build: takana_core::swm::SwmBuild {
compiler: "zig-cc".into(), compiler: "zig-cc".into(),
target: "x86_64-linux-musl".into(), target: "x86_64-linux-musl".into(),
link: "static".into(), link: "static".into(),
@@ -12,7 +12,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path; use std::path::Path;
use hammer_core::{BaseRef, Swm}; use takana_core::{BaseRef, Swm};
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum TranslateError { pub enum TranslateError {
@@ -165,7 +165,7 @@ impl IntentTranslator for MockTranslator {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use hammer_core::swm::{Base, Mutation}; use takana_core::swm::{Base, Mutation};
use std::collections::BTreeMap; use std::collections::BTreeMap;
fn empty_ctx() -> SystemContext { fn empty_ctx() -> SystemContext {
@@ -31,7 +31,7 @@
//! ``` //! ```
use crate::translator::{IntentTranslator, SystemContext, TranslateError}; use crate::translator::{IntentTranslator, SystemContext, TranslateError};
use hammer_core::Swm; use takana_core::Swm;
/// Inyección HTTP. La implementación por defecto ([`UreqHttp`]) usa `ureq` síncrono; /// Inyección HTTP. La implementación por defecto ([`UreqHttp`]) usa `ureq` síncrono;
/// los tests pasan una fake. /// los tests pasan una fake.
@@ -279,7 +279,7 @@ mod tests {
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use hammer_core::BaseRef; use takana_core::BaseRef;
/// Fake HTTP que captura el request y devuelve un body pre-armado. /// Fake HTTP que captura el request y devuelve un body pre-armado.
struct FakeHttp { struct FakeHttp {
@@ -11,12 +11,12 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use base64::{engine::general_purpose::STANDARD, Engine as _}; use base64::{engine::general_purpose::STANDARD, Engine as _};
use hammer_agent::{ use takana_agent::{
orchestrator::{ApplyTarget, CompileMode}, orchestrator::{ApplyTarget, CompileMode},
MockTranslator, Orchestrator, MockTranslator, Orchestrator,
}; };
use hammer_core::swm::{Base, Mutation, Swm}; use takana_core::swm::{Base, Mutation, Swm};
use hammer_core::{ArtifactHash, BaseRef}; use takana_core::{ArtifactHash, BaseRef};
fn baseref() -> BaseRef { fn baseref() -> BaseRef {
let mut pins = BTreeMap::new(); let mut pins = BTreeMap::new();
@@ -86,7 +86,7 @@ fn intent_to_overlay_proposal_via_mock_translator() {
proposal proposal
.checks .checks
.iter() .iter()
.all(|c| matches!(c.level, hammer_agent::orchestrator::CheckLevel::Pass)), .all(|c| matches!(c.level, takana_agent::orchestrator::CheckLevel::Pass)),
"todas las checks deben pasar" "todas las checks deben pasar"
); );
@@ -10,7 +10,7 @@ use std::path::PathBuf;
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
use hammer_core::proto::{Cap, Command, Event, Peer, RecipeInline, PROTOCOL_VERSION}; use takana_core::proto::{Cap, Command, Event, Peer, RecipeInline, PROTOCOL_VERSION};
fn start_stub_bus(sock: PathBuf) -> std::sync::mpsc::Sender<Event> { fn start_stub_bus(sock: PathBuf) -> std::sync::mpsc::Sender<Event> {
let (tx_async, rx_async) = std::sync::mpsc::channel::<Event>(); let (tx_async, rx_async) = std::sync::mpsc::channel::<Event>();
@@ -86,14 +86,14 @@ fn stub_serve(stream: UnixStream, rx_async: std::sync::mpsc::Receiver<Event>) {
// El stub refleja la evidencia declarada como un veredicto "todo pasa" para // El stub refleja la evidencia declarada como un veredicto "todo pasa" para
// ejercitar el camino H1c del cliente (sin lab real que la ejecute). // ejercitar el camino H1c del cliente (sin lab real que la ejecute).
let verdict = (!recipe.evidence.is_empty()).then(|| { let verdict = (!recipe.evidence.is_empty()).then(|| {
hammer_core::proto::EvidenceVerdict { takana_core::proto::EvidenceVerdict {
all_passed: true, all_passed: true,
max_level: recipe.evidence.checks.iter().map(|c| c.kind).max(), max_level: recipe.evidence.checks.iter().map(|c| c.kind).max(),
checks: recipe checks: recipe
.evidence .evidence
.checks .checks
.iter() .iter()
.map(|c| hammer_core::proto::EvidenceCheckVerdict { .map(|c| takana_core::proto::EvidenceCheckVerdict {
kind: c.kind, kind: c.kind,
cmd: c.cmd.clone(), cmd: c.cmd.clone(),
passed: true, passed: true,
@@ -152,7 +152,7 @@ fn client_handshake_compile_and_async_modified() {
thread::sleep(Duration::from_millis(10)); thread::sleep(Duration::from_millis(10));
} }
let mut client = hammer_agent::AgentClient::connect(&sock).expect("connect"); let mut client = takana_agent::AgentClient::connect(&sock).expect("connect");
assert_eq!(client.welcome.ver, PROTOCOL_VERSION); assert_eq!(client.welcome.ver, PROTOCOL_VERSION);
assert!(client.welcome.caps.contains(&Cap::Compile)); assert!(client.welcome.caps.contains(&Cap::Compile));
@@ -167,9 +167,9 @@ fn client_handshake_compile_and_async_modified() {
target: "x86_64-linux-musl".into(), target: "x86_64-linux-musl".into(),
link: "static".into(), link: "static".into(),
flags: vec![], flags: vec![],
evidence: hammer_core::Evidence { evidence: takana_core::Evidence {
checks: vec![hammer_core::EvidenceCheck { checks: vec![takana_core::EvidenceCheck {
kind: hammer_core::EvidenceKind::CmdExit, kind: takana_core::EvidenceKind::CmdExit,
cmd: "grep --version".into(), cmd: "grep --version".into(),
expected_exit: 0, expected_exit: 0,
expected_output: None, expected_output: None,
@@ -184,7 +184,7 @@ fn client_handshake_compile_and_async_modified() {
let verdict = outcome.verdict.expect("verdict presente (declaramos evidencia)"); let verdict = outcome.verdict.expect("verdict presente (declaramos evidencia)");
assert!(verdict.all_passed); assert!(verdict.all_passed);
assert_eq!(verdict.checks.len(), 1); assert_eq!(verdict.checks.len(), 1);
assert_eq!(verdict.max_level, Some(hammer_core::EvidenceKind::CmdExit)); assert_eq!(verdict.max_level, Some(takana_core::EvidenceKind::CmdExit));
// El stub también puede empujar eventos asíncronos. Inyectamos un Modified y vemos // El stub también puede empujar eventos asíncronos. Inyectamos un Modified y vemos
// que el cliente lo entrega por drain_async()/next_async(). // que el cliente lo entrega por drain_async()/next_async().
@@ -10,13 +10,13 @@ use std::path::PathBuf;
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
use hammer_agent::orchestrator::{ApplyTarget, CompileMode, Orchestrator}; use takana_agent::orchestrator::{ApplyTarget, CompileMode, Orchestrator};
use hammer_agent::MockTranslator; use takana_agent::MockTranslator;
use hammer_core::proto::{ use takana_core::proto::{
Cap, Command, Event, EvidenceCheckVerdict, EvidenceVerdict, Peer, PROTOCOL_VERSION, Cap, Command, Event, EvidenceCheckVerdict, EvidenceVerdict, Peer, PROTOCOL_VERSION,
}; };
use hammer_core::swm::{Base, Mutation, Swm, SwmBuild}; use takana_core::swm::{Base, Mutation, Swm, SwmBuild};
use hammer_core::{BaseRef, Evidence, EvidenceCheck, EvidenceKind}; use takana_core::{BaseRef, Evidence, EvidenceCheck, EvidenceKind};
use std::collections::BTreeMap; use std::collections::BTreeMap;
/// Bus stub que responde a `Compile` con `BuildReady` cuyo veredicto tiene `all_passed=false`. /// Bus stub que responde a `Compile` con `BuildReady` cuyo veredicto tiene `all_passed=false`.
@@ -11,12 +11,12 @@ use std::sync::{Arc, Mutex};
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
use hammer_core::proto::{Cap, Command, Event, Peer, PROTOCOL_VERSION}; use takana_core::proto::{Cap, Command, Event, Peer, PROTOCOL_VERSION};
use hammer_core::swm::{Base, Mutation}; use takana_core::swm::{Base, Mutation};
use hammer_core::{BaseRef, Swm}; use takana_core::{BaseRef, Swm};
use hammer_agent::orchestrator::{ApplyTarget, CompileMode, RepairPolicy}; use takana_agent::orchestrator::{ApplyTarget, CompileMode, RepairPolicy};
use hammer_agent::{MockTranslator, Orchestrator}; use takana_agent::{MockTranslator, Orchestrator};
/// El plan por conexión: lista de eventos a empujar tras el Welcome. El stub itera /// El plan por conexión: lista de eventos a empujar tras el Welcome. El stub itera
/// por las conexiones en orden y consume un slot del plan por cada una. /// por las conexiones en orden y consume un slot del plan por cada una.
@@ -112,7 +112,7 @@ fn swm_touch(path: &str) -> Swm {
mutations: vec![Mutation::FileDrop { mutations: vec![Mutation::FileDrop {
path: path.into(), path: path.into(),
// hash de "X" (un byte 0x58) — calculado con BLAKE3. // hash de "X" (un byte 0x58) — calculado con BLAKE3.
content_hash: hammer_core::ArtifactHash::of_inputs(&[b"X"]).as_str().to_string(), content_hash: takana_core::ArtifactHash::of_inputs(&[b"X"]).as_str().to_string(),
content_b64: Some("WA==".into()), content_b64: Some("WA==".into()),
content_url: None, content_url: None,
}], }],
@@ -1,5 +1,5 @@
[package] [package]
name = "hammer-bootstrap" name = "takana-bootstrap"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
@@ -8,8 +8,8 @@ repository.workspace = true
description = "Bootstrap from-scratch de hammer: Stage 0/1/2 hacia el auto-alojamiento (SDD 11)." description = "Bootstrap from-scratch de hammer: Stage 0/1/2 hacia el auto-alojamiento (SDD 11)."
[dependencies] [dependencies]
hammer-core.workspace = true takana-core.workspace = true
hammer-build.workspace = true takana-build.workspace = true
anyhow.workspace = true anyhow.workspace = true
thiserror.workspace = true thiserror.workspace = true
tracing.workspace = true tracing.workspace = true
@@ -17,8 +17,8 @@ pub mod manifest;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
use hammer_build::download; use takana_build::download;
use hammer_core::{ArtifactHash, Recipe, Store}; use takana_core::{ArtifactHash, Recipe, Store};
pub use manifest::{BootstrapManifest, StageEntry}; pub use manifest::{BootstrapManifest, StageEntry};
@@ -27,7 +27,7 @@ pub enum Error {
#[error("io: {0}")] #[error("io: {0}")]
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
#[error("core: {0}")] #[error("core: {0}")]
Core(#[from] hammer_core::Error), Core(#[from] takana_core::Error),
#[error("tar -x de la semilla falló (exit {0:?})")] #[error("tar -x de la semilla falló (exit {0:?})")]
Untar(Option<i32>), Untar(Option<i32>),
#[error("bootstrap: {0}")] #[error("bootstrap: {0}")]
@@ -147,17 +147,17 @@ fn locate_tool_dir(base: &Path, tool: &str) -> Result<PathBuf> {
} }
} }
/// Deriva una [`hammer_build::BuildConfig`] desde `base` cuyo `zig_dir` apunta a la **semilla /// Deriva una [`takana_build::BuildConfig`] desde `base` cuyo `zig_dir` apunta a la **semilla
/// sellada** en vez del toolchain del host: así las recetas del Stage 1 cross-compilan con el /// sellada** en vez del toolchain del host: así las recetas del Stage 1 cross-compilan con el
/// toolchain fijado por hash (`seed_hash`), no con el `.dev-fs/tools/zig` de desarrollo. El resto /// toolchain fijado por hash (`seed_hash`), no con el `.dev-fs/tools/zig` de desarrollo. El resto
/// de la config (rootfs, work, cache) se hereda intacto. /// de la config (rootfs, work, cache) se hereda intacto.
pub fn seed_build_config( pub fn seed_build_config(
base: &hammer_build::BuildConfig, base: &takana_build::BuildConfig,
seed: &SeedSpec, seed: &SeedSpec,
store: &Store, store: &Store,
) -> Result<hammer_build::BuildConfig> { ) -> Result<takana_build::BuildConfig> {
let zig_dir = seed.toolchain_dir(store)?; let zig_dir = seed.toolchain_dir(store)?;
Ok(hammer_build::BuildConfig { zig_dir, ..base.clone() }) Ok(takana_build::BuildConfig { zig_dir, ..base.clone() })
} }
/// Hash de un rootfs sellado por Stage 1. Es un `ArtifactHash` derivado del **contenido lógico** /// Hash de un rootfs sellado por Stage 1. Es un `ArtifactHash` derivado del **contenido lógico**
@@ -265,15 +265,15 @@ pub struct Stage1Spec {
/// ///
/// Idempotente: si el rootfs (mismo hash de contenido) ya está sellado, no reconstruye. El /// Idempotente: si el rootfs (mismo hash de contenido) ya está sellado, no reconstruye. El
/// cross-compile real requiere el rootfs base + toolchain del lab; en una máquina sin `.dev-fs` /// cross-compile real requiere el rootfs base + toolchain del lab; en una máquina sin `.dev-fs`
/// `hammer_build::build` falla limpio antes de ensamblar nada. /// `takana_build::build` falla limpio antes de ensamblar nada.
pub fn stage1( pub fn stage1(
spec: &Stage1Spec, spec: &Stage1Spec,
base_cfg: &hammer_build::BuildConfig, base_cfg: &takana_build::BuildConfig,
store: &Store, store: &Store,
) -> Result<RootfsHash> { ) -> Result<RootfsHash> {
// Toolchain = la semilla sellada, no el zig del host. // Toolchain = la semilla sellada, no el zig del host.
let zig_dir = seed_toolchain_dir(store, &spec.seed_hash, spec.seed_kind)?; let zig_dir = seed_toolchain_dir(store, &spec.seed_hash, spec.seed_kind)?;
let cfg = hammer_build::BuildConfig { zig_dir, ..base_cfg.clone() }; let cfg = takana_build::BuildConfig { zig_dir, ..base_cfg.clone() };
// 1) Construir cada componente (cacheado por el lab si ya existe). // 1) Construir cada componente (cacheado por el lab si ya existe).
let mut components: Vec<(String, ArtifactHash)> = Vec::with_capacity(STAGE1_COMPONENTS.len()); let mut components: Vec<(String, ArtifactHash)> = Vec::with_capacity(STAGE1_COMPONENTS.len());
@@ -282,7 +282,7 @@ pub fn stage1(
let recipe = Recipe::load_from_path(&recipe_path).map_err(|e| { let recipe = Recipe::load_from_path(&recipe_path).map_err(|e| {
Error::Other(format!("receta '{name}' ({}): {e}", recipe_path.display())) Error::Other(format!("receta '{name}' ({}): {e}", recipe_path.display()))
})?; })?;
let h = hammer_build::build(&recipe, &cfg, store)?; let h = takana_build::build(&recipe, &cfg, store)?;
components.push((name.to_string(), h)); components.push((name.to_string(), h));
} }
@@ -367,7 +367,7 @@ fn assemble_rootfs(
// Cada componente se proyecta al rootfs por hardlink (mismo store, mismo filesystem). // Cada componente se proyecta al rootfs por hardlink (mismo store, mismo filesystem).
for (name, h) in components { for (name, h) in components {
let dir = store.path_of(h, name); let dir = store.path_of(h, name);
hammer_build::run_hydrate(&dir, staging, hammer_core::LinkMode::Static, None)?; takana_build::run_hydrate(&dir, staging, takana_core::LinkMode::Static, None)?;
} }
// El sidecar de provenance de cada receta (`.hammer/recipe.toml`) no pertenece al rootfs // El sidecar de provenance de cada receta (`.hammer/recipe.toml`) no pertenece al rootfs
@@ -605,14 +605,14 @@ fn assemble_product_rootfs(
base_dir.display() base_dir.display()
))); )));
} }
hammer_build::run_hydrate(&base_dir, staging, hammer_core::LinkMode::Static, None)?; takana_build::run_hydrate(&base_dir, staging, takana_core::LinkMode::Static, None)?;
// 2) userland Rust + servicios ENCIMA (hardlink). El userland va DESPUÉS de la base: sus symlinks // 2) userland Rust + servicios ENCIMA (hardlink). El userland va DESPUÉS de la base: sus symlinks
// en /usr/bin reemplazan a los de busybox (run_hydrate sustituye el symlink existente) ⇒ el // en /usr/bin reemplazan a los de busybox (run_hydrate sustituye el symlink existente) ⇒ el
// producto corre las tools Rust. Orden userland→servicios (sin colisiones entre sí). // producto corre las tools Rust. Orden userland→servicios (sin colisiones entre sí).
for (name, h) in userland.iter().chain(services.iter()) { for (name, h) in userland.iter().chain(services.iter()) {
let dir = store.path_of(h, name); let dir = store.path_of(h, name);
hammer_build::run_hydrate(&dir, staging, hammer_core::LinkMode::Static, None)?; takana_build::run_hydrate(&dir, staging, takana_core::LinkMode::Static, None)?;
} }
let _ = std::fs::remove_dir_all(staging.join(".hammer")); let _ = std::fs::remove_dir_all(staging.join(".hammer"));
@@ -681,7 +681,7 @@ fn assemble_product_rootfs(
fn build_components( fn build_components(
names: &[&str], names: &[&str],
recipes_dir: &Path, recipes_dir: &Path,
cfg: &hammer_build::BuildConfig, cfg: &takana_build::BuildConfig,
store: &Store, store: &Store,
) -> Result<Vec<(String, ArtifactHash)>> { ) -> Result<Vec<(String, ArtifactHash)>> {
let mut out = Vec::with_capacity(names.len()); let mut out = Vec::with_capacity(names.len());
@@ -690,7 +690,7 @@ fn build_components(
let recipe = Recipe::load_from_path(&recipe_path).map_err(|e| { let recipe = Recipe::load_from_path(&recipe_path).map_err(|e| {
Error::Other(format!("receta '{name}' ({}): {e}", recipe_path.display())) Error::Other(format!("receta '{name}' ({}): {e}", recipe_path.display()))
})?; })?;
let h = hammer_build::build(&recipe, cfg, store)?; let h = takana_build::build(&recipe, cfg, store)?;
out.push((name.to_string(), h)); out.push((name.to_string(), h));
} }
Ok(out) Ok(out)
@@ -703,7 +703,7 @@ fn build_components(
/// semilla): cada componente usa su `zig_version` propia, y si ya está sellado sale cacheado. /// semilla): cada componente usa su `zig_version` propia, y si ya está sellado sale cacheado.
pub fn product( pub fn product(
base: &RootfsHash, base: &RootfsHash,
base_cfg: &hammer_build::BuildConfig, base_cfg: &takana_build::BuildConfig,
recipes_dir: &Path, recipes_dir: &Path,
store: &Store, store: &Store,
) -> Result<RootfsHash> { ) -> Result<RootfsHash> {
@@ -725,7 +725,7 @@ pub fn product_from_repo(
base: &RootfsHash, base: &RootfsHash,
repo_dir: &Path, repo_dir: &Path,
trust_dir: &Path, trust_dir: &Path,
base_cfg: &hammer_build::BuildConfig, base_cfg: &takana_build::BuildConfig,
store: &Store, store: &Store,
) -> Result<RootfsHash> { ) -> Result<RootfsHash> {
// 1) userland + servicios REPRODUCIDOS desde el repo firmado (mismo shape que build_components). // 1) userland + servicios REPRODUCIDOS desde el repo firmado (mismo shape que build_components).
@@ -780,10 +780,10 @@ pub fn install_components_from_repo(
names: &[&str], names: &[&str],
repo_dir: &Path, repo_dir: &Path,
trust_dir: &Path, trust_dir: &Path,
cfg: &hammer_build::BuildConfig, cfg: &takana_build::BuildConfig,
store: &Store, store: &Store,
) -> Result<Vec<(String, ArtifactHash)>> { ) -> Result<Vec<(String, ArtifactHash)>> {
use hammer_core::{RepoIndex, SigStatus, TrustStore}; use takana_core::{RepoIndex, SigStatus, TrustStore};
let index = RepoIndex::load(repo_dir)?; let index = RepoIndex::load(repo_dir)?;
// Firma del release: exigimos TRUSTED. Un release no firmado / con clave desconocida / con firma // Firma del release: exigimos TRUSTED. Un release no firmado / con clave desconocida / con firma
@@ -801,7 +801,7 @@ pub fn install_components_from_repo(
} }
} }
let catalog_dir = hammer_build::swm_bridge::catalog_dir_for(cfg, None); let catalog_dir = takana_build::swm_bridge::catalog_dir_for(cfg, None);
std::fs::create_dir_all(&catalog_dir)?; std::fs::create_dir_all(&catalog_dir)?;
let mut out = Vec::with_capacity(names.len()); let mut out = Vec::with_capacity(names.len());
@@ -817,7 +817,7 @@ pub fn install_components_from_repo(
let mutation = source_patch_of(&dep_swm).ok_or_else(|| { let mutation = source_patch_of(&dep_swm).ok_or_else(|| {
Error::Other(format!("la dep '{}' no es un paquete source_patch", dep.name)) Error::Other(format!("la dep '{}' no es un paquete source_patch", dep.name))
})?; })?;
let dep_recipe = hammer_build::swm_bridge::recipe_from_source_patch( let dep_recipe = takana_build::swm_bridge::recipe_from_source_patch(
mutation, mutation,
Some(&dep.name), Some(&dep.name),
&catalog_dir, &catalog_dir,
@@ -829,7 +829,7 @@ pub fn install_components_from_repo(
let mutation = source_patch_of(&swm).ok_or_else(|| { let mutation = source_patch_of(&swm).ok_or_else(|| {
Error::Other(format!("'{name}' no es un paquete source_patch")) Error::Other(format!("'{name}' no es un paquete source_patch"))
})?; })?;
let hash = hammer_build::swm_bridge::build_source_patch(mutation, cfg, store, None, Some(name))?; let hash = takana_build::swm_bridge::build_source_patch(mutation, cfg, store, None, Some(name))?;
tracing::info!(%name, %hash, "product: componente reproducido desde el repo firmado"); tracing::info!(%name, %hash, "product: componente reproducido desde el repo firmado");
out.push((name.to_string(), hash)); out.push((name.to_string(), hash));
} }
@@ -837,18 +837,18 @@ pub fn install_components_from_repo(
} }
/// Lee un `.swm` (YAML) de disco a `Swm`. Equivalente al helper homónimo de la CLI (privado allí). /// Lee un `.swm` (YAML) de disco a `Swm`. Equivalente al helper homónimo de la CLI (privado allí).
fn load_swm_file(path: &Path) -> Result<hammer_core::Swm> { fn load_swm_file(path: &Path) -> Result<takana_core::Swm> {
let yaml = std::fs::read_to_string(path) let yaml = std::fs::read_to_string(path)
.map_err(|e| Error::Other(format!("no pude leer {}: {e}", path.display())))?; .map_err(|e| Error::Other(format!("no pude leer {}: {e}", path.display())))?;
hammer_core::Swm::from_yaml(&yaml) takana_core::Swm::from_yaml(&yaml)
.map_err(|e| Error::Other(format!("no pude parsear {}: {e}", path.display()))) .map_err(|e| Error::Other(format!("no pude parsear {}: {e}", path.display())))
} }
/// Primera mutación `SourcePatch` de un `.swm` (un paquete hammer es un único source_patch). /// Primera mutación `SourcePatch` de un `.swm` (un paquete hammer es un único source_patch).
fn source_patch_of(swm: &hammer_core::Swm) -> Option<&hammer_core::Mutation> { fn source_patch_of(swm: &takana_core::Swm) -> Option<&takana_core::Mutation> {
swm.mutations swm.mutations
.iter() .iter()
.find(|m| matches!(m, hammer_core::Mutation::SourcePatch { .. })) .find(|m| matches!(m, takana_core::Mutation::SourcePatch { .. }))
} }
// ── Producto ATESTADO: la capa FIRMADA que el gate de arje verifica al boot (I4 / Etapa D) ─────── // ── Producto ATESTADO: la capa FIRMADA que el gate de arje verifica al boot (I4 / Etapa D) ───────
@@ -966,7 +966,7 @@ fn assemble_attested_product_rootfs(
base_dir.display() base_dir.display()
))); )));
} }
hammer_build::run_hydrate(&base_dir, staging, hammer_core::LinkMode::Static, None)?; takana_build::run_hydrate(&base_dir, staging, takana_core::LinkMode::Static, None)?;
let _ = std::fs::remove_dir_all(staging.join(".hammer")); let _ = std::fs::remove_dir_all(staging.join(".hammer"));
// 2) PISAR /usr/bin/arje-zero con el init CON gate. Es un hardlink read-only al store ⇒ romper el // 2) PISAR /usr/bin/arje-zero con el init CON gate. Es un hardlink read-only al store ⇒ romper el
@@ -1071,7 +1071,7 @@ fn assemble_attested_product_rootfs(
/// base quedan intactos (Separación Mecanismo/Política): el atestado es un sello APARTE. /// base quedan intactos (Separación Mecanismo/Política): el atestado es un sello APARTE.
pub fn product_attested( pub fn product_attested(
base: &RootfsHash, base: &RootfsHash,
base_cfg: &hammer_build::BuildConfig, base_cfg: &takana_build::BuildConfig,
recipes_dir: &Path, recipes_dir: &Path,
store: &Store, store: &Store,
cfg: &AttestConfig, cfg: &AttestConfig,
@@ -1245,7 +1245,7 @@ pub struct AllReport {
pub fn all( pub fn all(
seed: &SeedSpec, seed: &SeedSpec,
recipes_dir: &Path, recipes_dir: &Path,
base_cfg: &hammer_build::BuildConfig, base_cfg: &takana_build::BuildConfig,
store: &Store, store: &Store,
) -> Result<AllReport> { ) -> Result<AllReport> {
let seed_hash = stage0(seed, store)?; let seed_hash = stage0(seed, store)?;
@@ -1505,7 +1505,7 @@ fn assemble_builder(spec: &BuilderSpec, store: &Store, staging: &Path) -> Result
stage1_dir.display() stage1_dir.display()
))); )));
} }
hammer_build::run_hydrate(&stage1_dir, staging, hammer_core::LinkMode::Static, None)?; takana_build::run_hydrate(&stage1_dir, staging, takana_core::LinkMode::Static, None)?;
// 2) Esqueleto del builder: el sandbox de build (/toolchain), el store, el work y la config. // 2) Esqueleto del builder: el sandbox de build (/toolchain), el store, el work y la config.
for d in ["toolchain", "store", "work", "usr/bin", "etc/hammer/recipes"] { for d in ["toolchain", "store", "work", "usr/bin", "etc/hammer/recipes"] {
@@ -1953,7 +1953,7 @@ mod tests {
fn seed_build_config_points_zig_dir_at_seed() { fn seed_build_config_points_zig_dir_at_seed() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let (store, seed) = seal_seed_with_zig_at(tmp.path(), "zig-linux-x86_64-0.13.0/zig"); let (store, seed) = seal_seed_with_zig_at(tmp.path(), "zig-linux-x86_64-0.13.0/zig");
let base = hammer_build::BuildConfig::defaults_for_store(store.root()); let base = takana_build::BuildConfig::defaults_for_store(store.root());
let cfg = seed_build_config(&base, &seed, &store).unwrap(); let cfg = seed_build_config(&base, &seed, &store).unwrap();
assert_eq!(cfg.zig_dir, seed.toolchain_dir(&store).unwrap()); assert_eq!(cfg.zig_dir, seed.toolchain_dir(&store).unwrap());
// El resto de la config se hereda sin cambios. // El resto de la config se hereda sin cambios.
@@ -2360,7 +2360,7 @@ open(os.path.join(os.path.dirname(out),"..","..","bins-seen.txt"),"w").write("\n
let path = recipes_dir.join(format!("{name}.toml")); let path = recipes_dir.join(format!("{name}.toml"));
let r = Recipe::load_from_path(&path).unwrap_or_else(|e| panic!("parse {name}: {e}")); let r = Recipe::load_from_path(&path).unwrap_or_else(|e| panic!("parse {name}: {e}"));
assert_eq!(&r.name, name); assert_eq!(&r.name, name);
let h = hammer_build::artifact_hash(&r, &store, &hammer_core::lab::LabFingerprint::for_tests()).expect("hash"); let h = takana_build::artifact_hash(&r, &store, &takana_core::lab::LabFingerprint::for_tests()).expect("hash");
assert!(h.as_str().starts_with("b3:")); assert!(h.as_str().starts_with("b3:"));
} }
} }
@@ -2375,7 +2375,7 @@ open(os.path.join(os.path.dirname(out),"..","..","bins-seen.txt"),"w").write("\n
let path = recipes_dir.join(format!("{name}.toml")); let path = recipes_dir.join(format!("{name}.toml"));
let r = Recipe::load_from_path(&path).unwrap_or_else(|e| panic!("parse {name}: {e}")); let r = Recipe::load_from_path(&path).unwrap_or_else(|e| panic!("parse {name}: {e}"));
assert_eq!(&r.name, name); assert_eq!(&r.name, name);
let h = hammer_build::artifact_hash(&r, &store, &hammer_core::lab::LabFingerprint::for_tests()).expect("hash"); let h = takana_build::artifact_hash(&r, &store, &takana_core::lab::LabFingerprint::for_tests()).expect("hash");
assert!(h.as_str().starts_with("b3:"), "hash con prefijo b3:"); assert!(h.as_str().starts_with("b3:"), "hash con prefijo b3:");
} }
} }
@@ -2696,7 +2696,7 @@ open(os.path.join(os.path.dirname(out),"..","..","bins-seen.txt"),"w").write("\n
} }
let r = Recipe::load_from_path(&path) let r = Recipe::load_from_path(&path)
.unwrap_or_else(|e| panic!("parse {}: {e}", path.display())); .unwrap_or_else(|e| panic!("parse {}: {e}", path.display()));
let h = hammer_build::artifact_hash(&r, &store, &hammer_core::lab::LabFingerprint::for_tests()) let h = takana_build::artifact_hash(&r, &store, &takana_core::lab::LabFingerprint::for_tests())
.unwrap_or_else(|e| panic!("hash {}: {e}", path.display())); .unwrap_or_else(|e| panic!("hash {}: {e}", path.display()));
assert!(h.as_str().starts_with("b3:"), "{}: hash con prefijo b3:", path.display()); assert!(h.as_str().starts_with("b3:"), "{}: hash con prefijo b3:", path.display());
count += 1; count += 1;
@@ -10,7 +10,7 @@
use std::path::PathBuf; use std::path::PathBuf;
use hammer_core::{ArtifactHash, Store}; use takana_core::{ArtifactHash, Store};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{Error, Result}; use crate::{Error, Result};
@@ -1,5 +1,5 @@
[package] [package]
name = "hammer-build" name = "takana-build"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
@@ -8,7 +8,7 @@ repository.workspace = true
description = "El laboratorio de hammer: sandbox de build (bubblewrap + zig cc) e hidratación." description = "El laboratorio de hammer: sandbox de build (bubblewrap + zig cc) e hidratación."
[dependencies] [dependencies]
hammer-core.workspace = true takana-core.workspace = true
anyhow.workspace = true anyhow.workspace = true
thiserror.workspace = true thiserror.workspace = true
tracing.workspace = true tracing.workspace = true
@@ -12,7 +12,7 @@ use std::process::Command;
/// Descarga `url` y devuelve su contenido en memoria. Falla con el stderr de curl si el /// Descarga `url` y devuelve su contenido en memoria. Falla con el stderr de curl si el
/// código de salida no es 0 (incluye errores HTTP gracias a `-f`). /// código de salida no es 0 (incluye errores HTTP gracias a `-f`).
pub fn fetch_url_bytes(url: &str) -> hammer_core::Result<Vec<u8>> { pub fn fetch_url_bytes(url: &str) -> takana_core::Result<Vec<u8>> {
let out = Command::new("curl") let out = Command::new("curl")
.args([ .args([
"-fsSL", "-fsSL",
@@ -25,9 +25,9 @@ pub fn fetch_url_bytes(url: &str) -> hammer_core::Result<Vec<u8>> {
url, url,
]) ])
.output() .output()
.map_err(|e| hammer_core::Error::Other(anyhow::anyhow!("spawn curl: {e}")))?; .map_err(|e| takana_core::Error::Other(anyhow::anyhow!("spawn curl: {e}")))?;
if !out.status.success() { if !out.status.success() {
return Err(hammer_core::Error::Other(anyhow::anyhow!( return Err(takana_core::Error::Other(anyhow::anyhow!(
"curl {url} falló (exit {}): {}", "curl {url} falló (exit {}): {}",
out.status.code().map(|c| c.to_string()).unwrap_or_else(|| "?".into()), out.status.code().map(|c| c.to_string()).unwrap_or_else(|| "?".into()),
String::from_utf8_lossy(&out.stderr).trim() String::from_utf8_lossy(&out.stderr).trim()
@@ -37,7 +37,7 @@ pub fn fetch_url_bytes(url: &str) -> hammer_core::Result<Vec<u8>> {
} }
/// Descarga `url` y lo escribe en `dst` (creando los directorios padre). /// Descarga `url` y lo escribe en `dst` (creando los directorios padre).
pub fn fetch_url_to_file(url: &str, dst: &Path) -> hammer_core::Result<()> { pub fn fetch_url_to_file(url: &str, dst: &Path) -> takana_core::Result<()> {
let bytes = fetch_url_bytes(url)?; let bytes = fetch_url_bytes(url)?;
if let Some(parent) = dst.parent() { if let Some(parent) = dst.parent() {
std::fs::create_dir_all(parent)?; std::fs::create_dir_all(parent)?;
@@ -49,14 +49,14 @@ pub fn fetch_url_to_file(url: &str, dst: &Path) -> hammer_core::Result<()> {
/// Verifica que el sha256 de `path` coincide con `expected` (hex, case-insensitive). Lo usan /// Verifica que el sha256 de `path` coincide con `expected` (hex, case-insensitive). Lo usan
/// los consumidores que ingieren una fuente fijada (p. ej. el toolchain semilla del bootstrap, /// los consumidores que ingieren una fuente fijada (p. ej. el toolchain semilla del bootstrap,
/// [SDD 11](../../docs/11-bootstrap.md)) y necesitan comprobar integridad **antes** de sellar. /// [SDD 11](../../docs/11-bootstrap.md)) y necesitan comprobar integridad **antes** de sellar.
pub fn verify_sha256(path: &Path, expected: &str) -> hammer_core::Result<()> { pub fn verify_sha256(path: &Path, expected: &str) -> takana_core::Result<()> {
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
let mut f = std::fs::File::open(path)?; let mut f = std::fs::File::open(path)?;
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
std::io::copy(&mut f, &mut hasher)?; std::io::copy(&mut f, &mut hasher)?;
let got = hex::encode(hasher.finalize()); let got = hex::encode(hasher.finalize());
if !got.eq_ignore_ascii_case(expected) { if !got.eq_ignore_ascii_case(expected) {
return Err(hammer_core::Error::Other(anyhow::anyhow!( return Err(takana_core::Error::Other(anyhow::anyhow!(
"sha256 mismatch en {}: esperado {expected}, obtenido {got}", "sha256 mismatch en {}: esperado {expected}, obtenido {got}",
path.display() path.display()
))); )));
@@ -1,6 +1,6 @@
//! Fase `fetch`: materializa el árbol fuente de la receta en un directorio trabajable. //! Fase `fetch`: materializa el árbol fuente de la receta en un directorio trabajable.
//! //!
//! Dos modos, decididos por la receta (ver `hammer_core::SourceKind`): //! Dos modos, decididos por la receta (ver `takana_core::SourceKind`):
//! //!
//! * **Git** — mirror local en `<work_root>/repos/<name>.git` + `git archive` del commit //! * **Git** — mirror local en `<work_root>/repos/<name>.git` + `git archive` del commit
//! fijado al árbol trabajable. El mirror se reusa entre builds; el árbol es descartable. //! fijado al árbol trabajable. El mirror se reusa entre builds; el árbol es descartable.
@@ -14,10 +14,10 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
use hammer_core::{Recipe, SourceKind}; use takana_core::{Recipe, SourceKind};
/// Materializa el árbol fuente en un directorio trabajable y devuelve su ruta. /// Materializa el árbol fuente en un directorio trabajable y devuelve su ruta.
pub fn fetch(recipe: &Recipe, work_root: &Path) -> hammer_core::Result<PathBuf> { pub fn fetch(recipe: &Recipe, work_root: &Path) -> takana_core::Result<PathBuf> {
match recipe.source.kind()? { match recipe.source.kind()? {
SourceKind::Git { repo, commit } => fetch_git(recipe, repo, commit, work_root), SourceKind::Git { repo, commit } => fetch_git(recipe, repo, commit, work_root),
SourceKind::Tarball { url, sha256 } => fetch_tarball(recipe, url, sha256, work_root), SourceKind::Tarball { url, sha256 } => fetch_tarball(recipe, url, sha256, work_root),
@@ -34,16 +34,16 @@ pub fn fetch(recipe: &Recipe, work_root: &Path) -> hammer_core::Result<PathBuf>
/// ///
/// El nombre del workspace lleva el hash del CONTENIDO y no el de la ruta: editar el overlay cambia /// El nombre del workspace lleva el hash del CONTENIDO y no el de la ruta: editar el overlay cambia
/// de directorio, así que un árbol viejo nunca se reusa por accidente. /// de directorio, así que un árbol viejo nunca se reusa por accidente.
fn fetch_dir(recipe: &Recipe, dir: &str, work_root: &Path) -> hammer_core::Result<PathBuf> { fn fetch_dir(recipe: &Recipe, dir: &str, work_root: &Path) -> takana_core::Result<PathBuf> {
let origen = recipe.base_dir.join(dir); let origen = recipe.base_dir.join(dir);
if !origen.is_dir() { if !origen.is_dir() {
return Err(hammer_core::Error::Other(anyhow::anyhow!( return Err(takana_core::Error::Other(anyhow::anyhow!(
"source.dir apunta a {} y no es un directorio", "source.dir apunta a {} y no es un directorio",
origen.display() origen.display()
))); )));
} }
let h = hammer_core::ArtifactHash::of_tree(&origen) let h = takana_core::ArtifactHash::of_tree(&origen)
.map_err(|e| hammer_core::Error::Other(anyhow::anyhow!("hash del árbol {}: {e}", origen.display())))?; .map_err(|e| takana_core::Error::Other(anyhow::anyhow!("hash del árbol {}: {e}", origen.display())))?;
let bare = h.as_str().strip_prefix("b3:").unwrap_or(h.as_str()); let bare = h.as_str().strip_prefix("b3:").unwrap_or(h.as_str());
let sources_dir = work_root.join("sources"); let sources_dir = work_root.join("sources");
std::fs::create_dir_all(&sources_dir)?; std::fs::create_dir_all(&sources_dir)?;
@@ -57,9 +57,9 @@ fn fetch_dir(recipe: &Recipe, dir: &str, work_root: &Path) -> hammer_core::Resul
.arg(format!("{}/.", origen.display())) .arg(format!("{}/.", origen.display()))
.arg(&work_tree) .arg(&work_tree)
.status() .status()
.map_err(|e| hammer_core::Error::Other(anyhow::anyhow!("spawn cp -a del source.dir: {e}")))?; .map_err(|e| takana_core::Error::Other(anyhow::anyhow!("spawn cp -a del source.dir: {e}")))?;
if !st.success() { if !st.success() {
return Err(hammer_core::Error::Other(anyhow::anyhow!( return Err(takana_core::Error::Other(anyhow::anyhow!(
"cp -a de {} falló", "cp -a de {} falló",
origen.display() origen.display()
))); )));
@@ -72,7 +72,7 @@ fn fetch_git(
repo: &str, repo: &str,
commit: &str, commit: &str,
work_root: &Path, work_root: &Path,
) -> hammer_core::Result<PathBuf> { ) -> takana_core::Result<PathBuf> {
let repos_dir = work_root.join("repos"); let repos_dir = work_root.join("repos");
let sources_dir = work_root.join("sources"); let sources_dir = work_root.join("sources");
std::fs::create_dir_all(&repos_dir)?; std::fs::create_dir_all(&repos_dir)?;
@@ -105,7 +105,7 @@ fn fetch_git(
if run_git(&["cat-file", "-e", commit], Some(&mirror)).is_err() { if run_git(&["cat-file", "-e", commit], Some(&mirror)).is_err() {
run_git(&["fetch", "--all", "--tags"], Some(&mirror))?; run_git(&["fetch", "--all", "--tags"], Some(&mirror))?;
run_git(&["cat-file", "-e", commit], Some(&mirror)).map_err(|_| { run_git(&["cat-file", "-e", commit], Some(&mirror)).map_err(|_| {
hammer_core::Error::Other(anyhow::anyhow!( takana_core::Error::Other(anyhow::anyhow!(
"commit {commit} no existe en {repo} tras fetch" "commit {commit} no existe en {repo} tras fetch"
)) ))
})?; })?;
@@ -131,7 +131,7 @@ fn fetch_git(
} }
/// `git archive <commit> | tar -x -C <destino>`: materializa UN árbol sin pagar un worktree. /// `git archive <commit> | tar -x -C <destino>`: materializa UN árbol sin pagar un worktree.
fn archivar_en(mirror: &Path, commit: &str, destino: &Path) -> hammer_core::Result<()> { fn archivar_en(mirror: &Path, commit: &str, destino: &Path) -> takana_core::Result<()> {
let archive = Command::new("git") let archive = Command::new("git")
.arg("-C") .arg("-C")
.arg(mirror) .arg(mirror)
@@ -139,7 +139,7 @@ fn archivar_en(mirror: &Path, commit: &str, destino: &Path) -> hammer_core::Resu
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.stderr(Stdio::piped()) .stderr(Stdio::piped())
.spawn() .spawn()
.map_err(|e| hammer_core::Error::Other(anyhow::anyhow!("spawn git archive: {e}")))?; .map_err(|e| takana_core::Error::Other(anyhow::anyhow!("spawn git archive: {e}")))?;
let tar_status = Command::new("tar") let tar_status = Command::new("tar")
.args(["-x", "-C"]) .args(["-x", "-C"])
@@ -148,10 +148,10 @@ fn archivar_en(mirror: &Path, commit: &str, destino: &Path) -> hammer_core::Resu
.stdout(Stdio::inherit()) .stdout(Stdio::inherit())
.stderr(Stdio::inherit()) .stderr(Stdio::inherit())
.status() .status()
.map_err(|e| hammer_core::Error::Other(anyhow::anyhow!("spawn tar -x: {e}")))?; .map_err(|e| takana_core::Error::Other(anyhow::anyhow!("spawn tar -x: {e}")))?;
if !tar_status.success() { if !tar_status.success() {
return Err(hammer_core::Error::Other(anyhow::anyhow!( return Err(takana_core::Error::Other(anyhow::anyhow!(
"git archive | tar -x falló (tar exit {:?})", "git archive | tar -x falló (tar exit {:?})",
tar_status.code() tar_status.code()
))); )));
@@ -184,13 +184,13 @@ fn materializar_submodulos(
repos_dir: &Path, repos_dir: &Path,
url_padre: &str, url_padre: &str,
profundidad: usize, profundidad: usize,
) -> hammer_core::Result<()> { ) -> takana_core::Result<()> {
let gitlinks = gitlinks_de(mirror, commit)?; let gitlinks = gitlinks_de(mirror, commit)?;
if gitlinks.is_empty() { if gitlinks.is_empty() {
return Ok(()); return Ok(());
} }
if profundidad >= PROFUNDIDAD_SUBMODULOS { if profundidad >= PROFUNDIDAD_SUBMODULOS {
return Err(hammer_core::Error::Other(anyhow::anyhow!( return Err(takana_core::Error::Other(anyhow::anyhow!(
"submódulos anidados más de {PROFUNDIDAD_SUBMODULOS} niveles en {url_padre} \ "submódulos anidados más de {PROFUNDIDAD_SUBMODULOS} niveles en {url_padre} \
({commit}): parece un ciclo" ({commit}): parece un ciclo"
))); )));
@@ -200,7 +200,7 @@ fn materializar_submodulos(
for (sha, ruta) in gitlinks { for (sha, ruta) in gitlinks {
let declarada = urls.get(&ruta).ok_or_else(|| { let declarada = urls.get(&ruta).ok_or_else(|| {
hammer_core::Error::Other(anyhow::anyhow!( takana_core::Error::Other(anyhow::anyhow!(
"el commit {commit} declara el submódulo `{ruta}` pero .gitmodules no le da url" "el commit {commit} declara el submódulo `{ruta}` pero .gitmodules no le da url"
)) ))
})?; })?;
@@ -222,7 +222,7 @@ fn materializar_submodulos(
if run_git(&["cat-file", "-e", &sha], Some(&sub_mirror)).is_err() { if run_git(&["cat-file", "-e", &sha], Some(&sub_mirror)).is_err() {
run_git(&["fetch", "--all", "--tags"], Some(&sub_mirror))?; run_git(&["fetch", "--all", "--tags"], Some(&sub_mirror))?;
run_git(&["cat-file", "-e", &sha], Some(&sub_mirror)).map_err(|_| { run_git(&["cat-file", "-e", &sha], Some(&sub_mirror)).map_err(|_| {
hammer_core::Error::Other(anyhow::anyhow!( takana_core::Error::Other(anyhow::anyhow!(
"el submódulo `{ruta}` pide {sha}, que no existe en {url} tras fetch" "el submódulo `{ruta}` pide {sha}, que no existe en {url} tras fetch"
)) ))
})?; })?;
@@ -246,7 +246,7 @@ fn materializar_submodulos(
/// Las entradas de tipo `commit` (modo 160000) del commit: `(sha, ruta)`. /// Las entradas de tipo `commit` (modo 160000) del commit: `(sha, ruta)`.
/// ///
/// `-z` porque una ruta puede llevar espacios y el formato sin `-z` los escapa con comillas. /// `-z` porque una ruta puede llevar espacios y el formato sin `-z` los escapa con comillas.
fn gitlinks_de(mirror: &Path, commit: &str) -> hammer_core::Result<Vec<(String, String)>> { fn gitlinks_de(mirror: &Path, commit: &str) -> takana_core::Result<Vec<(String, String)>> {
let salida = salida_git(&["ls-tree", "-r", "-z", commit], mirror)?; let salida = salida_git(&["ls-tree", "-r", "-z", commit], mirror)?;
let mut out = Vec::new(); let mut out = Vec::new();
for reg in salida.split('\0').filter(|r| !r.is_empty()) { for reg in salida.split('\0').filter(|r| !r.is_empty()) {
@@ -269,7 +269,7 @@ fn gitlinks_de(mirror: &Path, commit: &str) -> hammer_core::Result<Vec<(String,
fn urls_de_gitmodules( fn urls_de_gitmodules(
mirror: &Path, mirror: &Path,
commit: &str, commit: &str,
) -> hammer_core::Result<std::collections::BTreeMap<String, String>> { ) -> takana_core::Result<std::collections::BTreeMap<String, String>> {
let blob = format!("{commit}:.gitmodules"); let blob = format!("{commit}:.gitmodules");
let salida = match salida_git( let salida = match salida_git(
&[ &[
@@ -365,7 +365,7 @@ fn sanear_para_ruta(url: &str) -> String {
} }
/// `git <args>` capturando stdout. Error si el comando falla. /// `git <args>` capturando stdout. Error si el comando falla.
fn salida_git(args: &[&str], cwd: &Path) -> hammer_core::Result<String> { fn salida_git(args: &[&str], cwd: &Path) -> takana_core::Result<String> {
let out = Command::new("git") let out = Command::new("git")
.arg("-C") .arg("-C")
.arg(cwd) .arg(cwd)
@@ -373,9 +373,9 @@ fn salida_git(args: &[&str], cwd: &Path) -> hammer_core::Result<String> {
.stdin(Stdio::null()) .stdin(Stdio::null())
.stderr(Stdio::piped()) .stderr(Stdio::piped())
.output() .output()
.map_err(|e| hammer_core::Error::Other(anyhow::anyhow!("spawn git {args:?}: {e}")))?; .map_err(|e| takana_core::Error::Other(anyhow::anyhow!("spawn git {args:?}: {e}")))?;
if !out.status.success() { if !out.status.success() {
return Err(hammer_core::Error::Other(anyhow::anyhow!( return Err(takana_core::Error::Other(anyhow::anyhow!(
"git {args:?} falló (exit {:?})", "git {args:?} falló (exit {:?})",
out.status.code() out.status.code()
))); )));
@@ -544,7 +544,7 @@ fn fetch_tarball(
url: &str, url: &str,
sha256: &str, sha256: &str,
work_root: &Path, work_root: &Path,
) -> hammer_core::Result<PathBuf> { ) -> takana_core::Result<PathBuf> {
let tarballs_dir = work_root.join("tarballs"); let tarballs_dir = work_root.join("tarballs");
let sources_dir = work_root.join("sources"); let sources_dir = work_root.join("sources");
std::fs::create_dir_all(&tarballs_dir)?; std::fs::create_dir_all(&tarballs_dir)?;
@@ -595,9 +595,9 @@ fn fetch_tarball(
.stdout(Stdio::inherit()) .stdout(Stdio::inherit())
.stderr(Stdio::inherit()) .stderr(Stdio::inherit())
.status() .status()
.map_err(|e| hammer_core::Error::Other(anyhow::anyhow!("spawn tar -x tarball: {e}")))?; .map_err(|e| takana_core::Error::Other(anyhow::anyhow!("spawn tar -x tarball: {e}")))?;
if !status.success() { if !status.success() {
return Err(hammer_core::Error::Other(anyhow::anyhow!( return Err(takana_core::Error::Other(anyhow::anyhow!(
"tar -x falló (exit {:?})", "tar -x falló (exit {:?})",
status.code() status.code()
))); )));
@@ -628,7 +628,7 @@ fn fetch_tarball(
/// `{base}/{sha256}.tar` en cada uno, en el orden que dicta [`bases_de_mirror`]. /// `{base}/{sha256}.tar` en cada uno, en el orden que dicta [`bases_de_mirror`].
/// `HAMMER_MIRROR_KEY`, si está, se pasa como `--key` (Storage Box por sftp). Sin `HAMMER_MIRROR` /// `HAMMER_MIRROR_KEY`, si está, se pasa como `--key` (Storage Box por sftp). Sin `HAMMER_MIRROR`
/// el comportamiento es el de siempre: derecho a upstream. /// el comportamiento es el de siempre: derecho a upstream.
fn fetch_con_mirror(url: &str, sha256: &str, dst: &Path) -> hammer_core::Result<()> { fn fetch_con_mirror(url: &str, sha256: &str, dst: &Path) -> takana_core::Result<()> {
for base in bases_de_mirror("HAMMER_MIRROR", sha256) { for base in bases_de_mirror("HAMMER_MIRROR", sha256) {
let espejo = format!("{base}/{sha256}.tar"); let espejo = format!("{base}/{sha256}.tar");
// Un fallo acá NO es un error: es el caso normal la primera vez que se ve una fuente, y // Un fallo acá NO es un error: es el caso normal la primera vez que se ve una fuente, y
@@ -645,7 +645,7 @@ fn fetch_con_mirror(url: &str, sha256: &str, dst: &Path) -> hammer_core::Result<
/// para que la próxima corrida lo vuelva a intentar (no envenenamos la caché). /// para que la próxima corrida lo vuelva a intentar (no envenenamos la caché).
/// Baja `url` a `dst` con curl. Sin verificar nada: quien llama decide cómo se comprueba — /// Baja `url` a `dst` con curl. Sin verificar nada: quien llama decide cómo se comprueba —
/// `download_verify` por sha256 (tarballs), `hidratar_desde_bundle` delegando en git (bundles). /// `download_verify` por sha256 (tarballs), `hidratar_desde_bundle` delegando en git (bundles).
fn descargar(url: &str, dst: &Path) -> hammer_core::Result<()> { fn descargar(url: &str, dst: &Path) -> takana_core::Result<()> {
let mut cmd = Command::new("curl"); let mut cmd = Command::new("curl");
cmd.args(["-fL", "--retry", "3", "--connect-timeout", "30"]); cmd.args(["-fL", "--retry", "3", "--connect-timeout", "30"]);
// Clave para el mirror por sftp (Storage Box). Inocua para http/file. // Clave para el mirror por sftp (Storage Box). Inocua para http/file.
@@ -661,9 +661,9 @@ fn descargar(url: &str, dst: &Path) -> hammer_core::Result<()> {
.stdout(Stdio::inherit()) .stdout(Stdio::inherit())
.stderr(Stdio::inherit()) .stderr(Stdio::inherit())
.status() .status()
.map_err(|e| hammer_core::Error::Other(anyhow::anyhow!("spawn curl: {e}")))?; .map_err(|e| takana_core::Error::Other(anyhow::anyhow!("spawn curl: {e}")))?;
if !status.success() { if !status.success() {
return Err(hammer_core::Error::Other(anyhow::anyhow!( return Err(takana_core::Error::Other(anyhow::anyhow!(
"curl {url} falló (exit {:?})", "curl {url} falló (exit {:?})",
status.code() status.code()
))); )));
@@ -671,7 +671,7 @@ fn descargar(url: &str, dst: &Path) -> hammer_core::Result<()> {
Ok(()) Ok(())
} }
fn download_verify(url: &str, sha256: &str, dst: &Path) -> hammer_core::Result<()> { fn download_verify(url: &str, sha256: &str, dst: &Path) -> takana_core::Result<()> {
let tmp = dst.with_extension("tar.partial"); let tmp = dst.with_extension("tar.partial");
if let Err(e) = descargar(url, &tmp) { if let Err(e) = descargar(url, &tmp) {
let _ = std::fs::remove_file(&tmp); let _ = std::fs::remove_file(&tmp);
@@ -685,14 +685,14 @@ fn download_verify(url: &str, sha256: &str, dst: &Path) -> hammer_core::Result<(
Ok(()) Ok(())
} }
fn verify_sha256(path: &Path, expected: &str) -> hammer_core::Result<()> { fn verify_sha256(path: &Path, expected: &str) -> takana_core::Result<()> {
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
let mut f = std::fs::File::open(path)?; let mut f = std::fs::File::open(path)?;
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
std::io::copy(&mut f, &mut hasher)?; std::io::copy(&mut f, &mut hasher)?;
let got = hex::encode(hasher.finalize()); let got = hex::encode(hasher.finalize());
if !got.eq_ignore_ascii_case(expected) { if !got.eq_ignore_ascii_case(expected) {
return Err(hammer_core::Error::Other(anyhow::anyhow!( return Err(takana_core::Error::Other(anyhow::anyhow!(
"sha256 mismatch en {}: esperado {expected}, obtenido {got}", "sha256 mismatch en {}: esperado {expected}, obtenido {got}",
path.display() path.display()
))); )));
@@ -714,7 +714,7 @@ fn verify_sha256(path: &Path, expected: &str) -> hammer_core::Result<()> {
/// (plan C.2 #5). Ver `docs/runbooks/stage1-vm-boot.md` §8. /// (plan C.2 #5). Ver `docs/runbooks/stage1-vm-boot.md` §8.
/// ///
/// Un crate sin deps de crates.io produce un config vacío y ningún `vendor/`: inocuo. /// Un crate sin deps de crates.io produce un config vacío y ningún `vendor/`: inocuo.
pub fn vendor_cargo_deps(src: &Path, vendor_dir: &str) -> hammer_core::Result<()> { pub fn vendor_cargo_deps(src: &Path, vendor_dir: &str) -> takana_core::Result<()> {
let locked = src.join("Cargo.lock").is_file(); let locked = src.join("Cargo.lock").is_file();
let mut args: Vec<&str> = vec!["vendor"]; let mut args: Vec<&str> = vec!["vendor"];
if locked { if locked {
@@ -735,9 +735,9 @@ pub fn vendor_cargo_deps(src: &Path, vendor_dir: &str) -> hammer_core::Result<()
.args(&args) .args(&args)
.stderr(Stdio::inherit()) .stderr(Stdio::inherit())
.output() .output()
.map_err(|e| hammer_core::Error::Other(anyhow::anyhow!("spawn cargo vendor: {e}")))?; .map_err(|e| takana_core::Error::Other(anyhow::anyhow!("spawn cargo vendor: {e}")))?;
if !output.status.success() { if !output.status.success() {
return Err(hammer_core::Error::Other(anyhow::anyhow!( return Err(takana_core::Error::Other(anyhow::anyhow!(
"cargo vendor falló (exit {:?})", "cargo vendor falló (exit {:?})",
output.status.code() output.status.code()
))); )));
@@ -762,7 +762,7 @@ pub fn vendor_cargo_deps(src: &Path, vendor_dir: &str) -> hammer_core::Result<()
/// `GOFLAGS=-mod=mod` permite poblar el `vendor/` aunque el árbol no lo traiga. /// `GOFLAGS=-mod=mod` permite poblar el `vendor/` aunque el árbol no lo traiga.
/// ///
/// Un proyecto sin deps externas produce un `vendor/` vacío (o ninguno): inocuo. /// Un proyecto sin deps externas produce un `vendor/` vacío (o ninguno): inocuo.
pub fn vendor_go_deps(src: &Path) -> hammer_core::Result<()> { pub fn vendor_go_deps(src: &Path) -> takana_core::Result<()> {
// Proyecto WORKSPACE (multimódulo): si el árbol trae `go.work`, `go mod vendor` con GOWORK=off // Proyecto WORKSPACE (multimódulo): si el árbol trae `go.work`, `go mod vendor` con GOWORK=off
// vendorea SÓLO el módulo raíz → el build (que ve todo el workspace) se queja de "inconsistent // vendorea SÓLO el módulo raíz → el build (que ve todo el workspace) se queja de "inconsistent
// vendoring". `go work vendor` (go 1.22+) produce un `vendor/` consistente para TODO el workspace. // vendoring". `go work vendor` (go 1.22+) produce un `vendor/` consistente para TODO el workspace.
@@ -787,13 +787,13 @@ pub fn vendor_go_deps(src: &Path) -> hammer_core::Result<()> {
.stderr(Stdio::inherit()) .stderr(Stdio::inherit())
.output() .output()
.map_err(|e| { .map_err(|e| {
hammer_core::Error::Other(anyhow::anyhow!( takana_core::Error::Other(anyhow::anyhow!(
"spawn {label}: {e} (¿está `go` en el PATH del host? lo provee el lab, \ "spawn {label}: {e} (¿está `go` en el PATH del host? lo provee el lab, \
cf. scripts/bootstrap-devfs.sh / vps-setup.sh)" cf. scripts/bootstrap-devfs.sh / vps-setup.sh)"
)) ))
})?; })?;
if !output.status.success() { if !output.status.success() {
return Err(hammer_core::Error::Other(anyhow::anyhow!( return Err(takana_core::Error::Other(anyhow::anyhow!(
"{label} falló (exit {:?})", "{label} falló (exit {:?})",
output.status.code() output.status.code()
))); )));
@@ -802,11 +802,11 @@ pub fn vendor_go_deps(src: &Path) -> hammer_core::Result<()> {
} }
/// Aplica los patches de la receta (relativos a `recipe.base_dir`) sobre `tree`. /// Aplica los patches de la receta (relativos a `recipe.base_dir`) sobre `tree`.
pub fn apply_patches(recipe: &Recipe, tree: &Path) -> hammer_core::Result<()> { pub fn apply_patches(recipe: &Recipe, tree: &Path) -> takana_core::Result<()> {
for p in &recipe.source.patches { for p in &recipe.source.patches {
let patch_path = recipe.base_dir.join(p); let patch_path = recipe.base_dir.join(p);
let bytes = std::fs::read(&patch_path).map_err(|e| { let bytes = std::fs::read(&patch_path).map_err(|e| {
hammer_core::Error::Recipe(format!( takana_core::Error::Recipe(format!(
"no pude leer patch {}: {e}", "no pude leer patch {}: {e}",
patch_path.display() patch_path.display()
)) ))
@@ -818,18 +818,18 @@ pub fn apply_patches(recipe: &Recipe, tree: &Path) -> hammer_core::Result<()> {
.stdout(Stdio::inherit()) .stdout(Stdio::inherit())
.stderr(Stdio::inherit()) .stderr(Stdio::inherit())
.spawn() .spawn()
.map_err(|e| hammer_core::Error::Other(anyhow::anyhow!("spawn patch: {e}")))?; .map_err(|e| takana_core::Error::Other(anyhow::anyhow!("spawn patch: {e}")))?;
if let Some(mut stdin) = child.stdin.take() { if let Some(mut stdin) = child.stdin.take() {
use std::io::Write; use std::io::Write;
stdin stdin
.write_all(&bytes) .write_all(&bytes)
.map_err(|e| hammer_core::Error::Other(anyhow::anyhow!("write patch: {e}")))?; .map_err(|e| takana_core::Error::Other(anyhow::anyhow!("write patch: {e}")))?;
} }
let status = child let status = child
.wait() .wait()
.map_err(|e| hammer_core::Error::Other(anyhow::anyhow!("wait patch: {e}")))?; .map_err(|e| takana_core::Error::Other(anyhow::anyhow!("wait patch: {e}")))?;
if !status.success() { if !status.success() {
return Err(hammer_core::Error::Other(anyhow::anyhow!( return Err(takana_core::Error::Other(anyhow::anyhow!(
"patch {} falló (exit {:?})", "patch {} falló (exit {:?})",
patch_path.display(), patch_path.display(),
status.code() status.code()
@@ -1047,7 +1047,7 @@ mod tests {
} }
} }
fn run_git(args: &[&str], cwd: Option<&Path>) -> hammer_core::Result<()> { fn run_git(args: &[&str], cwd: Option<&Path>) -> takana_core::Result<()> {
let mut cmd = Command::new("git"); let mut cmd = Command::new("git");
if let Some(c) = cwd { if let Some(c) = cwd {
cmd.arg("-C").arg(c); cmd.arg("-C").arg(c);
@@ -1058,9 +1058,9 @@ fn run_git(args: &[&str], cwd: Option<&Path>) -> hammer_core::Result<()> {
.stdout(Stdio::inherit()) .stdout(Stdio::inherit())
.stderr(Stdio::inherit()) .stderr(Stdio::inherit())
.status() .status()
.map_err(|e| hammer_core::Error::Other(anyhow::anyhow!("spawn git {args:?}: {e}")))?; .map_err(|e| takana_core::Error::Other(anyhow::anyhow!("spawn git {args:?}: {e}")))?;
if !status.success() { if !status.success() {
return Err(hammer_core::Error::Other(anyhow::anyhow!( return Err(takana_core::Error::Other(anyhow::anyhow!(
"git {args:?} falló (exit {:?})", "git {args:?} falló (exit {:?})",
status.code() status.code()
))); )));
@@ -70,19 +70,19 @@ impl Harkaq {
deps: &[PathBuf], deps: &[PathBuf],
contratos: &[String], contratos: &[String],
dst: &Path, dst: &Path,
) -> hammer_core::Result<()> { ) -> takana_core::Result<()> {
let mut ro: Vec<String> = Vec::new(); let mut ro: Vec<String> = Vec::new();
let mut dirs: std::collections::BTreeSet<String> = Default::default(); let mut dirs: std::collections::BTreeSet<String> = Default::default();
for dep in deps { for dep in deps {
let real = std::fs::canonicalize(dep).map_err(|e| { let real = std::fs::canonicalize(dep).map_err(|e| {
hammer_core::Error::Other(anyhow::anyhow!("canonicalize {}: {e}", dep.display())) takana_core::Error::Other(anyhow::anyhow!("canonicalize {}: {e}", dep.display()))
})?; })?;
recorrer(&real, &real, &mut ro, &mut dirs)?; recorrer(&real, &real, &mut ro, &mut dirs)?;
} }
let mut f = std::fs::File::create(dst).map_err(|e| { let mut f = std::fs::File::create(dst).map_err(|e| {
hammer_core::Error::Other(anyhow::anyhow!("crear política {}: {e}", dst.display())) takana_core::Error::Other(anyhow::anyhow!("crear política {}: {e}", dst.display()))
})?; })?;
writeln!(f, "# harkaq: clausura derivada de {} dep(s)", deps.len()).ok(); writeln!(f, "# harkaq: clausura derivada de {} dep(s)", deps.len()).ok();
@@ -98,7 +98,7 @@ impl Harkaq {
if let Some(base) = &self.base { if let Some(base) = &self.base {
let txt = std::fs::read_to_string(base).map_err(|e| { let txt = std::fs::read_to_string(base).map_err(|e| {
hammer_core::Error::Other(anyhow::anyhow!("leer HARKAQ_BASE {}: {e}", base.display())) takana_core::Error::Other(anyhow::anyhow!("leer HARKAQ_BASE {}: {e}", base.display()))
})?; })?;
writeln!(f, "# runtime base ({})", base.display()).ok(); writeln!(f, "# runtime base ({})", base.display()).ok();
f.write_all(txt.as_bytes()).ok(); f.write_all(txt.as_bytes()).ok();
@@ -130,7 +130,7 @@ impl Harkaq {
deps: &[PathBuf], deps: &[PathBuf],
contratos: &[String], contratos: &[String],
id: &str, id: &str,
) -> hammer_core::Result<Corrida> { ) -> takana_core::Result<Corrida> {
let tmp = std::env::temp_dir().join(format!("harkaq-{id}-{}", std::process::id())); let tmp = std::env::temp_dir().join(format!("harkaq-{id}-{}", std::process::id()));
std::fs::create_dir_all(&tmp).ok(); std::fs::create_dir_all(&tmp).ok();
let policy = tmp.join("policy"); let policy = tmp.join("policy");
@@ -146,7 +146,7 @@ impl Harkaq {
let verdict = tmp.join("verdict.json"); let verdict = tmp.join("verdict.json");
let vf = std::fs::File::create(&verdict).map_err(|e| { let vf = std::fs::File::create(&verdict).map_err(|e| {
hammer_core::Error::Other(anyhow::anyhow!("crear verdict: {e}")) takana_core::Error::Other(anyhow::anyhow!("crear verdict: {e}"))
})?; })?;
let reader = Command::new(self.bin.join("harkaq-audit")) let reader = Command::new(self.bin.join("harkaq-audit"))
.arg("--canary") .arg("--canary")
@@ -157,7 +157,7 @@ impl Harkaq {
.stderr(Stdio::inherit()) .stderr(Stdio::inherit())
.spawn() .spawn()
.map_err(|e| { .map_err(|e| {
hammer_core::Error::Other(anyhow::anyhow!( takana_core::Error::Other(anyhow::anyhow!(
"spawn harkaq-audit ({}): {e} — ¿compilado y con `setcap cap_audit_read`?", "spawn harkaq-audit ({}): {e} — ¿compilado y con `setcap cap_audit_read`?",
self.bin.display() self.bin.display()
)) ))
@@ -199,7 +199,7 @@ fn recorrer(
dir: &Path, dir: &Path,
ro: &mut Vec<String>, ro: &mut Vec<String>,
dirs: &mut std::collections::BTreeSet<String>, dirs: &mut std::collections::BTreeSet<String>,
) -> hammer_core::Result<()> { ) -> takana_core::Result<()> {
let rd = match std::fs::read_dir(dir) { let rd = match std::fs::read_dir(dir) {
Ok(r) => r, Ok(r) => r,
Err(_) => return Ok(()), Err(_) => return Ok(()),
@@ -15,7 +15,7 @@ use std::os::unix::fs::FileTypeExt;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
use hammer_core::LinkMode; use takana_core::LinkMode;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct HydratedFile { pub struct HydratedFile {
@@ -61,7 +61,7 @@ pub fn hydrate(
target_fhs: &Path, target_fhs: &Path,
mode: LinkMode, mode: LinkMode,
dynamic: Option<&DynamicSpec>, dynamic: Option<&DynamicSpec>,
) -> hammer_core::Result<HydrateReport> { ) -> takana_core::Result<HydrateReport> {
// `is_dir()` a secas acepta un directorio VACÍO: hydrate proyectaría 0 ficheros y devolvería // `is_dir()` a secas acepta un directorio VACÍO: hydrate proyectaría 0 ficheros y devolvería
// un HydrateReport de éxito. Es el mismo agujero que ya se tapó en `Store::has` (2026-08-10) y // un HydrateReport de éxito. Es el mismo agujero que ya se tapó en `Store::has` (2026-08-10) y
// en `Store::seal` (2026-08-29): un vacío no es un artefacto, es un nombre. Acá duele especial // en `Store::seal` (2026-08-29): un vacío no es un artefacto, es un nombre. Acá duele especial
@@ -71,7 +71,7 @@ pub fn hydrate(
.map(|mut e| e.next().is_none()) .map(|mut e| e.next().is_none())
.unwrap_or(true); .unwrap_or(true);
if !artifact_dir.is_dir() || vacio { if !artifact_dir.is_dir() || vacio {
return Err(hammer_core::Error::Store(format!( return Err(takana_core::Error::Store(format!(
"artefacto inexistente o VACÍO: {}", "artefacto inexistente o VACÍO: {}",
artifact_dir.display() artifact_dir.display()
))); )));
@@ -93,7 +93,7 @@ pub fn hydrate(
// La raíz REAL contra la que se mide la contención (ver `no_escapa`). Se resuelve una sola vez: // La raíz REAL contra la que se mide la contención (ver `no_escapa`). Se resuelve una sola vez:
// si el propio `target_fhs` es un symlink, eso es elección de quien hidrata y vale como raíz. // si el propio `target_fhs` es un symlink, eso es elección de quien hidrata y vale como raíz.
let raiz = target_fhs.canonicalize().map_err(|e| { let raiz = target_fhs.canonicalize().map_err(|e| {
hammer_core::Error::Store(format!("canonicalize {}: {e}", target_fhs.display())) takana_core::Error::Store(format!("canonicalize {}: {e}", target_fhs.display()))
})?; })?;
let mut report = HydrateReport::default(); let mut report = HydrateReport::default();
@@ -121,12 +121,12 @@ pub fn hydrate(
/// entre las dos. Para el fallo real —un symlink persistente puesto por otro artefacto— alcanza y /// entre las dos. Para el fallo real —un symlink persistente puesto por otro artefacto— alcanza y
/// es determinista; la versión sin carrera es mover las escrituras a `openat2` + `linkat`/`renameat` /// es determinista; la versión sin carrera es mover las escrituras a `openat2` + `linkat`/`renameat`
/// (SDD 25 §7-H3), y eso arrastra el camino de `patchelf`. /// (SDD 25 §7-H3), y eso arrastra el camino de `patchelf`.
fn no_escapa(raiz: &Path, dst: &Path) -> hammer_core::Result<()> { fn no_escapa(raiz: &Path, dst: &Path) -> takana_core::Result<()> {
let real = dst.canonicalize().map_err(|e| { let real = dst.canonicalize().map_err(|e| {
hammer_core::Error::Store(format!("canonicalize {}: {e}", dst.display())) takana_core::Error::Store(format!("canonicalize {}: {e}", dst.display()))
})?; })?;
if !real.starts_with(raiz) { if !real.starts_with(raiz) {
return Err(hammer_core::Error::Store(format!( return Err(takana_core::Error::Store(format!(
"la hidratación se saldría del root: {} lleva a {}, fuera de {}.\n\ "la hidratación se saldría del root: {} lleva a {}, fuera de {}.\n\
Es un symlink de directorio ya presente en el FHS (probablemente puesto por otro \ Es un symlink de directorio ya presente en el FHS (probablemente puesto por otro \
artefacto). No se escribe a través de él: borralo o hidratá sobre un root limpio.", artefacto). No se escribe a través de él: borralo o hidratá sobre un root limpio.",
@@ -145,7 +145,7 @@ fn walk_and_link(
raiz: &Path, raiz: &Path,
spec: Option<&DynamicSpec>, spec: Option<&DynamicSpec>,
report: &mut HydrateReport, report: &mut HydrateReport,
) -> hammer_core::Result<()> { ) -> takana_core::Result<()> {
for entry in std::fs::read_dir(cur)? { for entry in std::fs::read_dir(cur)? {
let entry = entry?; let entry = entry?;
let src = entry.path(); let src = entry.path();
@@ -183,7 +183,7 @@ fn walk_and_link(
report.files.push(HydratedFile { src, dst }); report.files.push(HydratedFile { src, dst });
} else if ft.is_block_device() || ft.is_char_device() || ft.is_fifo() || ft.is_socket() { } else if ft.is_block_device() || ft.is_char_device() || ft.is_fifo() || ft.is_socket() {
// Los artefactos del lab no deberían contener nodos especiales; lo señalamos. // Los artefactos del lab no deberían contener nodos especiales; lo señalamos.
return Err(hammer_core::Error::Other(anyhow::anyhow!( return Err(takana_core::Error::Other(anyhow::anyhow!(
"tipo no soportado en {}: {ft:?}", "tipo no soportado en {}: {ft:?}",
src.display() src.display()
))); )));
@@ -195,7 +195,7 @@ fn walk_and_link(
/// Crea un hardlink `dst → src` reemplazando atómicamente cualquier archivo previo en `dst`. /// Crea un hardlink `dst → src` reemplazando atómicamente cualquier archivo previo en `dst`.
/// Implementación: `link(src, dst.tmp)` + `rename(dst.tmp, dst)`. La técnica garantiza que /// Implementación: `link(src, dst.tmp)` + `rename(dst.tmp, dst)`. La técnica garantiza que
/// el archivo en `dst` nunca queda en estado intermedio y que el inode del store no se toca. /// el archivo en `dst` nunca queda en estado intermedio y que el inode del store no se toca.
fn atomic_hardlink(src: &Path, dst: &Path) -> hammer_core::Result<()> { fn atomic_hardlink(src: &Path, dst: &Path) -> takana_core::Result<()> {
if let Some(parent) = dst.parent() { if let Some(parent) = dst.parent() {
std::fs::create_dir_all(parent)?; std::fs::create_dir_all(parent)?;
} }
@@ -203,7 +203,7 @@ fn atomic_hardlink(src: &Path, dst: &Path) -> hammer_core::Result<()> {
// Limpia un tmp olvidado de una corrida anterior. // Limpia un tmp olvidado de una corrida anterior.
let _ = std::fs::remove_file(&tmp); let _ = std::fs::remove_file(&tmp);
std::fs::hard_link(src, &tmp).map_err(|e| { std::fs::hard_link(src, &tmp).map_err(|e| {
hammer_core::Error::Store(format!( takana_core::Error::Store(format!(
"hardlink {} → {}: {e}", "hardlink {} → {}: {e}",
src.display(), src.display(),
tmp.display() tmp.display()
@@ -212,7 +212,7 @@ fn atomic_hardlink(src: &Path, dst: &Path) -> hammer_core::Result<()> {
// rename sobreescribe atómicamente si dst es un archivo (no si es directorio). // rename sobreescribe atómicamente si dst es un archivo (no si es directorio).
if let Err(e) = std::fs::rename(&tmp, dst) { if let Err(e) = std::fs::rename(&tmp, dst) {
let _ = std::fs::remove_file(&tmp); let _ = std::fs::remove_file(&tmp);
return Err(hammer_core::Error::Store(format!( return Err(takana_core::Error::Store(format!(
"rename {} → {}: {e}", "rename {} → {}: {e}",
tmp.display(), tmp.display(),
dst.display() dst.display()
@@ -223,7 +223,7 @@ fn atomic_hardlink(src: &Path, dst: &Path) -> hammer_core::Result<()> {
/// `true` si el archivo empieza por el magic ELF (`\x7fELF`). Errores de I/O se propagan; /// `true` si el archivo empieza por el magic ELF (`\x7fELF`). Errores de I/O se propagan;
/// un archivo más corto que 4 bytes simplemente no es ELF. /// un archivo más corto que 4 bytes simplemente no es ELF.
fn is_elf(path: &Path) -> hammer_core::Result<bool> { fn is_elf(path: &Path) -> takana_core::Result<bool> {
use std::io::Read; use std::io::Read;
let mut f = std::fs::File::open(path)?; let mut f = std::fs::File::open(path)?;
let mut magic = [0u8; 4]; let mut magic = [0u8; 4];
@@ -235,14 +235,14 @@ fn is_elf(path: &Path) -> hammer_core::Result<bool> {
} }
/// Verifica que `patchelf` esté disponible antes de empezar a proyectar. /// Verifica que `patchelf` esté disponible antes de empezar a proyectar.
fn ensure_patchelf() -> hammer_core::Result<()> { fn ensure_patchelf() -> takana_core::Result<()> {
match Command::new("patchelf").arg("--version").output() { match Command::new("patchelf").arg("--version").output() {
Ok(o) if o.status.success() => Ok(()), Ok(o) if o.status.success() => Ok(()),
Ok(o) => Err(hammer_core::Error::Other(anyhow::anyhow!( Ok(o) => Err(takana_core::Error::Other(anyhow::anyhow!(
"patchelf falló al invocarse: {}", "patchelf falló al invocarse: {}",
String::from_utf8_lossy(&o.stderr).trim() String::from_utf8_lossy(&o.stderr).trim()
))), ))),
Err(e) => Err(hammer_core::Error::Other(anyhow::anyhow!( Err(e) => Err(takana_core::Error::Other(anyhow::anyhow!(
"hidratación dinámica requiere `patchelf` en el PATH y no se pudo ejecutar: {e}" "hidratación dinámica requiere `patchelf` en el PATH y no se pudo ejecutar: {e}"
))), ))),
} }
@@ -251,14 +251,14 @@ fn ensure_patchelf() -> hammer_core::Result<()> {
/// Copia `src → dst` atómicamente (copy a tmp → patchelf el tmp → rename), preservando los /// Copia `src → dst` atómicamente (copy a tmp → patchelf el tmp → rename), preservando los
/// permisos (`std::fs::copy` los replica, incluido el bit de ejecución). Patcheamos el tmp /// permisos (`std::fs::copy` los replica, incluido el bit de ejecución). Patcheamos el tmp
/// **antes** del rename para que `dst` nunca quede en un estado a medio parchear. /// **antes** del rename para que `dst` nunca quede en un estado a medio parchear.
fn atomic_copy_and_patch(src: &Path, dst: &Path, spec: &DynamicSpec) -> hammer_core::Result<()> { fn atomic_copy_and_patch(src: &Path, dst: &Path, spec: &DynamicSpec) -> takana_core::Result<()> {
if let Some(parent) = dst.parent() { if let Some(parent) = dst.parent() {
std::fs::create_dir_all(parent)?; std::fs::create_dir_all(parent)?;
} }
let tmp = with_suffix(dst, ".hammer-tmp"); let tmp = with_suffix(dst, ".hammer-tmp");
let _ = std::fs::remove_file(&tmp); let _ = std::fs::remove_file(&tmp);
std::fs::copy(src, &tmp).map_err(|e| { std::fs::copy(src, &tmp).map_err(|e| {
hammer_core::Error::Store(format!("copy {}{}: {e}", src.display(), tmp.display())) takana_core::Error::Store(format!("copy {}{}: {e}", src.display(), tmp.display()))
})?; })?;
if let Err(e) = run_patchelf(&tmp, spec) { if let Err(e) = run_patchelf(&tmp, spec) {
let _ = std::fs::remove_file(&tmp); let _ = std::fs::remove_file(&tmp);
@@ -266,7 +266,7 @@ fn atomic_copy_and_patch(src: &Path, dst: &Path, spec: &DynamicSpec) -> hammer_c
} }
if let Err(e) = std::fs::rename(&tmp, dst) { if let Err(e) = std::fs::rename(&tmp, dst) {
let _ = std::fs::remove_file(&tmp); let _ = std::fs::remove_file(&tmp);
return Err(hammer_core::Error::Store(format!( return Err(takana_core::Error::Store(format!(
"rename {} → {}: {e}", "rename {} → {}: {e}",
tmp.display(), tmp.display(),
dst.display() dst.display()
@@ -277,7 +277,7 @@ fn atomic_copy_and_patch(src: &Path, dst: &Path, spec: &DynamicSpec) -> hammer_c
/// Ejecuta `patchelf [--set-interpreter I] [--set-rpath R] <file>`. Asume que el caller ya /// Ejecuta `patchelf [--set-interpreter I] [--set-rpath R] <file>`. Asume que el caller ya
/// comprobó (`has_work`) que hay al menos una de las dos. /// comprobó (`has_work`) que hay al menos una de las dos.
fn run_patchelf(file: &Path, spec: &DynamicSpec) -> hammer_core::Result<()> { fn run_patchelf(file: &Path, spec: &DynamicSpec) -> takana_core::Result<()> {
let mut cmd = Command::new("patchelf"); let mut cmd = Command::new("patchelf");
if let Some(interp) = &spec.interpreter { if let Some(interp) = &spec.interpreter {
cmd.arg("--set-interpreter").arg(interp); cmd.arg("--set-interpreter").arg(interp);
@@ -287,13 +287,13 @@ fn run_patchelf(file: &Path, spec: &DynamicSpec) -> hammer_core::Result<()> {
} }
cmd.arg(file); cmd.arg(file);
let out = cmd.output().map_err(|e| { let out = cmd.output().map_err(|e| {
hammer_core::Error::Other(anyhow::anyhow!( takana_core::Error::Other(anyhow::anyhow!(
"no pude ejecutar patchelf sobre {}: {e}", "no pude ejecutar patchelf sobre {}: {e}",
file.display() file.display()
)) ))
})?; })?;
if !out.status.success() { if !out.status.success() {
return Err(hammer_core::Error::Other(anyhow::anyhow!( return Err(takana_core::Error::Other(anyhow::anyhow!(
"patchelf falló sobre {}: {}", "patchelf falló sobre {}: {}",
file.display(), file.display(),
String::from_utf8_lossy(&out.stderr).trim() String::from_utf8_lossy(&out.stderr).trim()
@@ -302,7 +302,7 @@ fn run_patchelf(file: &Path, spec: &DynamicSpec) -> hammer_core::Result<()> {
Ok(()) Ok(())
} }
fn atomic_replace_symlink(link_target: &Path, dst: &Path) -> hammer_core::Result<()> { fn atomic_replace_symlink(link_target: &Path, dst: &Path) -> takana_core::Result<()> {
use std::os::unix::fs::symlink; use std::os::unix::fs::symlink;
if let Some(parent) = dst.parent() { if let Some(parent) = dst.parent() {
std::fs::create_dir_all(parent)?; std::fs::create_dir_all(parent)?;
@@ -310,11 +310,11 @@ fn atomic_replace_symlink(link_target: &Path, dst: &Path) -> hammer_core::Result
let tmp = with_suffix(dst, ".hammer-tmp"); let tmp = with_suffix(dst, ".hammer-tmp");
let _ = std::fs::remove_file(&tmp); let _ = std::fs::remove_file(&tmp);
symlink(link_target, &tmp).map_err(|e| { symlink(link_target, &tmp).map_err(|e| {
hammer_core::Error::Store(format!("symlink {}{}: {e}", link_target.display(), tmp.display())) takana_core::Error::Store(format!("symlink {}{}: {e}", link_target.display(), tmp.display()))
})?; })?;
if let Err(e) = std::fs::rename(&tmp, dst) { if let Err(e) = std::fs::rename(&tmp, dst) {
let _ = std::fs::remove_file(&tmp); let _ = std::fs::remove_file(&tmp);
return Err(hammer_core::Error::Store(format!( return Err(takana_core::Error::Store(format!(
"rename {} → {}: {e}", "rename {} → {}: {e}",
tmp.display(), tmp.display(),
dst.display() dst.display()
@@ -4,7 +4,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use hammer_core::{ArtifactHash, Compiler, LinkMode, Phases, Recipe, Store}; use takana_core::{ArtifactHash, Compiler, LinkMode, Phases, Recipe, Store};
pub mod config; pub mod config;
pub mod download; pub mod download;
@@ -33,8 +33,8 @@ pub use swm_bridge::build_source_patch;
pub fn artifact_hash( pub fn artifact_hash(
recipe: &Recipe, recipe: &Recipe,
_store: &Store, _store: &Store,
lab: &hammer_core::lab::LabFingerprint, lab: &takana_core::lab::LabFingerprint,
) -> hammer_core::Result<ArtifactHash> { ) -> takana_core::Result<ArtifactHash> {
let mut stack: Vec<String> = Vec::new(); let mut stack: Vec<String> = Vec::new();
// Memo por path canónico de receta: el grafo de build es un DAG-diamante (p.ej. TODO KF6/Qt // Memo por path canónico de receta: el grafo de build es un DAG-diamante (p.ej. TODO KF6/Qt
// converge en qtbase/kcoreaddons, alcanzables por decenas de rutas). Sin memoizar, el walk // converge en qtbase/kcoreaddons, alcanzables por decenas de rutas). Sin memoizar, el walk
@@ -50,11 +50,11 @@ fn artifact_hash_rec(
recipe: &Recipe, recipe: &Recipe,
stack: &mut Vec<String>, stack: &mut Vec<String>,
memo: &mut HashMap<PathBuf, ArtifactHash>, memo: &mut HashMap<PathBuf, ArtifactHash>,
lab: &hammer_core::lab::LabFingerprint, lab: &takana_core::lab::LabFingerprint,
) -> hammer_core::Result<ArtifactHash> { ) -> takana_core::Result<ArtifactHash> {
if stack.iter().any(|n| n == &recipe.name) { if stack.iter().any(|n| n == &recipe.name) {
stack.push(recipe.name.clone()); stack.push(recipe.name.clone());
return Err(hammer_core::Error::Recipe(format!( return Err(takana_core::Error::Recipe(format!(
"ciclo de dependencias de build: {}", "ciclo de dependencias de build: {}",
stack.join(" -> ") stack.join(" -> ")
))); )));
@@ -92,13 +92,13 @@ fn resolve_build_dep_hashes(
recipe: &Recipe, recipe: &Recipe,
stack: &mut Vec<String>, stack: &mut Vec<String>,
memo: &mut HashMap<PathBuf, ArtifactHash>, memo: &mut HashMap<PathBuf, ArtifactHash>,
lab: &hammer_core::lab::LabFingerprint, lab: &takana_core::lab::LabFingerprint,
) -> hammer_core::Result<Vec<ArtifactHash>> { ) -> takana_core::Result<Vec<ArtifactHash>> {
if recipe.deps.build.is_empty() { if recipe.deps.build.is_empty() {
return Ok(Vec::new()); return Ok(Vec::new());
} }
if recipe.base_dir.as_os_str().is_empty() { if recipe.base_dir.as_os_str().is_empty() {
return Err(hammer_core::Error::Recipe(format!( return Err(takana_core::Error::Recipe(format!(
"la receta '{}' declara deps de build {:?} pero no tiene base_dir; \ "la receta '{}' declara deps de build {:?} pero no tiene base_dir; \
cárgala con Recipe::load_from_path para resolver el catálogo", cárgala con Recipe::load_from_path para resolver el catálogo",
recipe.name, recipe.deps.build recipe.name, recipe.deps.build
@@ -116,7 +116,7 @@ fn resolve_build_dep_hashes(
continue; continue;
} }
let dep_recipe = Recipe::load_from_path(&dep_path).map_err(|e| { let dep_recipe = Recipe::load_from_path(&dep_path).map_err(|e| {
hammer_core::Error::Recipe(format!( takana_core::Error::Recipe(format!(
"no pude cargar la dep de build '{dep}' de '{}' ({}): {e}", "no pude cargar la dep de build '{dep}' de '{}' ({}): {e}",
recipe.name, recipe.name,
dep_path.display() dep_path.display()
@@ -138,12 +138,12 @@ fn materialize_build_deps(
recipe: &Recipe, recipe: &Recipe,
cfg: &BuildConfig, cfg: &BuildConfig,
store: &Store, store: &Store,
) -> hammer_core::Result<Vec<PathBuf>> { ) -> takana_core::Result<Vec<PathBuf>> {
if recipe.deps.build.is_empty() { if recipe.deps.build.is_empty() {
return Ok(Vec::new()); return Ok(Vec::new());
} }
if recipe.base_dir.as_os_str().is_empty() { if recipe.base_dir.as_os_str().is_empty() {
return Err(hammer_core::Error::Recipe(format!( return Err(takana_core::Error::Recipe(format!(
"la receta '{}' declara deps de build {:?} pero no tiene base_dir; \ "la receta '{}' declara deps de build {:?} pero no tiene base_dir; \
cárgala con Recipe::load_from_path para resolver el catálogo", cárgala con Recipe::load_from_path para resolver el catálogo",
recipe.name, recipe.deps.build recipe.name, recipe.deps.build
@@ -153,7 +153,7 @@ fn materialize_build_deps(
for dep in &recipe.deps.build { for dep in &recipe.deps.build {
let dep_path = resolve_dep_path(&recipe.base_dir, dep); let dep_path = resolve_dep_path(&recipe.base_dir, dep);
let dep_recipe = Recipe::load_from_path(&dep_path).map_err(|e| { let dep_recipe = Recipe::load_from_path(&dep_path).map_err(|e| {
hammer_core::Error::Recipe(format!( takana_core::Error::Recipe(format!(
"no pude cargar la dep de build '{dep}' de '{}' ({}): {e}", "no pude cargar la dep de build '{dep}' de '{}' ({}): {e}",
recipe.name, recipe.name,
dep_path.display() dep_path.display()
@@ -169,14 +169,14 @@ fn materialize_build_deps(
/// la receta fije `build.zig_version`, en cuyo caso se usa el zig de esa versión, ubicado como /// la receta fije `build.zig_version`, en cuyo caso se usa el zig de esa versión, ubicado como
/// **hermano** del por defecto (`<…>/zig-x86_64-linux-<version>`, el layout de `.dev-fs/tools/`). /// **hermano** del por defecto (`<…>/zig-x86_64-linux-<version>`, el layout de `.dev-fs/tools/`).
/// Escotilla por-receta para esquivar la regresión de zig 0.14 sin tocar el toolchain global. /// Escotilla por-receta para esquivar la regresión de zig 0.14 sin tocar el toolchain global.
fn effective_zig_dir(cfg: &BuildConfig, recipe: &Recipe) -> hammer_core::Result<PathBuf> { fn effective_zig_dir(cfg: &BuildConfig, recipe: &Recipe) -> takana_core::Result<PathBuf> {
let Some(version) = &recipe.build.zig_version else { let Some(version) = &recipe.build.zig_version else {
return Ok(cfg.zig_dir.clone()); return Ok(cfg.zig_dir.clone());
}; };
let parent = cfg.zig_dir.parent().unwrap_or_else(|| Path::new(".")); let parent = cfg.zig_dir.parent().unwrap_or_else(|| Path::new("."));
let dir = parent.join(format!("zig-x86_64-linux-{version}")); let dir = parent.join(format!("zig-x86_64-linux-{version}"));
if !dir.join("zig").is_file() { if !dir.join("zig").is_file() {
return Err(hammer_core::Error::Recipe(format!( return Err(takana_core::Error::Recipe(format!(
"zig_version=\"{version}\": no encuentro el ejecutable zig en {} \ "zig_version=\"{version}\": no encuentro el ejecutable zig en {} \
(bajá ese zig a {}/)", (bajá ese zig a {}/)",
dir.display(), dir.display(),
@@ -192,13 +192,13 @@ pub fn build(
recipe: &Recipe, recipe: &Recipe,
cfg: &BuildConfig, cfg: &BuildConfig,
store: &Store, store: &Store,
) -> hammer_core::Result<ArtifactHash> { ) -> takana_core::Result<ArtifactHash> {
// La huella sale del rootfs de ESTE cfg, que es el lab que de verdad va a compilar — no de // La huella sale del rootfs de ESTE cfg, que es el lab que de verdad va a compilar — no de
// `docs/state/lab-toolchain.lock` ni de una ruta derivada del store. Derivarla del padre del // `docs/state/lab-toolchain.lock` ni de una ruta derivada del store. Derivarla del padre del
// store ya mordió una vez (ver `BuildConfig::defaults_for_store_with_lab`): al anclar el store // store ya mordió una vez (ver `BuildConfig::defaults_for_store_with_lab`): al anclar el store
// de un worker a un volumen, el lab se buscaba donde no estaba. `cfg.rootfs` respeta // de un worker a un volumen, el lab se buscaba donde no estaba. `cfg.rootfs` respeta
// `HAMMER_LAB`/`HAMMER_ROOTFS` y es la única fuente que no puede desincronizarse del build. // `HAMMER_LAB`/`HAMMER_ROOTFS` y es la única fuente que no puede desincronizarse del build.
let lab = hammer_core::lab::LabFingerprint::from_rootfs(&cfg.rootfs)?; let lab = takana_core::lab::LabFingerprint::from_rootfs(&cfg.rootfs)?;
let h = artifact_hash(recipe, store, &lab)?; let h = artifact_hash(recipe, store, &lab)?;
if store.has(&h, &recipe.name) { if store.has(&h, &recipe.name) {
tracing::info!(hash = %h, name = %recipe.name, "caché: artefacto ya en el store"); tracing::info!(hash = %h, name = %recipe.name, "caché: artefacto ya en el store");
@@ -342,7 +342,7 @@ pub fn build(
tracing::info!(cmd, "phase: compile"); tracing::info!(cmd, "phase: compile");
sb.run(cmd)?; sb.run(cmd)?;
} else { } else {
return Err(hammer_core::Error::Recipe( return Err(takana_core::Error::Recipe(
"receta sin fase 'compile' y heurística no encontró ninguna".into(), "receta sin fase 'compile' y heurística no encontró ninguna".into(),
)); ));
} }
@@ -350,7 +350,7 @@ pub fn build(
tracing::info!(cmd, "phase: install"); tracing::info!(cmd, "phase: install");
sb.run(cmd)?; sb.run(cmd)?;
} else { } else {
return Err(hammer_core::Error::Recipe( return Err(takana_core::Error::Recipe(
"receta sin fase 'install' y heurística no encontró ninguna".into(), "receta sin fase 'install' y heurística no encontró ninguna".into(),
)); ));
} }
@@ -404,7 +404,7 @@ pub fn build(
// seal, para que quede congelada en el árbol read-only del store. `hammer export` la // seal, para que quede congelada en el árbol read-only del store. `hammer export` la
// lee de vuelta para emitir `source_patch` con la receta original en vez de un // lee de vuelta para emitir `source_patch` con la receta original en vez de un
// `file_drop` opaco. Ver `docs/04-overlay.md` §provenance (Fase 4). // `file_drop` opaco. Ver `docs/04-overlay.md` §provenance (Fase 4).
let sidecar_path = out_dir.join(hammer_core::store::RECIPE_SIDECAR_REL); let sidecar_path = out_dir.join(takana_core::store::RECIPE_SIDECAR_REL);
if let Some(parent) = sidecar_path.parent() { if let Some(parent) = sidecar_path.parent() {
std::fs::create_dir_all(parent)?; std::fs::create_dir_all(parent)?;
} }
@@ -419,9 +419,9 @@ pub fn build(
/// El resultado de correr un check de evidencia, con su veredicto (H1b). /// El resultado de correr un check de evidencia, con su veredicto (H1b).
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct EvidenceOutcome { pub struct EvidenceOutcome {
pub kind: hammer_core::EvidenceKind, pub kind: takana_core::EvidenceKind,
pub cmd: String, pub cmd: String,
pub outcome: hammer_core::CheckOutcome, pub outcome: takana_core::CheckOutcome,
} }
/// Reporte de la ejecución de TODA la evidencia de una receta (H1b). `all_passed` es el gate: /// Reporte de la ejecución de TODA la evidencia de una receta (H1b). `all_passed` es el gate:
@@ -437,7 +437,7 @@ impl EvidenceReport {
} }
/// El estrato de confianza MÁS ALTO que pasó (para reportar "verificado hasta <nivel>"). `None` /// El estrato de confianza MÁS ALTO que pasó (para reportar "verificado hasta <nivel>"). `None`
/// si no hay checks. No promete que ese nivel *baste* — sólo que lo declarado a ese nivel pasó. /// si no hay checks. No promete que ese nivel *baste* — sólo que lo declarado a ese nivel pasó.
pub fn max_level_passed(&self) -> Option<hammer_core::EvidenceKind> { pub fn max_level_passed(&self) -> Option<takana_core::EvidenceKind> {
self.results self.results
.iter() .iter()
.filter(|r| r.outcome.passed) .filter(|r| r.outcome.passed)
@@ -460,7 +460,7 @@ pub fn run_evidence(
recipe: &Recipe, recipe: &Recipe,
cfg: &BuildConfig, cfg: &BuildConfig,
store: &Store, store: &Store,
) -> hammer_core::Result<EvidenceReport> { ) -> takana_core::Result<EvidenceReport> {
recipe.evidence.validate()?; recipe.evidence.validate()?;
if recipe.evidence.checks.is_empty() { if recipe.evidence.checks.is_empty() {
return Ok(EvidenceReport { results: Vec::new() }); return Ok(EvidenceReport { results: Vec::new() });
@@ -615,7 +615,7 @@ fn inject_cargo_package_selector(recipe: &mut Recipe, src: &Path) {
/// NO la considere parte del workspace del repo hammer (que la contiene en `work/sources/`). Append /// NO la considere parte del workspace del repo hammer (que la contiene en `work/sources/`). Append
/// idempotente: si ya hay `[workspace]` (crate suelto que es su propio workspace, o una receta que /// idempotente: si ya hay `[workspace]` (crate suelto que es su propio workspace, o una receta que
/// lo parchea), no toca nada. Caso virtual-manifest (root ya es `[workspace]`) tampoco se toca. /// lo parchea), no toca nada. Caso virtual-manifest (root ya es `[workspace]`) tampoco se toca.
fn ensure_cargo_workspace_isolation(src: &Path) -> hammer_core::Result<()> { fn ensure_cargo_workspace_isolation(src: &Path) -> takana_core::Result<()> {
let manifest = src.join("Cargo.toml"); let manifest = src.join("Cargo.toml");
let text = std::fs::read_to_string(&manifest)?; let text = std::fs::read_to_string(&manifest)?;
// Heurística simple: ¿hay una tabla `[workspace]` (al inicio de línea)? Cubre `[workspace]` y // Heurística simple: ¿hay una tabla `[workspace]` (al inicio de línea)? Cubre `[workspace]` y
@@ -804,7 +804,7 @@ fn cargo_linker_env_var(triple: &str) -> String {
/// Devuelve los comandos a ejecutar para cada fase: overrides explícitos en la receta /// Devuelve los comandos a ejecutar para cada fase: overrides explícitos en la receta
/// ganan; el resto lo deriva la heurística según el sistema de build detectado. /// ganan; el resto lo deriva la heurística según el sistema de build detectado.
fn resolve_phases(recipe: &Recipe, src: &Path) -> hammer_core::Result<Phases> { fn resolve_phases(recipe: &Recipe, src: &Path) -> takana_core::Result<Phases> {
let mut out = recipe.build.phases.clone(); let mut out = recipe.build.phases.clone();
// Raíz efectiva del módulo Go: `<src>/<subdir>` si la receta lo fija (dolt→go, csvtk→subdir), // Raíz efectiva del módulo Go: `<src>/<subdir>` si la receta lo fija (dolt→go, csvtk→subdir),
// si no `src`. detect/detect_go_main operan ahí; el compile hace `cd <subdir>` primero. // si no `src`. detect/detect_go_main operan ahí; el compile hace `cd <subdir>` primero.
@@ -1303,7 +1303,7 @@ commit = "deadbeef"
let d = tempfile::tempdir().unwrap(); let d = tempfile::tempdir().unwrap();
touch(d.path(), "Cargo.toml"); touch(d.path(), "Cargo.toml");
let mut r = recipe(&[]); let mut r = recipe(&[]);
r.build.link = hammer_core::LinkMode::Dynamic; r.build.link = takana_core::LinkMode::Dynamic;
let c = resolve_phases(&r, d.path()).unwrap().compile.unwrap(); let c = resolve_phases(&r, d.path()).unwrap().compile.unwrap();
assert!(c.contains("target-feature=-crt-static"), "{c}"); assert!(c.contains("target-feature=-crt-static"), "{c}");
} }
@@ -1359,7 +1359,7 @@ fn unique_out_dir(
work_root: &Path, work_root: &Path,
name: &str, name: &str,
h: &ArtifactHash, h: &ArtifactHash,
) -> hammer_core::Result<PathBuf> { ) -> takana_core::Result<PathBuf> {
let dir = work_root let dir = work_root
.join("out") .join("out")
.join(h.store_dir_name(name)); .join(h.store_dir_name(name));
@@ -1378,7 +1378,7 @@ pub fn hydrate(
target_fhs: &Path, target_fhs: &Path,
mode: LinkMode, mode: LinkMode,
dynamic: Option<&hydrate::DynamicSpec>, dynamic: Option<&hydrate::DynamicSpec>,
) -> hammer_core::Result<HydrateReport> { ) -> takana_core::Result<HydrateReport> {
let artifact_dir = store.find_by_hash(h.as_str())?; let artifact_dir = store.find_by_hash(h.as_str())?;
hydrate::hydrate(&artifact_dir, target_fhs, mode, dynamic) hydrate::hydrate(&artifact_dir, target_fhs, mode, dynamic)
} }
@@ -1417,8 +1417,8 @@ mod dep_hash_tests {
write_recipe(d.path(), "app", "a0", &["base"]); write_recipe(d.path(), "app", "a0", &["base"]);
write_recipe(d.path(), "app_nodep", "a0", &[]); // misma fuente, sin la dep write_recipe(d.path(), "app_nodep", "a0", &[]); // misma fuente, sin la dep
let with_dep = artifact_hash(&load(d.path(), "app"), &store, &hammer_core::lab::LabFingerprint::for_tests()).unwrap(); let with_dep = artifact_hash(&load(d.path(), "app"), &store, &takana_core::lab::LabFingerprint::for_tests()).unwrap();
let no_dep = artifact_hash(&load(d.path(), "app_nodep"), &store, &hammer_core::lab::LabFingerprint::for_tests()).unwrap(); let no_dep = artifact_hash(&load(d.path(), "app_nodep"), &store, &takana_core::lab::LabFingerprint::for_tests()).unwrap();
// `app_nodep` se llama distinto, pero el nombre no entra al hash: la única diferencia // `app_nodep` se llama distinto, pero el nombre no entra al hash: la única diferencia
// de entrada es la presencia de la dep. (Renombramos sólo para tener dos .toml.) // de entrada es la presencia de la dep. (Renombramos sólo para tener dos .toml.)
assert_ne!(with_dep, no_dep, "la dep de build debe entrar al hash"); assert_ne!(with_dep, no_dep, "la dep de build debe entrar al hash");
@@ -1431,11 +1431,11 @@ mod dep_hash_tests {
write_recipe(d.path(), "base", "b0", &[]); write_recipe(d.path(), "base", "b0", &[]);
write_recipe(d.path(), "lib", "l0", &["base"]); write_recipe(d.path(), "lib", "l0", &["base"]);
write_recipe(d.path(), "app", "a0", &["lib"]); write_recipe(d.path(), "app", "a0", &["lib"]);
let before = artifact_hash(&load(d.path(), "app"), &store, &hammer_core::lab::LabFingerprint::for_tests()).unwrap(); let before = artifact_hash(&load(d.path(), "app"), &store, &takana_core::lab::LabFingerprint::for_tests()).unwrap();
// Cambiar la fuente de la dep transitiva `base` debe re-hashear `app`. // Cambiar la fuente de la dep transitiva `base` debe re-hashear `app`.
write_recipe(d.path(), "base", "b1", &[]); write_recipe(d.path(), "base", "b1", &[]);
let after = artifact_hash(&load(d.path(), "app"), &store, &hammer_core::lab::LabFingerprint::for_tests()).unwrap(); let after = artifact_hash(&load(d.path(), "app"), &store, &takana_core::lab::LabFingerprint::for_tests()).unwrap();
assert_ne!(before, after, "un cambio transitivo debe propagarse"); assert_ne!(before, after, "un cambio transitivo debe propagarse");
} }
@@ -1444,7 +1444,7 @@ mod dep_hash_tests {
let d = tempfile::tempdir().unwrap(); let d = tempfile::tempdir().unwrap();
let store = store_in(d.path()); let store = store_in(d.path());
write_recipe(d.path(), "app", "a0", &["pcre2"]); // sin pcre2.toml write_recipe(d.path(), "app", "a0", &["pcre2"]); // sin pcre2.toml
let err = artifact_hash(&load(d.path(), "app"), &store, &hammer_core::lab::LabFingerprint::for_tests()).unwrap_err().to_string(); let err = artifact_hash(&load(d.path(), "app"), &store, &takana_core::lab::LabFingerprint::for_tests()).unwrap_err().to_string();
assert!(err.contains("pcre2"), "el error debe nombrar la dep: {err}"); assert!(err.contains("pcre2"), "el error debe nombrar la dep: {err}");
assert!(err.contains("app"), "y la receta que la pide: {err}"); assert!(err.contains("app"), "y la receta que la pide: {err}");
} }
@@ -1455,7 +1455,7 @@ mod dep_hash_tests {
let store = store_in(d.path()); let store = store_in(d.path());
write_recipe(d.path(), "a", "a0", &["b"]); write_recipe(d.path(), "a", "a0", &["b"]);
write_recipe(d.path(), "b", "b0", &["a"]); write_recipe(d.path(), "b", "b0", &["a"]);
let err = artifact_hash(&load(d.path(), "a"), &store, &hammer_core::lab::LabFingerprint::for_tests()).unwrap_err().to_string(); let err = artifact_hash(&load(d.path(), "a"), &store, &takana_core::lab::LabFingerprint::for_tests()).unwrap_err().to_string();
assert!(err.contains("ciclo"), "esperaba ciclo: {err}"); assert!(err.contains("ciclo"), "esperaba ciclo: {err}");
assert!(err.contains("a -> b -> a"), "debe mostrar la cadena: {err}"); assert!(err.contains("a -> b -> a"), "debe mostrar la cadena: {err}");
} }
@@ -1468,7 +1468,7 @@ mod dep_hash_tests {
"name=\"x\"\nversion=\"1\"\n[source]\nrepo=\"git://x\"\ncommit=\"c0\"\n[build]\n[deps]\nbuild=[\"y\"]\n", "name=\"x\"\nversion=\"1\"\n[source]\nrepo=\"git://x\"\ncommit=\"c0\"\n[build]\n[deps]\nbuild=[\"y\"]\n",
) )
.unwrap(); .unwrap();
let err = artifact_hash(&r, &store, &hammer_core::lab::LabFingerprint::for_tests()).unwrap_err().to_string(); let err = artifact_hash(&r, &store, &takana_core::lab::LabFingerprint::for_tests()).unwrap_err().to_string();
assert!(err.contains("base_dir"), "debe explicar la falta de base_dir: {err}"); assert!(err.contains("base_dir"), "debe explicar la falta de base_dir: {err}");
} }
@@ -1477,8 +1477,8 @@ mod dep_hash_tests {
let d = tempfile::tempdir().unwrap(); let d = tempfile::tempdir().unwrap();
let store = store_in(d.path()); let store = store_in(d.path());
write_recipe(d.path(), "solo", "s0", &[]); write_recipe(d.path(), "solo", "s0", &[]);
let a = artifact_hash(&load(d.path(), "solo"), &store, &hammer_core::lab::LabFingerprint::for_tests()).unwrap(); let a = artifact_hash(&load(d.path(), "solo"), &store, &takana_core::lab::LabFingerprint::for_tests()).unwrap();
let b = artifact_hash(&load(d.path(), "solo"), &store, &hammer_core::lab::LabFingerprint::for_tests()).unwrap(); let b = artifact_hash(&load(d.path(), "solo"), &store, &takana_core::lab::LabFingerprint::for_tests()).unwrap();
assert_eq!(a, b, "sin deps el hash es estable"); assert_eq!(a, b, "sin deps el hash es estable");
} }
} }
@@ -18,7 +18,7 @@ use std::sync::{Arc, Mutex};
const TAIL_LINES: usize = 50; const TAIL_LINES: usize = 50;
/// Fallo de una fase de build, con la cola del log del lab si la pudimos capturar. Viaja /// Fallo de una fase de build, con la cola del log del lab si la pudimos capturar. Viaja
/// dentro de `hammer_core::Error::Other(anyhow)`; el bus la recupera por `downcast_ref` para /// dentro de `takana_core::Error::Other(anyhow)`; el bus la recupera por `downcast_ref` para
/// poblar `Event::BuildFailed.log_tail`. Ver `docs/07-agent-bus.md`. /// poblar `Event::BuildFailed.log_tail`. Ver `docs/07-agent-bus.md`.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct BuildFailure { pub struct BuildFailure {
@@ -41,9 +41,9 @@ impl std::error::Error for BuildFailure {}
impl BuildFailure { impl BuildFailure {
/// Recupera un `BuildFailure` de un error de hammer-core, si lo lleva dentro. El bus lo /// Recupera un `BuildFailure` de un error de hammer-core, si lo lleva dentro. El bus lo
/// usa para separar `reason` de `log_tail`. /// usa para separar `reason` de `log_tail`.
pub fn from_error(e: &hammer_core::Error) -> Option<&BuildFailure> { pub fn from_error(e: &takana_core::Error) -> Option<&BuildFailure> {
match e { match e {
hammer_core::Error::Other(a) => a.downcast_ref::<BuildFailure>(), takana_core::Error::Other(a) => a.downcast_ref::<BuildFailure>(),
_ => None, _ => None,
} }
} }
@@ -87,8 +87,8 @@ impl Sandbox {
/// Ejecuta `cmd` dentro del sandbox bajo `sh -c`. Hace *tee* de stdout/stderr al padre /// Ejecuta `cmd` dentro del sandbox bajo `sh -c`. Hace *tee* de stdout/stderr al padre
/// (para no perder el streaming en vivo) y a la vez retiene las últimas [`TAIL_LINES`] /// (para no perder el streaming en vivo) y a la vez retiene las últimas [`TAIL_LINES`]
/// líneas; si el exit code es != 0, devuelve un [`BuildFailure`] con esa cola en /// líneas; si el exit code es != 0, devuelve un [`BuildFailure`] con esa cola en
/// `log_tail`, envuelto en `hammer_core::Error::Other`. /// `log_tail`, envuelto en `takana_core::Error::Other`.
pub fn run(&self, cmd: &str) -> hammer_core::Result<()> { pub fn run(&self, cmd: &str) -> takana_core::Result<()> {
// harkaq (SDD 16): sin HARKAQ=1 esto es `None` y todo lo de abajo es el camino de // harkaq (SDD 16): sin HARKAQ=1 esto es `None` y todo lo de abajo es el camino de
// siempre — mismos args de bwrap, mismo entorno, ningún proceso extra. Inerte por // siempre — mismos args de bwrap, mismo entorno, ningún proceso extra. Inerte por
// requisito: hay 700+ artefactos sellados cuyo hash no puede moverse por encender un // requisito: hay 700+ artefactos sellados cuyo hash no puede moverse por encender un
@@ -109,7 +109,7 @@ impl Sandbox {
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.stderr(Stdio::piped()) .stderr(Stdio::piped())
.spawn() .spawn()
.map_err(|e| hammer_core::Error::Other(anyhow::anyhow!("spawn bwrap: {e}")))?; .map_err(|e| takana_core::Error::Other(anyhow::anyhow!("spawn bwrap: {e}")))?;
// Ring buffer compartido por ambos pumps: la cola combinada de stdout+stderr. // Ring buffer compartido por ambos pumps: la cola combinada de stdout+stderr.
let tail = Arc::new(Mutex::new(VecDeque::<String>::with_capacity(TAIL_LINES))); let tail = Arc::new(Mutex::new(VecDeque::<String>::with_capacity(TAIL_LINES)));
@@ -120,7 +120,7 @@ impl Sandbox {
let status = child let status = child
.wait() .wait()
.map_err(|e| hammer_core::Error::Other(anyhow::anyhow!("wait bwrap: {e}")))?; .map_err(|e| takana_core::Error::Other(anyhow::anyhow!("wait bwrap: {e}")))?;
// Aseguramos drenar ambos streams antes de leer la cola. // Aseguramos drenar ambos streams antes de leer la cola.
let _ = h_out.join(); let _ = h_out.join();
let _ = h_err.join(); let _ = h_err.join();
@@ -148,7 +148,7 @@ impl Sandbox {
"build phase falló (exit {}): {cmd}", "build phase falló (exit {}): {cmd}",
status.code().map(|c| c.to_string()).unwrap_or_else(|| "?".into()) status.code().map(|c| c.to_string()).unwrap_or_else(|| "?".into())
); );
return Err(hammer_core::Error::Other(anyhow::Error::new(BuildFailure { return Err(takana_core::Error::Other(anyhow::Error::new(BuildFailure {
reason, reason,
log_tail: tail_str, log_tail: tail_str,
}))); })));
@@ -160,7 +160,7 @@ impl Sandbox {
/// exit code en vez de fallar en != 0 — el caller decide contra lo esperado. El stderr se teea al /// exit code en vez de fallar en != 0 — el caller decide contra lo esperado. El stderr se teea al
/// padre en vivo (logging), como en `run`. Es el motor de ejecución del checker de evidencia (H1b): /// padre en vivo (logging), como en `run`. Es el motor de ejecución del checker de evidencia (H1b):
/// correr un `cmd` en el sandbox reproducible y comparar (exit, blake3(stdout)) contra lo declarado. /// correr un `cmd` en el sandbox reproducible y comparar (exit, blake3(stdout)) contra lo declarado.
pub fn run_capture(&self, cmd: &str) -> hammer_core::Result<CmdOutcome> { pub fn run_capture(&self, cmd: &str) -> takana_core::Result<CmdOutcome> {
let args = self.bwrap_args(cmd); let args = self.bwrap_args(cmd);
tracing::debug!(?args, "bwrap (capture)"); tracing::debug!(?args, "bwrap (capture)");
let mut child = Command::new("bwrap") let mut child = Command::new("bwrap")
@@ -169,7 +169,7 @@ impl Sandbox {
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.stderr(Stdio::piped()) .stderr(Stdio::piped())
.spawn() .spawn()
.map_err(|e| hammer_core::Error::Other(anyhow::anyhow!("spawn bwrap: {e}")))?; .map_err(|e| takana_core::Error::Other(anyhow::anyhow!("spawn bwrap: {e}")))?;
let mut out = child.stdout.take().expect("stdout piped"); let mut out = child.stdout.take().expect("stdout piped");
let err = child.stderr.take().expect("stderr piped"); let err = child.stderr.take().expect("stderr piped");
@@ -184,7 +184,7 @@ impl Sandbox {
let status = child let status = child
.wait() .wait()
.map_err(|e| hammer_core::Error::Other(anyhow::anyhow!("wait bwrap: {e}")))?; .map_err(|e| takana_core::Error::Other(anyhow::anyhow!("wait bwrap: {e}")))?;
let stdout = h_out.join().unwrap_or_default(); let stdout = h_out.join().unwrap_or_default();
let _ = h_err.join(); let _ = h_err.join();
@@ -526,17 +526,17 @@ fn spawn_pump<R: std::io::Read + Send + 'static>(
} }
/// Errores tempranos: comprueba que las rutas base existen antes de invocar el build. /// Errores tempranos: comprueba que las rutas base existen antes de invocar el build.
pub fn ensure_layout(rootfs: &Path, zig_dir: &Path) -> hammer_core::Result<()> { pub fn ensure_layout(rootfs: &Path, zig_dir: &Path) -> takana_core::Result<()> {
// bin/sh en Alpine es un symlink absoluto a /bin/busybox, que desde el host no resuelve. // bin/sh en Alpine es un symlink absoluto a /bin/busybox, que desde el host no resuelve.
// Comprobamos el archivo regular real para no engañarnos. // Comprobamos el archivo regular real para no engañarnos.
if !rootfs.join("bin/busybox").is_file() { if !rootfs.join("bin/busybox").is_file() {
return Err(hammer_core::Error::Other(anyhow::anyhow!( return Err(takana_core::Error::Other(anyhow::anyhow!(
"rootfs inválido: no encuentro {}/bin/busybox — ¿corriste el bootstrap de Alpine?", "rootfs inválido: no encuentro {}/bin/busybox — ¿corriste el bootstrap de Alpine?",
rootfs.display() rootfs.display()
))); )));
} }
if !zig_dir.join("zig").exists() { if !zig_dir.join("zig").exists() {
return Err(hammer_core::Error::Other(anyhow::anyhow!( return Err(takana_core::Error::Other(anyhow::anyhow!(
"zig no encontrado en {}/zig — ¿instalaste zig en .dev-fs/tools/zig?", "zig no encontrado en {}/zig — ¿instalaste zig en .dev-fs/tools/zig?",
zig_dir.display() zig_dir.display()
))); )));
@@ -597,11 +597,11 @@ pub const RANLIB_WRAPPER: &str = "ranlib";
/// (ejecutable estático / `.a`) el wrapper es TRANSPARENTE ⇒ byte-idéntico a `zig cc`, y el resto del /// (ejecutable estático / `.a`) el wrapper es TRANSPARENTE ⇒ byte-idéntico a `zig cc`, y el resto del
/// árbol sigue estático. El ArtifactHash se deriva de inputs (receta+deps), no de los bytes ⇒ este /// árbol sigue estático. El ArtifactHash se deriva de inputs (receta+deps), no de los bytes ⇒ este
/// cambio NO re-hashea nada sellado; sólo destraba builds nuevos con enlace dinámico irreducible. /// cambio NO re-hashea nada sellado; sólo destraba builds nuevos con enlace dinámico irreducible.
fn write_cc_wrappers(rootfs: &Path) -> hammer_core::Result<()> { fn write_cc_wrappers(rootfs: &Path) -> takana_core::Result<()> {
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
let bindir = rootfs.join("usr/local/bin"); let bindir = rootfs.join("usr/local/bin");
std::fs::create_dir_all(&bindir).map_err(|e| { std::fs::create_dir_all(&bindir).map_err(|e| {
hammer_core::Error::Other(anyhow::anyhow!("no pude crear {}: {e}", bindir.display())) takana_core::Error::Other(anyhow::anyhow!("no pude crear {}: {e}", bindir.display()))
})?; })?;
// `case " $* "` detecta como palabra los marcadores de "este link es dinámico": -shared, // `case " $* "` detecta como palabra los marcadores de "este link es dinámico": -shared,
// -rdynamic, --export-dynamic, o un shared object en la línea (`…​.so ` final o `…​.so.N` versionado). // -rdynamic, --export-dynamic, o un shared object en la línea (`…​.so ` final o `…​.so.N` versionado).
@@ -636,10 +636,10 @@ fn write_cc_wrappers(rootfs: &Path) -> hammer_core::Result<()> {
for (name, driver) in [(CC_WRAPPER, "zig cc"), (CXX_WRAPPER, "zig c++")] { for (name, driver) in [(CC_WRAPPER, "zig cc"), (CXX_WRAPPER, "zig c++")] {
let path = bindir.join(name); let path = bindir.join(name);
std::fs::write(&path, body(driver)).map_err(|e| { std::fs::write(&path, body(driver)).map_err(|e| {
hammer_core::Error::Other(anyhow::anyhow!("no pude escribir {}: {e}", path.display())) takana_core::Error::Other(anyhow::anyhow!("no pude escribir {}: {e}", path.display()))
})?; })?;
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).map_err(|e| { std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).map_err(|e| {
hammer_core::Error::Other(anyhow::anyhow!("no pude chmod {}: {e}", path.display())) takana_core::Error::Other(anyhow::anyhow!("no pude chmod {}: {e}", path.display()))
})?; })?;
} }
@@ -658,10 +658,10 @@ fn write_cc_wrappers(rootfs: &Path) -> hammer_core::Result<()> {
"#!/bin/sh\n # Interpuesto por hammer: fuerza archivos deterministas (sin hora de pared en la\n # cabecera `ar`). Sin esto dos builds idénticos sellan bytes distintos. Ver\n # sandbox.rs::AR_WRAPPER.\n [ -x /usr/bin/{tool} ] || exit 127\n exec /usr/bin/{tool} -D \"$@\"\n" "#!/bin/sh\n # Interpuesto por hammer: fuerza archivos deterministas (sin hora de pared en la\n # cabecera `ar`). Sin esto dos builds idénticos sellan bytes distintos. Ver\n # sandbox.rs::AR_WRAPPER.\n [ -x /usr/bin/{tool} ] || exit 127\n exec /usr/bin/{tool} -D \"$@\"\n"
); );
std::fs::write(&path, body).map_err(|e| { std::fs::write(&path, body).map_err(|e| {
hammer_core::Error::Other(anyhow::anyhow!("no pude escribir {}: {e}", path.display())) takana_core::Error::Other(anyhow::anyhow!("no pude escribir {}: {e}", path.display()))
})?; })?;
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).map_err(|e| { std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).map_err(|e| {
hammer_core::Error::Other(anyhow::anyhow!("no pude chmod {}: {e}", path.display())) takana_core::Error::Other(anyhow::anyhow!("no pude chmod {}: {e}", path.display()))
})?; })?;
} }
Ok(()) Ok(())
@@ -688,7 +688,7 @@ mod tests {
reason: "exit 2".into(), reason: "exit 2".into(),
log_tail: Some("undefined reference to `foo'".into()), log_tail: Some("undefined reference to `foo'".into()),
}; };
let err = hammer_core::Error::Other(anyhow::Error::new(bf)); let err = takana_core::Error::Other(anyhow::Error::new(bf));
let recovered = BuildFailure::from_error(&err).expect("debe recuperarse"); let recovered = BuildFailure::from_error(&err).expect("debe recuperarse");
assert_eq!(recovered.reason, "exit 2"); assert_eq!(recovered.reason, "exit 2");
assert_eq!( assert_eq!(
@@ -699,7 +699,7 @@ mod tests {
#[test] #[test]
fn from_error_none_for_plain_errors() { fn from_error_none_for_plain_errors() {
let err = hammer_core::Error::Recipe("nope".into()); let err = takana_core::Error::Recipe("nope".into());
assert!(BuildFailure::from_error(&err).is_none()); assert!(BuildFailure::from_error(&err).is_none());
} }
@@ -1,4 +1,4 @@
//! Puente entre `hammer_core::Mutation::SourcePatch` y el laboratorio. //! Puente entre `takana_core::Mutation::SourcePatch` y el laboratorio.
//! //!
//! Convierte la mutación serializada en una `Recipe` efímera + un archivo de patch //! Convierte la mutación serializada en una `Recipe` efímera + un archivo de patch
//! temporal, y delega en `build` para sellar el artefacto en el store. Luego se hidrata. //! temporal, y delega en `build` para sellar el artefacto en el store. Luego se hidrata.
@@ -9,7 +9,7 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use hammer_core::{ use takana_core::{
swm::{Mutation, SwmBuild}, swm::{Mutation, SwmBuild},
ArtifactHash, Compiler, LinkMode, Recipe, Store, ArtifactHash, Compiler, LinkMode, Recipe, Store,
}; };
@@ -27,7 +27,7 @@ pub fn build_source_patch(
store: &Store, store: &Store,
scratch_root: Option<&Path>, scratch_root: Option<&Path>,
name: Option<&str>, name: Option<&str>,
) -> hammer_core::Result<ArtifactHash> { ) -> takana_core::Result<ArtifactHash> {
let catalog_dir = catalog_dir_for(cfg, scratch_root); let catalog_dir = catalog_dir_for(cfg, scratch_root);
// El NOMBRE de la receta sintetizada decide el sufijo del store (`<hash>-<name>`) Y la // El NOMBRE de la receta sintetizada decide el sufijo del store (`<hash>-<name>`) Y la
// clave de cache-hit (`store.has(hash, name)`). Si quien llama conoce el nombre del paquete // clave de cache-hit (`store.has(hash, name)`). Si quien llama conoce el nombre del paquete
@@ -47,7 +47,7 @@ pub fn build_source_patch(
.strip_prefix("b3:") .strip_prefix("b3:")
.expect("of_inputs siempre devuelve 'b3:'"); .expect("of_inputs siempre devuelve 'b3:'");
if want_hex != got_hex { if want_hex != got_hex {
return Err(hammer_core::Error::Recipe(format!( return Err(takana_core::Error::Recipe(format!(
"expected_hash no coincide: declarado=b3:{want_hex}, obtenido={hash}" "expected_hash no coincide: declarado=b3:{want_hex}, obtenido={hash}"
))); )));
} }
@@ -72,7 +72,7 @@ pub fn recipe_from_source_patch(
mutation: &Mutation, mutation: &Mutation,
name: Option<&str>, name: Option<&str>,
catalog_dir: &Path, catalog_dir: &Path,
) -> hammer_core::Result<Recipe> { ) -> takana_core::Result<Recipe> {
let (repo, commit, tarball, sha256, strip_components, patch, patch_url, build_cfg, target_bin, deps, evidence, slots) = let (repo, commit, tarball, sha256, strip_components, patch, patch_url, build_cfg, target_bin, deps, evidence, slots) =
match mutation { match mutation {
Mutation::SourcePatch { Mutation::SourcePatch {
@@ -83,33 +83,33 @@ pub fn recipe_from_source_patch(
target_bin, deps, evidence, slots, target_bin, deps, evidence, slots,
), ),
_ => { _ => {
return Err(hammer_core::Error::Recipe( return Err(takana_core::Error::Recipe(
"recipe_from_source_patch: la mutación no es 'source_patch'".into(), "recipe_from_source_patch: la mutación no es 'source_patch'".into(),
)); ));
} }
}; };
// git xor tarball — misma regla que el schema (`swm_source_kind`). // git xor tarball — misma regla que el schema (`swm_source_kind`).
let source_kind = hammer_core::swm::swm_source_kind( let source_kind = takana_core::swm::swm_source_kind(
repo.as_deref(), repo.as_deref(),
commit.as_deref(), commit.as_deref(),
tarball.as_deref(), tarball.as_deref(),
sha256.as_deref(), sha256.as_deref(),
) )
.map_err(hammer_core::Error::Recipe)?; .map_err(takana_core::Error::Recipe)?;
std::fs::create_dir_all(catalog_dir)?; std::fs::create_dir_all(catalog_dir)?;
// Clave estable del origen para nombrar el patch a disco: el commit (git) o el sha256 // Clave estable del origen para nombrar el patch a disco: el commit (git) o el sha256
// (tarball). Idéntica reaplicación ⇒ mismo nombre ⇒ hash de receta determinista. // (tarball). Idéntica reaplicación ⇒ mismo nombre ⇒ hash de receta determinista.
let source_key: &str = match &source_kind { let source_key: &str = match &source_kind {
hammer_core::SourceKind::Git { commit, .. } => commit, takana_core::SourceKind::Git { commit, .. } => commit,
hammer_core::SourceKind::Tarball { sha256, .. } => sha256, takana_core::SourceKind::Tarball { sha256, .. } => sha256,
// Inalcanzable por construcción —`swm_source_kind` sólo produce Git o Tarball— pero se // Inalcanzable por construcción —`swm_source_kind` sólo produce Git o Tarball— pero se
// escribe como error y no como `unreachable!`: esto es una librería, y un panic aquí // escribe como error y no como `unreachable!`: esto es una librería, y un panic aquí
// mataría al llamador por una receta mal formada en vez de decírselo. // mataría al llamador por una receta mal formada en vez de decírselo.
hammer_core::SourceKind::Dir { .. } => { takana_core::SourceKind::Dir { .. } => {
return Err(hammer_core::Error::Recipe( return Err(takana_core::Error::Recipe(
"un .swm no puede llevar un source por directorio".into(), "un .swm no puede llevar un source por directorio".into(),
)) ))
} }
@@ -162,31 +162,31 @@ fn derive_name(target_bin: &str) -> String {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn synthesize_recipe( fn synthesize_recipe(
name: &str, name: &str,
source: &hammer_core::SourceKind<'_>, source: &takana_core::SourceKind<'_>,
build_cfg: &SwmBuild, build_cfg: &SwmBuild,
strip_components: Option<usize>, strip_components: Option<usize>,
deps: &hammer_core::Deps, deps: &takana_core::Deps,
evidence: &hammer_core::Evidence, evidence: &takana_core::Evidence,
slots: &hammer_core::Slots, slots: &takana_core::Slots,
patches: Vec<String>, patches: Vec<String>,
base_dir: &Path, base_dir: &Path,
) -> hammer_core::Result<Recipe> { ) -> takana_core::Result<Recipe> {
let compiler = parse_compiler(&build_cfg.compiler)?; let compiler = parse_compiler(&build_cfg.compiler)?;
let link = parse_link(&build_cfg.link)?; let link = parse_link(&build_cfg.link)?;
// Sección `[source]` y sufijo de versión según el modo de origen. El validador de // Sección `[source]` y sufijo de versión según el modo de origen. El validador de
// `Source::kind` ya garantiza git xor tarball; acá sólo emitimos el TOML correspondiente. // `Source::kind` ya garantiza git xor tarball; acá sólo emitimos el TOML correspondiente.
let (version_key, source_block) = match source { let (version_key, source_block) = match source {
hammer_core::SourceKind::Git { repo, commit } => ( takana_core::SourceKind::Git { repo, commit } => (
*commit, *commit,
format!("repo = \"{repo}\"\ncommit = \"{commit}\"\n"), format!("repo = \"{repo}\"\ncommit = \"{commit}\"\n"),
), ),
hammer_core::SourceKind::Tarball { url, sha256 } => ( takana_core::SourceKind::Tarball { url, sha256 } => (
*sha256, *sha256,
format!("tarball = \"{url}\"\nsha256 = \"{sha256}\"\n"), format!("tarball = \"{url}\"\nsha256 = \"{sha256}\"\n"),
), ),
// Ver la nota del `source_key` de arriba: un `.swm` no puede traer un `dir`. // Ver la nota del `source_key` de arriba: un `.swm` no puede traer un `dir`.
hammer_core::SourceKind::Dir { .. } => { takana_core::SourceKind::Dir { .. } => {
return Err(hammer_core::Error::Recipe( return Err(takana_core::Error::Recipe(
"un .swm no puede llevar un source por directorio".into(), "un .swm no puede llevar un source por directorio".into(),
)) ))
} }
@@ -241,22 +241,22 @@ flags = []
Ok(recipe) Ok(recipe)
} }
fn parse_compiler(s: &str) -> hammer_core::Result<Compiler> { fn parse_compiler(s: &str) -> takana_core::Result<Compiler> {
match s { match s {
"zig-cc" => Ok(Compiler::ZigCc), "zig-cc" => Ok(Compiler::ZigCc),
"clang" => Ok(Compiler::Clang), "clang" => Ok(Compiler::Clang),
"gcc" => Ok(Compiler::Gcc), "gcc" => Ok(Compiler::Gcc),
other => Err(hammer_core::Error::Recipe(format!( other => Err(takana_core::Error::Recipe(format!(
"compiler desconocido: '{other}' (esperado zig-cc/clang/gcc)" "compiler desconocido: '{other}' (esperado zig-cc/clang/gcc)"
))), ))),
} }
} }
fn parse_link(s: &str) -> hammer_core::Result<LinkMode> { fn parse_link(s: &str) -> takana_core::Result<LinkMode> {
match s { match s {
"static" => Ok(LinkMode::Static), "static" => Ok(LinkMode::Static),
"dynamic" => Ok(LinkMode::Dynamic), "dynamic" => Ok(LinkMode::Dynamic),
other => Err(hammer_core::Error::Recipe(format!( other => Err(takana_core::Error::Recipe(format!(
"link desconocido: '{other}' (esperado static/dynamic)" "link desconocido: '{other}' (esperado static/dynamic)"
))), ))),
} }
@@ -284,9 +284,9 @@ logo = "b3:azul"
[slots.requires] [slots.requires]
"wayland-protocol" = "b3:stock" "wayland-protocol" = "b3:stock"
"#; "#;
let recipe = hammer_core::Recipe::from_toml(toml).unwrap(); let recipe = takana_core::Recipe::from_toml(toml).unwrap();
let swm = let swm =
hammer_core::Swm::from_recipe(&recipe, "/usr/bin/tema", None, None, "2026-06-06").unwrap(); takana_core::Swm::from_recipe(&recipe, "/usr/bin/tema", None, None, "2026-06-06").unwrap();
// El .swm YAML debe llevar el bloque slots. // El .swm YAML debe llevar el bloque slots.
let yaml = swm.to_yaml().unwrap(); let yaml = swm.to_yaml().unwrap();
assert!(yaml.contains("slots"), "el .swm debe transportar slots: {yaml}"); assert!(yaml.contains("slots"), "el .swm debe transportar slots: {yaml}");
@@ -327,15 +327,15 @@ logo = "b3:azul"
let d = tempfile::tempdir().unwrap(); let d = tempfile::tempdir().unwrap();
let r = synthesize_recipe( let r = synthesize_recipe(
"grep", "grep",
&hammer_core::SourceKind::Git { &takana_core::SourceKind::Git {
repo: "git://example/grep.git", repo: "git://example/grep.git",
commit: "a1b2c3d4e5f6a7b8c9", commit: "a1b2c3d4e5f6a7b8c9",
}, },
&fake_swm_build(), &fake_swm_build(),
None, None,
&hammer_core::Deps::default(), &takana_core::Deps::default(),
&hammer_core::Evidence::default(), &takana_core::Evidence::default(),
&hammer_core::Slots::default(), &takana_core::Slots::default(),
vec![], vec![],
d.path(), d.path(),
) )
@@ -344,7 +344,7 @@ logo = "b3:azul"
assert!(r.version.starts_with("swm-a1b2c3d4")); assert!(r.version.starts_with("swm-a1b2c3d4"));
assert_eq!(r.build.flags, vec!["--enable-foo"]); assert_eq!(r.build.flags, vec!["--enable-foo"]);
match r.source.kind().unwrap() { match r.source.kind().unwrap() {
hammer_core::SourceKind::Git { repo, commit } => { takana_core::SourceKind::Git { repo, commit } => {
assert_eq!(repo, "git://example/grep.git"); assert_eq!(repo, "git://example/grep.git");
assert_eq!(commit, "a1b2c3d4e5f6a7b8c9"); assert_eq!(commit, "a1b2c3d4e5f6a7b8c9");
} }
@@ -357,15 +357,15 @@ logo = "b3:azul"
let d = tempfile::tempdir().unwrap(); let d = tempfile::tempdir().unwrap();
let r = synthesize_recipe( let r = synthesize_recipe(
"grep", "grep",
&hammer_core::SourceKind::Tarball { &takana_core::SourceKind::Tarball {
url: "https://ftp.gnu.org/gnu/grep/grep-3.11.tar.gz", url: "https://ftp.gnu.org/gnu/grep/grep-3.11.tar.gz",
sha256: "abcdef0123456789", sha256: "abcdef0123456789",
}, },
&fake_swm_build(), &fake_swm_build(),
None, None,
&hammer_core::Deps::default(), &takana_core::Deps::default(),
&hammer_core::Evidence::default(), &takana_core::Evidence::default(),
&hammer_core::Slots::default(), &takana_core::Slots::default(),
vec![], vec![],
d.path(), d.path(),
) )
@@ -373,7 +373,7 @@ logo = "b3:azul"
assert_eq!(r.name, "grep"); assert_eq!(r.name, "grep");
assert!(r.version.starts_with("swm-abcdef012345")); assert!(r.version.starts_with("swm-abcdef012345"));
match r.source.kind().unwrap() { match r.source.kind().unwrap() {
hammer_core::SourceKind::Tarball { url, sha256 } => { takana_core::SourceKind::Tarball { url, sha256 } => {
assert_eq!(url, "https://ftp.gnu.org/gnu/grep/grep-3.11.tar.gz"); assert_eq!(url, "https://ftp.gnu.org/gnu/grep/grep-3.11.tar.gz");
assert_eq!(sha256, "abcdef0123456789"); assert_eq!(sha256, "abcdef0123456789");
} }
@@ -9,8 +9,8 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
use hammer_build::{build, BuildConfig}; use takana_build::{build, BuildConfig};
use hammer_core::{Recipe, Store}; use takana_core::{Recipe, Store};
const HELLO_C: &str = r#" const HELLO_C: &str = r#"
#include <stdio.h> #include <stdio.h>
@@ -11,8 +11,8 @@
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Command; use std::process::Command;
use hammer_build::{build, run_hydrate, BuildConfig}; use takana_build::{build, run_hydrate, BuildConfig};
use hammer_core::{LinkMode, Recipe, Store}; use takana_core::{LinkMode, Recipe, Store};
const RECIPE_TOML: &str = r#" const RECIPE_TOML: &str = r#"
name = "grep" name = "grep"
@@ -1,7 +1,7 @@
//! Integration test del pipeline completo de Fase 0: //! Integration test del pipeline completo de Fase 0:
//! 1. Inicializa un repo git local con un `hello.c`. //! 1. Inicializa un repo git local con un `hello.c`.
//! 2. Escribe una receta TOML que apunta a ese repo con phases override. //! 2. Escribe una receta TOML que apunta a ese repo con phases override.
//! 3. Llama a `hammer_build::build` con un store/work temporales. //! 3. Llama a `takana_build::build` con un store/work temporales.
//! 4. Verifica que el artefacto sellado contiene `usr/bin/hello`, es ELF estático y //! 4. Verifica que el artefacto sellado contiene `usr/bin/hello`, es ELF estático y
//! se ejecuta dentro del sandbox imprimiendo el output esperado. //! se ejecuta dentro del sandbox imprimiendo el output esperado.
//! //!
@@ -12,8 +12,8 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
use hammer_build::{build, BuildConfig}; use takana_build::{build, BuildConfig};
use hammer_core::{Recipe, Store}; use takana_core::{Recipe, Store};
const HELLO_C: &str = r#" const HELLO_C: &str = r#"
#include <stdio.h> #include <stdio.h>
@@ -4,7 +4,7 @@
//! - Genero un tarball en un tempdir con contenido conocido. //! - Genero un tarball en un tempdir con contenido conocido.
//! - Calculo su sha256 real con la misma crate (`sha2`) que usa el fetch. //! - Calculo su sha256 real con la misma crate (`sha2`) que usa el fetch.
//! - Sirvo el tarball vía URL `file://` (curl la maneja nativamente). //! - Sirvo el tarball vía URL `file://` (curl la maneja nativamente).
//! - Llamo a `hammer_build::fetch::fetch` con una receta TOML que apunta a esa URL. //! - Llamo a `takana_build::fetch::fetch` con una receta TOML que apunta a esa URL.
//! //!
//! Cubre: happy path con strip_components, mismatch de sha256 no envenena la caché, reuso //! Cubre: happy path con strip_components, mismatch de sha256 no envenena la caché, reuso
//! de caché en segunda llamada, strip_components = 0 vs 1. //! de caché en segunda llamada, strip_components = 0 vs 1.
@@ -12,8 +12,8 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
use hammer_build::fetch::fetch; use takana_build::fetch::fetch;
use hammer_core::Recipe; use takana_core::Recipe;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
/// Crea `<dir>/inner/{a.txt,sub/b.txt}` y empaqueta `inner/` en `<dir>/payload.tar.gz`. /// Crea `<dir>/inner/{a.txt,sub/b.txt}` y empaqueta `inner/` en `<dir>/payload.tar.gz`.
@@ -12,8 +12,8 @@
use std::path::Path; use std::path::Path;
use hammer_build::hydrate::hydrate; use takana_build::hydrate::hydrate;
use hammer_core::LinkMode; use takana_core::LinkMode;
/// Artefacto que trae `usr/share/pkg` como symlink a `destino`. /// Artefacto que trae `usr/share/pkg` como symlink a `destino`.
fn artefacto_con_symlink(dir: &Path, destino: &Path) { fn artefacto_con_symlink(dir: &Path, destino: &Path) {
@@ -1,5 +1,5 @@
[package] [package]
name = "hammer-cli" name = "takana-cli"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
@@ -23,17 +23,17 @@ path = "src/main.rs"
default = [] default = []
# Activa el traductor LLM real (Claude API) detrás del flag `hammer ai --llm`. # Activa el traductor LLM real (Claude API) detrás del flag `hammer ai --llm`.
# Sin esta feature, `--llm` falla con un mensaje claro pidiendo recompilar. # Sin esta feature, `--llm` falla con un mensaje claro pidiendo recompilar.
llm-claude = ["hammer-agent/llm-claude"] llm-claude = ["takana-agent/llm-claude"]
[dependencies] [dependencies]
hammer-core.workspace = true takana-core.workspace = true
hammer-build.workspace = true takana-build.workspace = true
hammer-bootstrap.workspace = true takana-bootstrap.workspace = true
hammer-mirror.workspace = true takana-mirror.workspace = true
hammer-upgrade.workspace = true takana-upgrade.workspace = true
hammer-overlay.workspace = true takana-overlay.workspace = true
hammer-journal.workspace = true takana-journal.workspace = true
hammer-agent.workspace = true takana-agent.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
toml.workspace = true toml.workspace = true
@@ -334,7 +334,7 @@ package() {
#[test] #[test]
fn extracts_source_patches_deps_phases() { fn extracts_source_patches_deps_phases() {
let toml = recipe_from_apkbuild(APKBUILD_PATCHED).unwrap(); let toml = recipe_from_apkbuild(APKBUILD_PATCHED).unwrap();
let recipe = hammer_core::Recipe::from_toml(&toml).expect("recipe válida"); let recipe = takana_core::Recipe::from_toml(&toml).expect("recipe válida");
assert_eq!(recipe.name, "foo"); assert_eq!(recipe.name, "foo");
assert_eq!(recipe.version, "2.5"); assert_eq!(recipe.version, "2.5");
// tarball con $pkgver expandido: // tarball con $pkgver expandido:
@@ -378,7 +378,7 @@ package() {
#[test] #[test]
fn no_patches_is_empty() { fn no_patches_is_empty() {
let apk = "pkgname=zlib\npkgver=1.3\nsource=\"https://zlib.net/zlib-$pkgver.tar.gz\"\nsha512sums=\"x z\"\n"; let apk = "pkgname=zlib\npkgver=1.3\nsource=\"https://zlib.net/zlib-$pkgver.tar.gz\"\nsha512sums=\"x z\"\n";
let recipe = hammer_core::Recipe::from_toml(&recipe_from_apkbuild(apk).unwrap()).unwrap(); let recipe = takana_core::Recipe::from_toml(&recipe_from_apkbuild(apk).unwrap()).unwrap();
assert_eq!(recipe.name, "zlib"); assert_eq!(recipe.name, "zlib");
assert!(recipe.source.patches.is_empty()); assert!(recipe.source.patches.is_empty());
assert_eq!(recipe.source.tarball.as_deref(), Some("https://zlib.net/zlib-1.3.tar.gz")); assert_eq!(recipe.source.tarball.as_deref(), Some("https://zlib.net/zlib-1.3.tar.gz"));
@@ -8,8 +8,8 @@ use std::path::{Path, PathBuf};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use clap::Subcommand; use clap::Subcommand;
use hammer_core::kernel::kconfig::KconfigTree; use takana_core::kernel::kconfig::KconfigTree;
use hammer_core::kernel::KernelConfig; use takana_core::kernel::KernelConfig;
use serde::Serialize; use serde::Serialize;
/// Dónde busca el árbol de Kconfig si no se pasa `--kconfig`. /// Dónde busca el árbol de Kconfig si no se pasa `--kconfig`.
@@ -425,7 +425,7 @@ struct ClosureReport {
/// De la clausura, los que un humano puede marcar en `menuconfig`. /// De la clausura, los que un humano puede marcar en `menuconfig`.
visible: usize, visible: usize,
/// `select` que entran desde fuera: reencienden algo de la clausura ignorando sus `depends on`. /// `select` que entran desde fuera: reencienden algo de la clausura ignorando sus `depends on`.
leaks: Vec<hammer_core::kernel::SelectLeak>, leaks: Vec<takana_core::kernel::SelectLeak>,
/// Símbolos distintos de la clausura alcanzados por esas fugas. /// Símbolos distintos de la clausura alcanzados por esas fugas.
leaked_targets: usize, leaked_targets: usize,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@@ -447,7 +447,7 @@ struct FixpointReport {
/// Lo que hubo que apagar de más, con la ronda en que apareció. /// Lo que hubo que apagar de más, con la ronda en que apareció.
added: Vec<(usize, String)>, added: Vec<(usize, String)>,
/// Fugas que sobrevivieron al tope de rondas. Vacío = cerró. /// Fugas que sobrevivieron al tope de rondas. Vacío = cerró.
remaining_leaks: Vec<hammer_core::kernel::SelectLeak>, remaining_leaks: Vec<takana_core::kernel::SelectLeak>,
} }
/// Contraste contra un `.config` real: la validación §3 del SDD 22. /// Contraste contra un `.config` real: la validación §3 del SDD 22.
@@ -520,7 +520,7 @@ fn closure(
let on: Vec<String> = cl.iter().filter(|s| cfg.is_on(s)).cloned().collect(); let on: Vec<String> = cl.iter().filter(|s| cfg.is_on(s)).cloned().collect();
let off = cl let off = cl
.iter() .iter()
.filter(|s| matches!(cfg.get(s), Some(hammer_core::kernel::ConfigValue::No))) .filter(|s| matches!(cfg.get(s), Some(takana_core::kernel::ConfigValue::No)))
.count(); .count();
let absent = cl.iter().filter(|s| cfg.get(s).is_none()).count(); let absent = cl.iter().filter(|s| cfg.get(s).is_none()).count();
Some(AgainstReport { Some(AgainstReport {
@@ -644,7 +644,7 @@ fn probe(
with_hw: bool, with_hw: bool,
json: bool, json: bool,
) -> Result<()> { ) -> Result<()> {
use hammer_core::kernel::reverse::{BundleState, HwSignal}; use takana_core::kernel::reverse::{BundleState, HwSignal};
let cfg_path = match config { let cfg_path = match config {
Some(p) => p.to_path_buf(), Some(p) => p.to_path_buf(),
@@ -655,9 +655,9 @@ fn probe(
anyhow::bail!("{} no tiene un solo CONFIG_*: ¿es un .config?", cfg_path.display()); anyhow::bail!("{} no tiene un solo CONFIG_*: ¿es un .config?", cfg_path.display());
} }
let tree = load_tree(kroot)?; let tree = load_tree(kroot)?;
let cat = hammer_core::kernel::Catalog::load(catalog)?; let cat = takana_core::kernel::Catalog::load(catalog)?;
let hw = with_hw.then(hammer_core::kernel::Hardware::probe_local); let hw = with_hw.then(takana_core::kernel::Hardware::probe_local);
let rep = hammer_core::kernel::reverse::analyze(&cat, &tree, &cfg, hw.as_ref()); let rep = takana_core::kernel::reverse::analyze(&cat, &tree, &cfg, hw.as_ref());
if json { if json {
println!("{}", serde_json::to_string_pretty(&rep)?); println!("{}", serde_json::to_string_pretty(&rep)?);
@@ -793,7 +793,7 @@ struct BundleView {
closure_len: usize, closure_len: usize,
visible: usize, visible: usize,
unknown_symbols: Vec<String>, unknown_symbols: Vec<String>,
undeclared_leaks: Vec<hammer_core::kernel::SelectLeak>, undeclared_leaks: Vec<takana_core::kernel::SelectLeak>,
accepted_leaks: usize, accepted_leaks: usize,
} }
@@ -811,7 +811,7 @@ struct BundlesReport {
struct KnobView { struct KnobView {
id: String, id: String,
title: String, title: String,
side: hammer_core::kernel::Side, side: takana_core::kernel::Side,
enable: Vec<String>, enable: Vec<String>,
disable: Vec<String>, disable: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@@ -821,7 +821,7 @@ struct KnobView {
fn bundles(kroot: &Path, catalog: &Path, check: bool, json: bool) -> Result<()> { fn bundles(kroot: &Path, catalog: &Path, check: bool, json: bool) -> Result<()> {
let tree = load_tree(kroot)?; let tree = load_tree(kroot)?;
let cat = hammer_core::kernel::Catalog::load(catalog)?; let cat = takana_core::kernel::Catalog::load(catalog)?;
let mut views = Vec::new(); let mut views = Vec::new();
for b in &cat.bundles { for b in &cat.bundles {
@@ -932,17 +932,17 @@ fn bundles(kroot: &Path, catalog: &Path, check: bool, json: bool) -> Result<()>
println!("NIVEL 2 — política y rendimiento (ojo al lado del que cae cada perilla)"); println!("NIVEL 2 — política y rendimiento (ojo al lado del que cae cada perilla)");
for k in &rep.knobs { for k in &rep.knobs {
let lado = match k.side { let lado = match k.side {
hammer_core::kernel::Side::Kconfig => "kconfig", takana_core::kernel::Side::Kconfig => "kconfig",
hammer_core::kernel::Side::Recipe => "receta ", takana_core::kernel::Side::Recipe => "receta ",
}; };
let que = match k.side { let que = match k.side {
hammer_core::kernel::Side::Kconfig => { takana_core::kernel::Side::Kconfig => {
let mut v: Vec<String> = let mut v: Vec<String> =
k.enable.iter().map(|s| format!("+{s}")).collect(); k.enable.iter().map(|s| format!("+{s}")).collect();
v.extend(k.disable.iter().map(|s| format!("-{s}"))); v.extend(k.disable.iter().map(|s| format!("-{s}")));
v.join(" ") v.join(" ")
} }
hammer_core::kernel::Side::Recipe => { takana_core::kernel::Side::Recipe => {
k.recipe_field.clone().unwrap_or_default() k.recipe_field.clone().unwrap_or_default()
} }
}; };
@@ -988,9 +988,9 @@ fn plan(
if bundle_ids.is_empty() && knob_ids.is_empty() { if bundle_ids.is_empty() && knob_ids.is_empty() {
anyhow::bail!("un plan sin bundles ni perillas es la receta base: pasá --bundle/--knob"); anyhow::bail!("un plan sin bundles ni perillas es la receta base: pasá --bundle/--knob");
} }
let base = hammer_core::Recipe::load_from_path(recipe_path)?; let base = takana_core::Recipe::load_from_path(recipe_path)?;
let tree = load_tree(kroot)?; let tree = load_tree(kroot)?;
let cat = hammer_core::kernel::Catalog::load(catalog)?; let cat = takana_core::kernel::Catalog::load(catalog)?;
// Las recetas de kernel del repo NO son todas la misma versión (`linux`/`linux-metal` van por // Las recetas de kernel del repo NO son todas la misma versión (`linux`/`linux-metal` van por
// 6.16.12; `linux-metal-dual`/`linux-generic` por 7.1.2). Planear una contra el árbol de la otra // 6.16.12; `linux-metal-dual`/`linux-generic` por 7.1.2). Planear una contra el árbol de la otra
@@ -1015,7 +1015,7 @@ fn plan(
Some(_) => {} Some(_) => {}
} }
let mut p = hammer_core::kernel::plan::make_plan( let mut p = takana_core::kernel::plan::make_plan(
&base, &base,
&recipe_path.display().to_string(), &recipe_path.display().to_string(),
&cat, &cat,
@@ -1027,14 +1027,14 @@ fn plan(
// El plan DETERMINA el artefacto (SDD 22 §1), así que el hash va en el plan: la UI puede decir // El plan DETERMINA el artefacto (SDD 22 §1), así que el hash va en el plan: la UI puede decir
// «esto ya está construido y firmado» sin construir nada. Si las deps no están resueltas, se // «esto ya está construido y firmado» sin construir nada. Si las deps no están resueltas, se
// dice que no se pudo en vez de emitir un hash inventado. // dice que no se pudo en vez de emitir un hash inventado.
let derived = hammer_core::kernel::plan::derive_recipe(&base, &p); let derived = takana_core::kernel::plan::derive_recipe(&base, &p);
match hammer_core::Store::open(Path::new(store)) match takana_core::Store::open(Path::new(store))
.map_err(anyhow::Error::from) .map_err(anyhow::Error::from)
.and_then(|s| { .and_then(|s| {
let cfg = hammer_build::BuildConfig::from_env_or_defaults(s.root()); let cfg = takana_build::BuildConfig::from_env_or_defaults(s.root());
let lab = hammer_core::lab::LabFingerprint::from_rootfs(&cfg.rootfs) let lab = takana_core::lab::LabFingerprint::from_rootfs(&cfg.rootfs)
.map_err(anyhow::Error::from)?; .map_err(anyhow::Error::from)?;
hammer_build::artifact_hash(&derived, &s, &lab).map_err(anyhow::Error::from) takana_build::artifact_hash(&derived, &s, &lab).map_err(anyhow::Error::from)
}) })
{ {
Ok(h) => p.artifact_hash = Some(h.to_string()), Ok(h) => p.artifact_hash = Some(h.to_string()),
@@ -1065,7 +1065,7 @@ fn plan(
.map(|d| d.display().to_string()) .map(|d| d.display().to_string())
.unwrap_or_default(), .unwrap_or_default(),
); );
let cuerpo = hammer_core::kernel::plan::render_recipe_toml(&derived)?; let cuerpo = takana_core::kernel::plan::render_recipe_toml(&derived)?;
std::fs::write(ro, format!("{cabecera}{cuerpo}")) std::fs::write(ro, format!("{cabecera}{cuerpo}"))
.with_context(|| format!("escribiendo {}", ro.display()))?; .with_context(|| format!("escribiendo {}", ro.display()))?;
} }
@@ -1091,8 +1091,8 @@ fn plan(
println!("(se emiten RAÍCES: la clausura la calcula el olddefconfig del propio kernel)"); println!("(se emiten RAÍCES: la clausura la calcula el olddefconfig del propio kernel)");
for s in &p.symbols { for s in &p.symbols {
let signo = match s.action { let signo = match s.action {
hammer_core::kernel::plan::Action::Enable => "+", takana_core::kernel::plan::Action::Enable => "+",
hammer_core::kernel::plan::Action::Disable => "-", takana_core::kernel::plan::Action::Disable => "-",
}; };
println!(" {signo}{:<24}{} ({})", s.symbol, s.from, s.reason); println!(" {signo}{:<24}{} ({})", s.symbol, s.from, s.reason);
} }
@@ -1121,7 +1121,7 @@ fn plan(
} }
fn diff_back(plan_path: &Path, config: &Path, json: bool) -> Result<()> { fn diff_back(plan_path: &Path, config: &Path, json: bool) -> Result<()> {
let p: hammer_core::kernel::Plan = serde_json::from_str( let p: takana_core::kernel::Plan = serde_json::from_str(
&std::fs::read_to_string(plan_path) &std::fs::read_to_string(plan_path)
.with_context(|| format!("leyendo {}", plan_path.display()))?, .with_context(|| format!("leyendo {}", plan_path.display()))?,
) )
@@ -1130,7 +1130,7 @@ fn diff_back(plan_path: &Path, config: &Path, json: bool) -> Result<()> {
if cfg.is_empty() { if cfg.is_empty() {
anyhow::bail!("{} no tiene un solo CONFIG_*: ¿es un .config?", config.display()); anyhow::bail!("{} no tiene un solo CONFIG_*: ¿es un .config?", config.display());
} }
let d = hammer_core::kernel::diff_back(&p, &cfg); let d = takana_core::kernel::diff_back(&p, &cfg);
if json { if json {
println!("{}", serde_json::to_string_pretty(&d)?); println!("{}", serde_json::to_string_pretty(&d)?);
@@ -1167,7 +1167,7 @@ fn diff_back(plan_path: &Path, config: &Path, json: bool) -> Result<()> {
// --------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------
fn hw(out: Option<&Path>) -> Result<()> { fn hw(out: Option<&Path>) -> Result<()> {
let h = hammer_core::kernel::Hardware::probe_local(); let h = takana_core::kernel::Hardware::probe_local();
let texto = serde_json::to_string_pretty(&h)?; let texto = serde_json::to_string_pretty(&h)?;
match out { match out {
Some(o) => { Some(o) => {
@@ -1201,7 +1201,7 @@ fn gate(
if plan_path.is_none() && config.is_none() { if plan_path.is_none() && config.is_none() {
anyhow::bail!("pasá --plan (qué apagaría) o --config (qué quedó): sin uno de los dos no hay nada que comparar"); anyhow::bail!("pasá --plan (qué apagaría) o --config (qué quedó): sin uno de los dos no hay nada que comparar");
} }
let p: Option<hammer_core::kernel::Plan> = match plan_path { let p: Option<takana_core::kernel::Plan> = match plan_path {
None => None, None => None,
Some(pp) => Some( Some(pp) => Some(
serde_json::from_str( serde_json::from_str(
@@ -1210,9 +1210,9 @@ fn gate(
.with_context(|| format!("{} no es un plan de `hammer kernel plan`", pp.display()))?, .with_context(|| format!("{} no es un plan de `hammer kernel plan`", pp.display()))?,
), ),
}; };
let cat = hammer_core::kernel::Catalog::load(catalog)?; let cat = takana_core::kernel::Catalog::load(catalog)?;
let tree = load_tree(kroot)?; let tree = load_tree(kroot)?;
let modmap = hammer_core::kernel::ModMap::parse(kroot) let modmap = takana_core::kernel::ModMap::parse(kroot)
.with_context(|| format!("leyendo los Makefiles de {}", kroot.display()))?; .with_context(|| format!("leyendo los Makefiles de {}", kroot.display()))?;
if modmap.is_empty() { if modmap.is_empty() {
anyhow::bail!( anyhow::bail!(
@@ -1223,11 +1223,11 @@ fn gate(
} }
let hardware = match devices { let hardware = match devices {
Some(d) => serde_json::from_str::<hammer_core::kernel::Hardware>( Some(d) => serde_json::from_str::<takana_core::kernel::Hardware>(
&std::fs::read_to_string(d).with_context(|| format!("leyendo {}", d.display()))?, &std::fs::read_to_string(d).with_context(|| format!("leyendo {}", d.display()))?,
) )
.with_context(|| format!("{} no es un volcado de `hammer kernel hw`", d.display()))?, .with_context(|| format!("{} no es un volcado de `hammer kernel hw`", d.display()))?,
None => hammer_core::kernel::Hardware::probe_local(), None => takana_core::kernel::Hardware::probe_local(),
}; };
let r = match config { let r = match config {
@@ -1242,7 +1242,7 @@ fn gate(
anyhow::bail!("{} no tiene un solo CONFIG_*: ¿es un .config?", c.display()); anyhow::bail!("{} no tiene un solo CONFIG_*: ¿es un .config?", c.display());
} }
println!("referente {} ({} símbolos)", base_path.display(), base.len()); println!("referente {} ({} símbolos)", base_path.display(), base.len());
hammer_core::kernel::gate::run_against_config( takana_core::kernel::gate::run_against_config(
&base, &base,
&nuevo, &nuevo,
&cat, &cat,
@@ -1253,7 +1253,7 @@ fn gate(
Some(&tree), Some(&tree),
)? )?
} }
None => hammer_core::kernel::gate::run( None => takana_core::kernel::gate::run(
p.as_ref().expect("validado arriba"), p.as_ref().expect("validado arriba"),
&cat, &cat,
&tree, &tree,
@@ -1312,7 +1312,7 @@ fn gate(
} }
} }
if r.verdict == hammer_core::kernel::Verdict::Bloquea { if r.verdict == takana_core::kernel::Verdict::Bloquea {
anyhow::bail!("el gate bloquea"); anyhow::bail!("el gate bloquea");
} }
Ok(()) Ok(())
@@ -1370,10 +1370,10 @@ fn deps_resuelven_desde(dir: &Path, deps: &[String]) -> bool {
/// Primero por convención (`<dir>/<artifact>.toml`) y sólo si eso no da, barriendo el directorio: /// Primero por convención (`<dir>/<artifact>.toml`) y sólo si eso no da, barriendo el directorio:
/// **el artefacto se nombra por el campo `name`, no por el fichero**, y en este corpus hay recetas /// **el artefacto se nombra por el campo `name`, no por el fichero**, y en este corpus hay recetas
/// donde los dos difieren. /// donde los dos difieren.
fn receta_de(artifact: &str, dirs: &[PathBuf]) -> Option<hammer_core::Recipe> { fn receta_de(artifact: &str, dirs: &[PathBuf]) -> Option<takana_core::Recipe> {
for d in dirs { for d in dirs {
let directo = d.join(format!("{artifact}.toml")); let directo = d.join(format!("{artifact}.toml"));
if let Ok(r) = hammer_core::Recipe::load_from_path(&directo) { if let Ok(r) = takana_core::Recipe::load_from_path(&directo) {
if r.name == artifact { if r.name == artifact {
return Some(con_catalogo(r, dirs)); return Some(con_catalogo(r, dirs));
} }
@@ -1382,7 +1382,7 @@ fn receta_de(artifact: &str, dirs: &[PathBuf]) -> Option<hammer_core::Recipe> {
for e in rd.flatten() { for e in rd.flatten() {
let p = e.path(); let p = e.path();
if p.extension().is_some_and(|x| x == "toml") { if p.extension().is_some_and(|x| x == "toml") {
if let Ok(r) = hammer_core::Recipe::load_from_path(&p) { if let Ok(r) = takana_core::Recipe::load_from_path(&p) {
if r.name == artifact { if r.name == artifact {
return Some(con_catalogo(r, dirs)); return Some(con_catalogo(r, dirs));
} }
@@ -1404,7 +1404,7 @@ fn receta_de(artifact: &str, dirs: &[PathBuf]) -> Option<hammer_core::Recipe> {
/// ///
/// NO se toca si la receta trae `patches`: `base_dir` también los resuelve, y moverlo los rompería /// NO se toca si la receta trae `patches`: `base_dir` también los resuelve, y moverlo los rompería
/// en silencio. Ahí es mejor no saber la vigencia (se comprueba todo) que hashear otra cosa. /// en silencio. Ahí es mejor no saber la vigencia (se comprueba todo) que hashear otra cosa.
fn con_catalogo(mut r: hammer_core::Recipe, dirs: &[PathBuf]) -> hammer_core::Recipe { fn con_catalogo(mut r: takana_core::Recipe, dirs: &[PathBuf]) -> takana_core::Recipe {
if r.deps.build.is_empty() || deps_resuelven_desde(&r.base_dir, &r.deps.build) { if r.deps.build.is_empty() || deps_resuelven_desde(&r.base_dir, &r.deps.build) {
return r; return r;
} }
@@ -1434,13 +1434,13 @@ fn con_catalogo(mut r: hammer_core::Recipe, dirs: &[PathBuf]) -> hammer_core::Re
/// lab de hoy — o sea que un kernel sellado con otro rootfs también sale superado, y es correcto /// lab de hoy — o sea que un kernel sellado con otro rootfs también sale superado, y es correcto
/// (SDD 25: el lab entra en el artefacto). /// (SDD 25: el lab entra en el artefacto).
fn dirs_vigentes( fn dirs_vigentes(
c: &hammer_core::kernel::Contract, c: &takana_core::kernel::Contract,
dirs: &[PathBuf], dirs: &[PathBuf],
store: &str, store: &str,
) -> Result<std::collections::BTreeMap<String, String>> { ) -> Result<std::collections::BTreeMap<String, String>> {
let st = hammer_core::Store::open(Path::new(store))?; let st = takana_core::Store::open(Path::new(store))?;
let cfg = hammer_build::BuildConfig::from_env_or_defaults(st.root()); let cfg = takana_build::BuildConfig::from_env_or_defaults(st.root());
let lab = hammer_core::lab::LabFingerprint::from_rootfs(&cfg.rootfs)?; let lab = takana_core::lab::LabFingerprint::from_rootfs(&cfg.rootfs)?;
let mut out = std::collections::BTreeMap::new(); let mut out = std::collections::BTreeMap::new();
for t in &c.targets { for t in &c.targets {
let Some(r) = receta_de(&t.artifact, dirs) else { let Some(r) = receta_de(&t.artifact, dirs) else {
@@ -1451,7 +1451,7 @@ fn dirs_vigentes(
); );
continue; continue;
}; };
match hammer_build::artifact_hash(&r, &st, &lab) { match takana_build::artifact_hash(&r, &st, &lab) {
Ok(h) => { Ok(h) => {
out.insert(t.artifact.clone(), h.store_dir_name(&r.name)); out.insert(t.artifact.clone(), h.store_dir_name(&r.name));
} }
@@ -1464,10 +1464,10 @@ fn dirs_vigentes(
#[derive(Serialize)] #[derive(Serialize)]
struct ContractRun { struct ContractRun {
contract: String, contract: String,
reports: Vec<hammer_core::kernel::ContractReport>, reports: Vec<takana_core::kernel::ContractReport>,
/// Sellados que la receta de HOY ya no produce. Se informan y no bloquean: ver `dirs_vigentes`. /// Sellados que la receta de HOY ya no produce. Se informan y no bloquean: ver `dirs_vigentes`.
#[serde(default)] #[serde(default)]
superados: Vec<hammer_core::kernel::ContractReport>, superados: Vec<takana_core::kernel::ContractReport>,
/// Configs que NO se comprobaron por no tener perfil declarado. No cuentan como aprobados. /// Configs que NO se comprobaron por no tener perfil declarado. No cuentan como aprobados.
sin_perfil: Vec<String>, sin_perfil: Vec<String>,
/// Objetivos del contrato cuya receta de hoy NO tiene ni un sellado en el store. No se /// Objetivos del contrato cuya receta de hoy NO tiene ni un sellado en el store. No se
@@ -1486,8 +1486,8 @@ fn contract_cmd(
list: bool, list: bool,
json: bool, json: bool,
) -> Result<()> { ) -> Result<()> {
use hammer_core::kernel::contract::sealed_kernel_configs; use takana_core::kernel::contract::sealed_kernel_configs;
use hammer_core::kernel::Contract; use takana_core::kernel::Contract;
let c = Contract::load(contract)?; let c = Contract::load(contract)?;
@@ -1501,7 +1501,7 @@ fn contract_cmd(
println!("revisado contra kernel {r}"); println!("revisado contra kernel {r}");
} }
println!(); println!();
let req = hammer_core::kernel::contract::required_by(&c); let req = takana_core::kernel::contract::required_by(&c);
for cap in &c.capabilities { for cap in &c.capabilities {
let quien = req.get(cap.id.as_str()).map(|v| v.join(", ")).unwrap_or_default(); let quien = req.get(cap.id.as_str()).map(|v| v.join(", ")).unwrap_or_default();
let quien = if quien.is_empty() { "".to_string() } else { quien }; let quien = if quien.is_empty() { "".to_string() } else { quien };
@@ -1586,7 +1586,7 @@ fn contract_cmd(
}; };
for (label, artifact, path) in &objetivos { for (label, artifact, path) in &objetivos {
let cfg = hammer_core::kernel::KernelConfig::parse(&read_maybe_gz(path)?); let cfg = takana_core::kernel::KernelConfig::parse(&read_maybe_gz(path)?);
if cfg.is_empty() { if cfg.is_empty() {
anyhow::bail!("{} no tiene un solo CONFIG_*: ¿es un .config?", path.display()); anyhow::bail!("{} no tiene un solo CONFIG_*: ¿es un .config?", path.display());
} }
@@ -1634,7 +1634,7 @@ fn contract_cmd(
let bloquean = run let bloquean = run
.reports .reports
.iter() .iter()
.filter(|r| r.verdict == hammer_core::kernel::Verdict::Bloquea) .filter(|r| r.verdict == takana_core::kernel::Verdict::Bloquea)
.count(); .count();
if bloquean > 0 { if bloquean > 0 {
anyhow::bail!("el contrato NO se cumple en {bloquean} kernel(es)"); anyhow::bail!("el contrato NO se cumple en {bloquean} kernel(es)");
@@ -1655,7 +1655,7 @@ fn imprimir_contrato(run: &ContractRun) {
println!("contrato {}", run.contract); println!("contrato {}", run.contract);
println!(); println!();
for r in &run.reports { for r in &run.reports {
let marca = if r.verdict == hammer_core::kernel::Verdict::Bloquea { "" } else { "" }; let marca = if r.verdict == takana_core::kernel::Verdict::Bloquea { "" } else { "" };
println!("{marca} {}", r.config); println!("{marca} {}", r.config);
println!(" perfil {} ({} capacidad(es) miradas)", r.profile, r.checked); println!(" perfil {} ({} capacidad(es) miradas)", r.profile, r.checked);
if let Some(k) = &r.kernel { if let Some(k) = &r.kernel {
@@ -1699,13 +1699,13 @@ fn imprimir_contrato(run: &ContractRun) {
let malos = run let malos = run
.reports .reports
.iter() .iter()
.filter(|r| r.verdict == hammer_core::kernel::Verdict::Bloquea) .filter(|r| r.verdict == takana_core::kernel::Verdict::Bloquea)
.count(); .count();
if !run.superados.is_empty() { if !run.superados.is_empty() {
let sup_malos = run let sup_malos = run
.superados .superados
.iter() .iter()
.filter(|r| r.verdict == hammer_core::kernel::Verdict::Bloquea) .filter(|r| r.verdict == takana_core::kernel::Verdict::Bloquea)
.count(); .count();
println!( println!(
"· {} sellado(s) SUPERADO(s) por la receta de hoy — no bloquean, {} con faltas:", "· {} sellado(s) SUPERADO(s) por la receta de hoy — no bloquean, {} con faltas:",
File diff suppressed because it is too large Load Diff
@@ -417,7 +417,7 @@ mod tests {
let pkg: NixPkg = serde_json::from_str(json).unwrap(); let pkg: NixPkg = serde_json::from_str(json).unwrap();
let toml = to_recipe_toml(&pkg).unwrap(); let toml = to_recipe_toml(&pkg).unwrap();
// Parsea como Recipe válida de hammer. // Parsea como Recipe válida de hammer.
let recipe = hammer_core::Recipe::from_toml(&toml).expect("recipe válida"); let recipe = takana_core::Recipe::from_toml(&toml).expect("recipe válida");
assert_eq!(recipe.name, "hello"); assert_eq!(recipe.name, "hello");
assert_eq!(recipe.version, "2.12.1"); assert_eq!(recipe.version, "2.12.1");
// mirror://gnu/ se traduce a una URL concreta que el fetch de hammer entiende. // mirror://gnu/ se traduce a una URL concreta que el fetch de hammer entiende.
@@ -438,7 +438,7 @@ mod tests {
"is_rust": true, "main_program": "rg" "is_rust": true, "main_program": "rg"
}"#; }"#;
let pkg: NixPkg = serde_json::from_str(json).unwrap(); let pkg: NixPkg = serde_json::from_str(json).unwrap();
let recipe = hammer_core::Recipe::from_toml(&to_recipe_toml(&pkg).unwrap()).unwrap(); let recipe = takana_core::Recipe::from_toml(&to_recipe_toml(&pkg).unwrap()).unwrap();
assert_eq!(recipe.build.flags, vec!["--bin", "rg"]); assert_eq!(recipe.build.flags, vec!["--bin", "rg"]);
let inst = recipe.build.phases.install.as_deref().unwrap(); let inst = recipe.build.phases.install.as_deref().unwrap();
assert!(inst.contains("cp target/release/rg /out/usr/bin/rg"), "{inst}"); assert!(inst.contains("cp target/release/rg /out/usr/bin/rg"), "{inst}");
@@ -456,7 +456,7 @@ mod tests {
}"#; }"#;
let pkg: NixPkg = serde_json::from_str(json).unwrap(); let pkg: NixPkg = serde_json::from_str(json).unwrap();
let toml = to_recipe_toml(&pkg).unwrap(); let toml = to_recipe_toml(&pkg).unwrap();
let recipe = hammer_core::Recipe::from_toml(&toml).unwrap(); let recipe = takana_core::Recipe::from_toml(&toml).unwrap();
assert!(recipe.deps.build.is_empty(), "Rust no emite deps activas: {:?}", recipe.deps.build); assert!(recipe.deps.build.is_empty(), "Rust no emite deps activas: {:?}", recipe.deps.build);
assert!(toml.contains("# buildInputs de nix"), "deja provenance comentada: {toml}"); assert!(toml.contains("# buildInputs de nix"), "deja provenance comentada: {toml}");
assert!(toml.contains("pcre2"), "lista los nombres en el comentario: {toml}"); assert!(toml.contains("pcre2"), "lista los nombres en el comentario: {toml}");
@@ -471,7 +471,7 @@ mod tests {
"native_build_inputs": ["install-shell-files", "version-check-hook"] "native_build_inputs": ["install-shell-files", "version-check-hook"]
}"#; }"#;
let pkg: NixPkg = serde_json::from_str(json).unwrap(); let pkg: NixPkg = serde_json::from_str(json).unwrap();
let recipe = hammer_core::Recipe::from_toml(&to_recipe_toml(&pkg).unwrap()).unwrap(); let recipe = takana_core::Recipe::from_toml(&to_recipe_toml(&pkg).unwrap()).unwrap();
// pcre2 sobrevive (paquete real); hooks/wrappers/shell-files y el toolchain rustc/cargo // pcre2 sobrevive (paquete real); hooks/wrappers/shell-files y el toolchain rustc/cargo
// se filtran (rustc/cargo son el lab, no deps). // se filtran (rustc/cargo son el lab, no deps).
assert_eq!(recipe.deps.build, vec!["pcre2"]); assert_eq!(recipe.deps.build, vec!["pcre2"]);
@@ -503,7 +503,7 @@ mod tests {
}"#; }"#;
let pkg: NixPkg = serde_json::from_str(json).unwrap(); let pkg: NixPkg = serde_json::from_str(json).unwrap();
let toml = to_recipe_toml(&pkg).unwrap(); let toml = to_recipe_toml(&pkg).unwrap();
let recipe = hammer_core::Recipe::from_toml(&toml).unwrap(); let recipe = takana_core::Recipe::from_toml(&toml).unwrap();
assert_eq!(recipe.source.repo.as_deref(), Some("https://github.com/BurntSushi/ripgrep")); assert_eq!(recipe.source.repo.as_deref(), Some("https://github.com/BurntSushi/ripgrep"));
assert_eq!( assert_eq!(
recipe.source.commit.as_deref(), recipe.source.commit.as_deref(),
@@ -293,7 +293,7 @@ fn pull(
download_to_file(url, &archive)?; download_to_file(url, &archive)?;
// El corte. Si el contenido no es el que dice ser, se ABORTA — no se avisa y se sigue. // El corte. Si el contenido no es el que dice ser, se ABORTA — no se avisa y se sigue.
hammer_build::download::verify_sha256(&archive, &sha).inspect_err(|_| { takana_build::download::verify_sha256(&archive, &sha).inspect_err(|_| {
std::fs::remove_file(&archive).ok(); std::fs::remove_file(&archive).ok();
})?; })?;
let archive_bytes = std::fs::metadata(&archive)?.len(); let archive_bytes = std::fs::metadata(&archive)?.len();
@@ -636,7 +636,7 @@ fn write_manifest(dir: &Path, m: &ImageManifest) -> Result<()> {
// importada del store y la traída con `pull` serían dos imágenes distintas con los mismos bytes, y // importada del store y la traída con `pull` serían dos imágenes distintas con los mismos bytes, y
// las instancias de dos máquinas dejarían de coincidir — justo lo que el pin existe para evitar. // las instancias de dos máquinas dejarían de coincidir — justo lo que el pin existe para evitar.
fn import(root: &Path, store: &Path, hash: &str, copiar: bool) -> Result<()> { fn import(root: &Path, store: &Path, hash: &str, copiar: bool) -> Result<()> {
let store = hammer_core::store::Store::open(store) let store = takana_core::store::Store::open(store)
.map_err(|e| anyhow::anyhow!("abriendo el store: {e}"))?; .map_err(|e| anyhow::anyhow!("abriendo el store: {e}"))?;
let art = store.find_by_hash(hash) let art = store.find_by_hash(hash)
.map_err(|e| anyhow::anyhow!("{e}"))?; .map_err(|e| anyhow::anyhow!("{e}"))?;
@@ -8,7 +8,7 @@
use std::process::Command; use std::process::Command;
use base64::{engine::general_purpose::STANDARD, Engine as _}; use base64::{engine::general_purpose::STANDARD, Engine as _};
use hammer_core::ArtifactHash; use takana_core::ArtifactHash;
#[test] #[test]
fn install_aborta_ante_colision_de_fichero_y_force_slots_la_supera() { fn install_aborta_ante_colision_de_fichero_y_force_slots_la_supera() {
@@ -10,9 +10,9 @@ use std::collections::BTreeMap;
use std::path::Path; use std::path::Path;
use base64::{engine::general_purpose::STANDARD, Engine as _}; use base64::{engine::general_purpose::STANDARD, Engine as _};
use hammer_core::apply::{apply_config_edit, apply_file_drop, rebase_path, verify_content_hash}; use takana_core::apply::{apply_config_edit, apply_file_drop, rebase_path, verify_content_hash};
use hammer_core::swm::{Base, BaseCompat, BaseRef, Mutation, Swm}; use takana_core::swm::{Base, BaseCompat, BaseRef, Mutation, Swm};
use hammer_core::ArtifactHash; use takana_core::ArtifactHash;
fn write(p: &Path, content: &str) { fn write(p: &Path, content: &str) {
if let Some(parent) = p.parent() { if let Some(parent) = p.parent() {
@@ -1,5 +1,5 @@
[package] [package]
name = "hammer-core" name = "takana-core"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
@@ -41,7 +41,7 @@ pub enum Command {
/// Consulta de estado. `what` decide qué campos se usan: /// Consulta de estado. `what` decide qué campos se usan:
/// - `"file"`: usa `path` (metadata del archivo). /// - `"file"`: usa `path` (metadata del archivo).
/// - `"artifact"`: usa `name` o `path` (busca un hash en el store). /// - `"artifact"`: usa `name` o `path` (busca un hash en el store).
/// - `"expr"`: usa `expr`, una expresión del mini-lenguaje (ver `hammer_core::query`). /// - `"expr"`: usa `expr`, una expresión del mini-lenguaje (ver `takana_core::query`).
Query { Query {
what: String, what: String,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
@@ -162,9 +162,9 @@ pub struct Peer {
pub pid: i32, pub pid: i32,
} }
/// [H1c] Forma serializable del `EvidenceReport` de `hammer_build` que viaja en `BuildReady`. /// [H1c] Forma serializable del `EvidenceReport` de `takana_build` que viaja en `BuildReady`.
/// El lado del build (hammerd) ejecuta la evidencia y la adjunta; el orquestador la lee sin /// El lado del build (hammerd) ejecuta la evidencia y la adjunta; el orquestador la lee sin
/// depender de `hammer_build` (la separación agente-PROPONE / lab-CONSTRUYE se preserva). /// depender de `takana_build` (la separación agente-PROPONE / lab-CONSTRUYE se preserva).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EvidenceVerdict { pub struct EvidenceVerdict {
/// Gate: `false` ⇒ al menos un check declarado falló ⇒ el orquestador no propone la mutación. /// Gate: `false` ⇒ al menos un check declarado falló ⇒ el orquestador no propone la mutación.
@@ -244,7 +244,7 @@ impl Swm {
/// Construye un `.swm` de un único `source_patch` a partir de una [`crate::Recipe`] del /// Construye un `.swm` de un único `source_patch` a partir de una [`crate::Recipe`] del
/// corpus — la dirección **forward** que cierra la paquetería (Etapa F): una receta que el /// corpus — la dirección **forward** que cierra la paquetería (Etapa F): una receta que el
/// sistema YA sabe construir se vuelve un paquete distribuible y reproducible-desde-fuente. /// sistema YA sabe construir se vuelve un paquete distribuible y reproducible-desde-fuente.
/// Es la inversa de `hammer_build::swm_bridge` (que va `source_patch` → `Recipe` → build). /// Es la inversa de `takana_build::swm_bridge` (que va `source_patch` → `Recipe` → build).
/// ///
/// `hammer-core` no toca disco: el caller resuelve y **lee** los patches de la receta /// `hammer-core` no toca disco: el caller resuelve y **lee** los patches de la receta
/// (relativos a `recipe.base_dir`) y pasa su texto ya concatenado en `patch_text`. El /// (relativos a `recipe.base_dir`) y pasa su texto ya concatenado en `patch_text`. El
@@ -1,5 +1,5 @@
[package] [package]
name = "hammer-journal" name = "takana-journal"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
@@ -8,7 +8,7 @@ repository.workspace = true
description = "El diario de mutaciones de hammer: append-only JSON-líneas sobre eventos del FHS." description = "El diario de mutaciones de hammer: append-only JSON-líneas sobre eventos del FHS."
[dependencies] [dependencies]
hammer-core.workspace = true takana-core.workspace = true
anyhow.workspace = true anyhow.workspace = true
thiserror.workspace = true thiserror.workspace = true
tracing.workspace = true tracing.workspace = true
@@ -5,7 +5,7 @@
//! //!
//! Esta crate es **lógica pura**: nada de fanotify ni de mounts. El watcher real vive en //! Esta crate es **lógica pura**: nada de fanotify ni de mounts. El watcher real vive en
//! `hammerd` y empuja eventos aquí vía `Journal::record`. El hook desde //! `hammerd` y empuja eventos aquí vía `Journal::record`. El hook desde
//! `hammer_overlay::commit` también empuja por aquí. //! `takana_overlay::commit` también empuja por aquí.
//! //!
//! Diseño rationale (SDD 05): //! Diseño rationale (SDD 05):
//! - El diario no bloquea acciones; sólo observa. Pero también acepta eventos "trazables" //! - El diario no bloquea acciones; sólo observa. Pero también acepta eventos "trazables"
@@ -1,5 +1,5 @@
[package] [package]
name = "hammer-mirror" name = "takana-mirror"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
@@ -8,7 +8,7 @@ repository.workspace = true
description = "Mirror del store content-addressed de hammer (E3): replica artefactos por hash, verifica of_tree en recepción." description = "Mirror del store content-addressed de hammer (E3): replica artefactos por hash, verifica of_tree en recepción."
[dependencies] [dependencies]
hammer-core.workspace = true takana-core.workspace = true
anyhow.workspace = true anyhow.workspace = true
thiserror.workspace = true thiserror.workspace = true
tracing.workspace = true tracing.workspace = true
@@ -20,7 +20,7 @@
use std::path::Path; use std::path::Path;
use hammer_core::{ArtifactHash, Store}; use takana_core::{ArtifactHash, Store};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
@@ -28,7 +28,7 @@ pub enum Error {
#[error("io: {0}")] #[error("io: {0}")]
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
#[error("core: {0}")] #[error("core: {0}")]
Core(#[from] hammer_core::Error), Core(#[from] takana_core::Error),
#[error( #[error(
"integridad: el artefacto '{dir}' recibido hashea a {got} pero el índice anuncia {want} — \ "integridad: el artefacto '{dir}' recibido hashea a {got} pero el índice anuncia {want} — \
transferencia corrupta o manipulada; NO se selló" transferencia corrupta o manipulada; NO se selló"
@@ -1,5 +1,5 @@
[package] [package]
name = "hammer-overlay" name = "takana-overlay"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
@@ -8,8 +8,8 @@ repository.workspace = true
description = "El overlay de experimentación de hammer: try/commit/discard/status sobre overlayfs." description = "El overlay de experimentación de hammer: try/commit/discard/status sobre overlayfs."
[dependencies] [dependencies]
hammer-core.workspace = true takana-core.workspace = true
hammer-journal.workspace = true takana-journal.workspace = true
anyhow.workspace = true anyhow.workspace = true
thiserror.workspace = true thiserror.workspace = true
tracing.workspace = true tracing.workspace = true
@@ -248,7 +248,7 @@ pub fn commit(id: &OverlayId, state_root: &Path) -> Result<CommitReport> {
pub fn commit_with_journal( pub fn commit_with_journal(
id: &OverlayId, id: &OverlayId,
state_root: &Path, state_root: &Path,
journal: &hammer_journal::Journal, journal: &takana_journal::Journal,
) -> Result<CommitReport> { ) -> Result<CommitReport> {
commit_inner(id, state_root, Some(journal)) commit_inner(id, state_root, Some(journal))
} }
@@ -256,7 +256,7 @@ pub fn commit_with_journal(
fn commit_inner( fn commit_inner(
id: &OverlayId, id: &OverlayId,
state_root: &Path, state_root: &Path,
journal: Option<&hammer_journal::Journal>, journal: Option<&takana_journal::Journal>,
) -> Result<CommitReport> { ) -> Result<CommitReport> {
let base = state_root.join(id.as_str()); let base = state_root.join(id.as_str());
let state_file = base.join("state.json"); let state_file = base.join("state.json");
@@ -295,11 +295,11 @@ fn commit_inner(
} }
fn record_commit_to_journal( fn record_commit_to_journal(
j: &hammer_journal::Journal, j: &takana_journal::Journal,
id: &OverlayId, id: &OverlayId,
report: &CommitReport, report: &CommitReport,
) { ) {
use hammer_journal::{Actor, MutationEvent, MutationOp, Source}; use takana_journal::{Actor, MutationEvent, MutationOp, Source};
let actor = Actor { let actor = Actor {
source: Source::HammerCommit { source: Source::HammerCommit {
overlay: id.as_str().to_string(), overlay: id.as_str().to_string(),
@@ -308,7 +308,7 @@ fn record_commit_to_journal(
pid: Some(std::process::id()), pid: Some(std::process::id()),
uid: Some(unsafe { libc::getuid() }), uid: Some(unsafe { libc::getuid() }),
}; };
let ts = hammer_journal::now_rfc3339(); let ts = takana_journal::now_rfc3339();
for p in &report.copied { for p in &report.copied {
let ev = MutationEvent { let ev = MutationEvent {
ts: ts.clone(), ts: ts.clone(),
@@ -665,7 +665,7 @@ mod tests {
// No tocamos mount real: ejercitamos directamente la función privada que el commit // No tocamos mount real: ejercitamos directamente la función privada que el commit
// delega para anotar al diario. Eso valida el contrato sin necesidad de overlayfs. // delega para anotar al diario. Eso valida el contrato sin necesidad de overlayfs.
let d = tempfile::tempdir().unwrap(); let d = tempfile::tempdir().unwrap();
let journal = hammer_journal::Journal::open(d.path().join("journal")).unwrap(); let journal = takana_journal::Journal::open(d.path().join("journal")).unwrap();
let report = CommitReport { let report = CommitReport {
copied: vec![PathBuf::from("/etc/hosts"), PathBuf::from("/bin/grep")], copied: vec![PathBuf::from("/etc/hosts"), PathBuf::from("/bin/grep")],
removed: vec![PathBuf::from("/bin/obsoleto")], removed: vec![PathBuf::from("/bin/obsoleto")],
@@ -678,13 +678,13 @@ mod tests {
.iter() .iter()
.map(|e| (e.op, e.path.display().to_string())) .map(|e| (e.op, e.path.display().to_string()))
.collect(); .collect();
assert!(ops_paths.contains(&(hammer_journal::MutationOp::Replace, "/etc/hosts".into()))); assert!(ops_paths.contains(&(takana_journal::MutationOp::Replace, "/etc/hosts".into())));
assert!(ops_paths.contains(&(hammer_journal::MutationOp::Replace, "/bin/grep".into()))); assert!(ops_paths.contains(&(takana_journal::MutationOp::Replace, "/bin/grep".into())));
assert!(ops_paths.contains(&(hammer_journal::MutationOp::Delete, "/bin/obsoleto".into()))); assert!(ops_paths.contains(&(takana_journal::MutationOp::Delete, "/bin/obsoleto".into())));
// Todos comparten el mismo source con overlay id. // Todos comparten el mismo source con overlay id.
for e in &events { for e in &events {
match &e.by.source { match &e.by.source {
hammer_journal::Source::HammerCommit { overlay, .. } => { takana_journal::Source::HammerCommit { overlay, .. } => {
assert_eq!(overlay, "1234-1"); assert_eq!(overlay, "1234-1");
} }
other => panic!("source inesperado: {other:?}"), other => panic!("source inesperado: {other:?}"),
@@ -16,7 +16,7 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
use hammer_overlay::{commit, discard, status, try_overlay, OverlayId}; use takana_overlay::{commit, discard, status, try_overlay, OverlayId};
fn have_bwrap() -> bool { fn have_bwrap() -> bool {
Command::new("bwrap") Command::new("bwrap")
@@ -197,7 +197,7 @@ fn body_nested_stack() {
// --- guard LIFO: no se puede commitear la capa de abajo con la de arriba encima --- // --- guard LIFO: no se puede commitear la capa de abajo con la de arriba encima ---
match commit(&id1, &state) { match commit(&id1, &state) {
Err(hammer_overlay::Error::Shadowed { id, shadowed_by }) => { Err(takana_overlay::Error::Shadowed { id, shadowed_by }) => {
assert_eq!(id, id1); assert_eq!(id, id1);
assert_eq!(shadowed_by, vec![id2.clone()]); assert_eq!(shadowed_by, vec![id2.clone()]);
} }
@@ -1,5 +1,5 @@
[package] [package]
name = "hammer-recover" name = "takana-recover"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
@@ -7,10 +7,13 @@ authors.workspace = true
repository.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." 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]] [[bin]]
name = "hammer-recover" name = "hammer-recover"
path = "src/main.rs" path = "src/main.rs"
[dependencies] [dependencies]
hammer-upgrade.workspace = true takana-upgrade.workspace = true
hammer-journal.workspace = true takana-journal.workspace = true
@@ -34,7 +34,7 @@ fn main() {
let state = env_path("HAMMER_RECOVER_STATE", "/var/lib/hammer/upgrades"); let state = env_path("HAMMER_RECOVER_STATE", "/var/lib/hammer/upgrades");
let journal_dir = env_path("HAMMER_RECOVER_JOURNAL", "/var/lib/hammer/journal"); let journal_dir = env_path("HAMMER_RECOVER_JOURNAL", "/var/lib/hammer/journal");
match hammer_upgrade::pending(&state) { match takana_upgrade::pending(&state) {
Ok(None) => { Ok(None) => {
println!("hammer-recover: sin upgrade interrumpido"); println!("hammer-recover: sin upgrade interrumpido");
return; return;
@@ -48,11 +48,11 @@ fn main() {
} }
} }
let journal = hammer_journal::Journal::open(&journal_dir).ok(); let journal = takana_journal::Journal::open(&journal_dir).ok();
let j = journal.as_ref(); let j = journal.as_ref();
if force_rollback { if force_rollback {
match hammer_upgrade::recover(&store, &root, &state, j, true) { match takana_upgrade::recover(&store, &root, &state, j, true) {
Ok(r) => println!("hammer-recover: DESHECHO (generación {} descartada)", r.generation), Ok(r) => println!("hammer-recover: DESHECHO (generación {} descartada)", r.generation),
Err(e) => eprintln!("hammer-recover: rollback falló: {e}"), Err(e) => eprintln!("hammer-recover: rollback falló: {e}"),
} }
@@ -60,11 +60,11 @@ fn main() {
} }
// Optimista: completar el upgrade. Si el roll-forward falla, deshacer ⇒ FHS consistente igual. // Optimista: completar el upgrade. Si el roll-forward falla, deshacer ⇒ FHS consistente igual.
match hammer_upgrade::recover(&store, &root, &state, j, false) { match takana_upgrade::recover(&store, &root, &state, j, false) {
Ok(r) => println!("hammer-recover: COMPLETADO (generación {})", r.generation), Ok(r) => println!("hammer-recover: COMPLETADO (generación {})", r.generation),
Err(e) => { Err(e) => {
eprintln!("hammer-recover: roll-forward falló ({e}); deshaciendo para dejar el FHS consistente"); eprintln!("hammer-recover: roll-forward falló ({e}); deshaciendo para dejar el FHS consistente");
match hammer_upgrade::recover(&store, &root, &state, j, true) { match takana_upgrade::recover(&store, &root, &state, j, true) {
Ok(r) => println!("hammer-recover: DESHECHO tras fallo (generación {})", r.generation), Ok(r) => println!("hammer-recover: DESHECHO tras fallo (generación {})", r.generation),
Err(e2) => eprintln!("hammer-recover: rollback de respaldo también falló: {e2}"), Err(e2) => eprintln!("hammer-recover: rollback de respaldo también falló: {e2}"),
} }
@@ -1,5 +1,5 @@
[package] [package]
name = "hammer-upgrade" name = "takana-upgrade"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
@@ -8,8 +8,8 @@ repository.workspace = true
description = "Upgrades atómicos de hammer (E4): aplica un árbol Stage1/producto del store al root vivo con generaciones y rollback." description = "Upgrades atómicos de hammer (E4): aplica un árbol Stage1/producto del store al root vivo con generaciones y rollback."
[dependencies] [dependencies]
hammer-core.workspace = true takana-core.workspace = true
hammer-journal.workspace = true takana-journal.workspace = true
anyhow.workspace = true anyhow.workspace = true
thiserror.workspace = true thiserror.workspace = true
tracing.workspace = true tracing.workspace = true
@@ -22,8 +22,8 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path; use std::path::Path;
use hammer_core::ArtifactHash; use takana_core::ArtifactHash;
use hammer_journal::Journal; use takana_journal::Journal;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
@@ -3,7 +3,7 @@
//! Aplicar un árbol Stage1/producto **nuevo** al root vivo sin reinstalar, de forma atómica y con //! Aplicar un árbol Stage1/producto **nuevo** al root vivo sin reinstalar, de forma atómica y con
//! **rollback** al árbol anterior. El árbol nuevo es un artefacto sellado del store (CAS BLAKE3); el //! **rollback** al árbol anterior. El árbol nuevo es un artefacto sellado del store (CAS BLAKE3); el
//! "apply" lo proyecta sobre el FHS vivo (`target_root`, normalmente `/`), registra cada cambio en el //! "apply" lo proyecta sobre el FHS vivo (`target_root`, normalmente `/`), registra cada cambio en el
//! [diario](hammer_journal) y deja una **generación** nueva. Si algo sale mal, `rollback` revierte //! [diario](takana_journal) y deja una **generación** nueva. Si algo sale mal, `rollback` revierte
//! exactamente al árbol anterior. //! exactamente al árbol anterior.
//! //!
//! ## Modelo de generaciones (inspirado en NixOS, aplicado in-place al FHS) //! ## Modelo de generaciones (inspirado en NixOS, aplicado in-place al FHS)
@@ -35,8 +35,8 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use hammer_core::{ArtifactHash, Store}; use takana_core::{ArtifactHash, Store};
use hammer_journal::{Actor, Journal, MutationEvent, MutationOp, Source}; use takana_journal::{Actor, Journal, MutationEvent, MutationOp, Source};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
pub mod boot_graph; pub mod boot_graph;
@@ -48,9 +48,9 @@ pub enum Error {
#[error("io: {0}")] #[error("io: {0}")]
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
#[error("core: {0}")] #[error("core: {0}")]
Core(#[from] hammer_core::Error), Core(#[from] takana_core::Error),
#[error("journal: {0}")] #[error("journal: {0}")]
Journal(#[from] hammer_journal::Error), Journal(#[from] takana_journal::Error),
#[error("json: {0}")] #[error("json: {0}")]
Json(#[from] serde_json::Error), Json(#[from] serde_json::Error),
#[error( #[error(
@@ -789,7 +789,7 @@ fn journal_change(journal: Option<&Journal>, tree_dir: &str, dst: &Path, op: Cha
ChangeOp::Removed => MutationOp::Delete, ChangeOp::Removed => MutationOp::Delete,
}; };
let ev = MutationEvent { let ev = MutationEvent {
ts: hammer_journal::now_rfc3339(), ts: takana_journal::now_rfc3339(),
op: mop, op: mop,
path: dst.to_path_buf(), path: dst.to_path_buf(),
by: Actor { by: Actor {
+1 -1
View File
@@ -12,7 +12,7 @@ PKG="${1:?uso: alpine-import.sh <pkg> [main|community]}"
REPO="${2:-main}" REPO="${2:-main}"
BRANCH="${ALPINE_BRANCH:-master}" BRANCH="${ALPINE_BRANCH:-master}"
OUTDIR="${OUTDIR:-.}" OUTDIR="${OUTDIR:-.}"
HAMMER="${HAMMER:-cargo run -q -p hammer-cli --}" HAMMER="${HAMMER:-cargo run -q -p takana-cli --}"
BASE="https://gitlab.alpinelinux.org/alpine/aports/-/raw/${BRANCH}/${REPO}/${PKG}" BASE="https://gitlab.alpinelinux.org/alpine/aports/-/raw/${BRANCH}/${REPO}/${PKG}"
apk=$(curl -fsSL "${BASE}/APKBUILD") || { echo "no encontré ${REPO}/${PKG} en aports@${BRANCH}" >&2; exit 1; } apk=$(curl -fsSL "${BASE}/APKBUILD") || { echo "no encontré ${REPO}/${PKG} en aports@${BRANCH}" >&2; exit 1; }
+1 -1
View File
@@ -218,7 +218,7 @@ else
# imprime (busca «LLD»/«GNU ld»/«GNU gold») y `--enable-lto=cross` necesita un linker con plugin # imprime (busca «LLD»/«GNU ld»/«GNU gold») y `--enable-lto=cross` necesita un linker con plugin
# LLVM; `llvm22-linker-tools` da el LLVMgold.so para GNU ld, pero el camino soportado por # LLVM; `llvm22-linker-tools` da el LLVMgold.so para GNU ld, pero el camino soportado por
# upstream —y el que usa el propio APKBUILD de Alpine con `--enable-linker=lld`— es lld. # upstream —y el que usa el propio APKBUILD de Alpine con `--enable-linker=lld`— es lld.
# ⚠ DEUDA CONOCIDA: `lld` NO casa ningún prefijo de `TOOLCHAIN_PREFIXES` (hammer-core/src/lab.rs) # ⚠ DEUDA CONOCIDA: `lld` NO casa ningún prefijo de `TOOLCHAIN_PREFIXES` (takana-core/src/lab.rs)
# ⇒ su versión NO entra en la huella del lab. Eso es lo que permite añadirlo HOY sin re-hashear # ⇒ su versión NO entra en la huella del lab. Eso es lo que permite añadirlo HOY sin re-hashear
# los 837 artefactos sellados (medido: la huella no se movió), y a la vez es un agujero: dos labs # los 837 artefactos sellados (medido: la huella no se movió), y a la vez es un agujero: dos labs
# con distinto lld pueden sellar bytes distintos en la misma dirección. Sólo expone a las recetas # con distinto lld pueden sellar bytes distintos en la misma dirección. Sólo expone a las recetas
+1 -1
View File
@@ -21,7 +21,7 @@ REPO="${REPO:-$(pwd)/dist/repo}"
STORE="${STORE:-./store}" STORE="${STORE:-./store}"
DISTRO="${DISTRO:-dev}" DISTRO="${DISTRO:-dev}"
BUILD_TIMEOUT="${BUILD_TIMEOUT:-90}" BUILD_TIMEOUT="${BUILD_TIMEOUT:-90}"
HAMMER="cargo run -q -p hammer-cli --" HAMMER="cargo run -q -p takana-cli --"
rm -rf "$REPO" rm -rf "$REPO"
mkdir -p "$REPO" mkdir -p "$REPO"
+1 -1
View File
@@ -56,7 +56,7 @@ while [ "$i" -le "$N" ]; do
--exclude /dist --exclude /tandas --exclude /.dev-fs --exclude /.git --exclude /.scratch \ --exclude /dist --exclude /tandas --exclude /.dev-fs --exclude /.git --exclude /.scratch \
--exclude '*.png' --exclude '/content*' ./ "root@$ip:/opt/hammer/" >/dev/null 2>&1 --exclude '*.png' --exclude '/content*' ./ "root@$ip:/opt/hammer/" >/dev/null 2>&1
hN=$($SSH "root@$ip" "export PATH=\$HOME/.cargo/bin:\$PATH; cd /opt/hammer && \ hN=$($SSH "root@$ip" "export PATH=\$HOME/.cargo/bin:\$PATH; cd /opt/hammer && \
cargo build --release -q -p hammer-cli 2>/dev/null; \ cargo build --release -q -p takana-cli 2>/dev/null; \
timeout ${POR:-900} ./target/release/takana build $RECETA --store \$PWD/store 2>/dev/null \ timeout ${POR:-900} ./target/release/takana build $RECETA --store \$PWD/store 2>/dev/null \
| grep -oE '^b3:[a-f0-9]{64}' | head -1" 2>/dev/null | tr -d '\r') | grep -oE '^b3:[a-f0-9]{64}' | head -1" 2>/dev/null | tr -d '\r')
echo "${hN:-FALLÓ}" echo "${hN:-FALLÓ}"
+1 -1
View File
@@ -228,7 +228,7 @@ fi
# granja podaba eso: `store-gc.sh` mira el store, la poda de caché de CI mira la caché de CI, y este # granja podaba eso: `store-gc.sh` mira el store, la poda de caché de CI mira la caché de CI, y este
# tercer montón crecía sin dueño desde que el store se mudó al volumen. Se recuperaron 70 G. # tercer montón crecía sin dueño desde que el store se mudó al volumen. Se recuperaron 70 G.
# #
# POR QUÉ ES SEGURO: no es una caché. Los dos caminos de `hammer-build/src/fetch.rs` borran el árbol # POR QUÉ ES SEGURO: no es una caché. Los dos caminos de `takana-build/src/fetch.rs` borran el árbol
# y lo re-extraen en CADA build, sin una sola rama que reutilice ⇒ un árbol viejo no acelera nada y # y lo re-extraen en CADA build, sin una sola rama que reutilice ⇒ un árbol viejo no acelera nada y
# borrarlo no cuesta ni la re-extracción. Lo único que se pierde es el post-mortem del último build # borrarlo no cuesta ni la re-extracción. Lo único que se pierde es el post-mortem del último build
# de esa receta, y por eso el suelo son 24 h y no cero. # de esa receta, y por eso el suelo son 24 h y no cero.
+1 -1
View File
@@ -4,7 +4,7 @@
# ── POR QUÉ NO BASTA CON farm-sync ────────────────────────────────────────────────────────────── # ── POR QUÉ NO BASTA CON farm-sync ──────────────────────────────────────────────────────────────
# `farm-sync.sh` sube el código con `--exclude /.dev-fs`, a propósito: el lab es del entorno, no del # `farm-sync.sh` sube el código con `--exclude /.dev-fs`, a propósito: el lab es del entorno, no del
# repo. Eso estaba bien cuando el toolchain NO entraba en el hash. Desde el 2026-08-10 sí entra # repo. Eso estaba bien cuando el toolchain NO entraba en el hash. Desde el 2026-08-10 sí entra
# (`hammer-core/src/lab.rs`), así que un worker con su lab horneado en la golden —rustc 1.96— sella # (`takana-core/src/lab.rs`), así que un worker con su lab horneado en la golden —rustc 1.96— sella
# en direcciones DISTINTAS a las del hub —1.97— y su trabajo no le sirve a nadie: no es que compile # en direcciones DISTINTAS a las del hub —1.97— y su trabajo no le sirve a nadie: no es que compile
# distinto, es que lo guarda en otra dirección y el hub nunca lo encuentra. # distinto, es que lo guarda en otra dirección y el hub nunca lo encuentra.
# #
+1 -1
View File
@@ -83,7 +83,7 @@ EOF
./ "root@$ip:$REMOTE/" >/dev/null ./ "root@$ip:$REMOTE/" >/dev/null
$SSH "root@$ip" "MNT=/mnt/cosecha; mkdir -p \$MNT; mount $DEV \$MNT 2>/dev/null || (mkfs.ext4 -q -F $DEV && mount $DEV \$MNT); mkdir -p \$MNT/verdicts \$MNT/logs" $SSH "root@$ip" "MNT=/mnt/cosecha; mkdir -p \$MNT; mount $DEV \$MNT 2>/dev/null || (mkfs.ext4 -q -F $DEV && mount $DEV \$MNT); mkdir -p \$MNT/verdicts \$MNT/logs"
scripts/farm/harkaq-farm-setup.sh "$ip" >/dev/null 2>&1 || echo " (setup con avisos, sigo)" scripts/farm/harkaq-farm-setup.sh "$ip" >/dev/null 2>&1 || echo " (setup con avisos, sigo)"
$SSH "root@$ip" "export PATH=\$HOME/.cargo/bin:\$PATH; cd $REMOTE && cargo build --release -q -p hammer-cli 2>&1 | tail -1" $SSH "root@$ip" "export PATH=\$HOME/.cargo/bin:\$PATH; cd $REMOTE && cargo build --release -q -p takana-cli 2>&1 | tail -1"
# Servicio con auto-shutdown (el script harkaq-campana-vol lo maneja). # Servicio con auto-shutdown (el script harkaq-campana-vol lo maneja).
$SSH "root@$ip" "cat > /etc/systemd/system/harkaq-vol.service <<UNIT $SSH "root@$ip" "cat > /etc/systemd/system/harkaq-vol.service <<UNIT
[Unit] [Unit]
+1 -1
View File
@@ -34,7 +34,7 @@ DRY="${DRY:-0}"
SKIP_GUI="${SKIP_GUI:-1}" SKIP_GUI="${SKIP_GUI:-1}"
GUI_RE='^(cairo|pango|gtk|harfbuzz|glib|gdk-pixbuf|libadwaita|gtksourceview|fribidi|graphene|fontconfig|adwaita|fcft|.*-hello|mirada|appstream|gnome)' GUI_RE='^(cairo|pango|gtk|harfbuzz|glib|gdk-pixbuf|libadwaita|gtksourceview|fribidi|graphene|fontconfig|adwaita|fcft|.*-hello|mirada|appstream|gnome)'
[ -x "$HAMMER" ] || { echo "no encuentro hammer en $HAMMER (cargo build --release -p hammer-cli)"; exit 1; } [ -x "$HAMMER" ] || { echo "no encuentro hammer en $HAMMER (cargo build --release -p takana-cli)"; exit 1; }
deuda=""; skipped_gui="" deuda=""; skipped_gui=""
for f in $(grep -lE '^[[:space:]]*link[[:space:]]*=[[:space:]]*"static"' recipes/*.toml); do for f in $(grep -lE '^[[:space:]]*link[[:space:]]*=[[:space:]]*"static"' recipes/*.toml); do
+1 -1
View File
@@ -57,7 +57,7 @@ $SSH "root@$IP" "cd $REMOTE &&
echo \" runtime base: \$(grep -c '^ro ' /root/.cache/harkaq/base.policy) reglas + \$(grep -c '^# expect' /root/.cache/harkaq/base.policy) esperadas\"" echo \" runtime base: \$(grep -c '^ro ' /root/.cache/harkaq/base.policy) reglas + \$(grep -c '^# expect' /root/.cache/harkaq/base.policy) esperadas\""
echo "== 4/4 comprobando la cadena de evidencia de punta a punta" echo "== 4/4 comprobando la cadena de evidencia de punta a punta"
$SSH "root@$IP" "cd $REMOTE && cargo build -q -p hammer-cli 2>/dev/null || true; ls target/debug/hammer >/dev/null 2>&1 && echo ' hammer-cli listo ✓' || echo ' OJO: falta target/debug/hammer (cargo build -p hammer-cli)'" $SSH "root@$IP" "cd $REMOTE && cargo build -q -p takana-cli 2>/dev/null || true; ls target/debug/hammer >/dev/null 2>&1 && echo ' hammer-cli listo ✓' || echo ' OJO: falta target/debug/hammer (cargo build -p takana-cli)'"
cat <<EOF cat <<EOF
+1 -1
View File
@@ -149,7 +149,7 @@ if [ -x "$HAMMER_MUSL" ]; then
cp "$HAMMER_MUSL" "$ROOT_TREE/usr/bin/hammer"; chmod 0755 "$ROOT_TREE/usr/bin/hammer" cp "$HAMMER_MUSL" "$ROOT_TREE/usr/bin/hammer"; chmod 0755 "$ROOT_TREE/usr/bin/hammer"
echo " + CLI hammer (static musl) para el menú de arranque por grafo" echo " + CLI hammer (static musl) para el menú de arranque por grafo"
else else
echo " ! sin hammer musl ($HAMMER_MUSL) ⇒ el disco no traerá el menú de arranque (cargo build --target x86_64-unknown-linux-musl -p hammer-cli)" >&2 echo " ! sin hammer musl ($HAMMER_MUSL) ⇒ el disco no traerá el menú de arranque (cargo build --target x86_64-unknown-linux-musl -p takana-cli)" >&2
fi fi
# /sbin/init: wrapper que emite un marcador a /dev/ttyS0 (sobrevive `quiet`, que suprime el printk # /sbin/init: wrapper que emite un marcador a /dev/ttyS0 (sobrevive `quiet`, que suprime el printk
# kernel-side) y hace exec de arje-zero. Prueba positiva de que el switch_root llegó al init real. # kernel-side) y hace exec de arje-zero. Prueba positiva de que el switch_root llegó al init real.
+2 -2
View File
@@ -121,7 +121,7 @@ if [ "${INSTALLER:-0}" = 1 ]; then
RECOVER_BIN="$ROOT/target/x86_64-unknown-linux-musl/release/hammer-recover" RECOVER_BIN="$ROOT/target/x86_64-unknown-linux-musl/release/hammer-recover"
if [ ! -x "$RECOVER_BIN" ]; then if [ ! -x "$RECOVER_BIN" ]; then
echo "==> compilando hammer-recover (static musl) para el payload" echo "==> compilando hammer-recover (static musl) para el payload"
cargo build --release --target x86_64-unknown-linux-musl -p hammer-recover --manifest-path "$ROOT/Cargo.toml" >/dev/null 2>&1 cargo build --release --target x86_64-unknown-linux-musl -p takana-recover --manifest-path "$ROOT/Cargo.toml" >/dev/null 2>&1
fi fi
[ -x "$RECOVER_BIN" ] || { echo "no pude construir hammer-recover (target musl?)" >&2; exit 1; } [ -x "$RECOVER_BIN" ] || { echo "no pude construir hammer-recover (target musl?)" >&2; exit 1; }
install -m 0755 "$RECOVER_BIN" "$IPL/hammer-recover" install -m 0755 "$RECOVER_BIN" "$IPL/hammer-recover"
@@ -131,7 +131,7 @@ if [ "${INSTALLER:-0}" = 1 ]; then
HAMMER_BIN="$ROOT/target/x86_64-unknown-linux-musl/release/hammer" HAMMER_BIN="$ROOT/target/x86_64-unknown-linux-musl/release/hammer"
if [ ! -x "$HAMMER_BIN" ]; then if [ ! -x "$HAMMER_BIN" ]; then
echo "==> compilando hammer CLI (static musl) para el payload" echo "==> compilando hammer CLI (static musl) para el payload"
cargo build --release --target x86_64-unknown-linux-musl -p hammer-cli --manifest-path "$ROOT/Cargo.toml" >/dev/null 2>&1 cargo build --release --target x86_64-unknown-linux-musl -p takana-cli --manifest-path "$ROOT/Cargo.toml" >/dev/null 2>&1
fi fi
[ -x "$HAMMER_BIN" ] || { echo "no pude construir el CLI hammer (target musl?)" >&2; exit 1; } [ -x "$HAMMER_BIN" ] || { echo "no pude construir el CLI hammer (target musl?)" >&2; exit 1; }
install -m 0755 "$HAMMER_BIN" "$IPL/hammer" install -m 0755 "$HAMMER_BIN" "$IPL/hammer"
+1 -1
View File
@@ -2,7 +2,7 @@
# lab-image.sh — empaqueta y publica el ROOTFS YA BOOTSTRAPEADO como imagen pineada por sha256. # lab-image.sh — empaqueta y publica el ROOTFS YA BOOTSTRAPEADO como imagen pineada por sha256.
# #
# ── POR QUÉ EXISTE ────────────────────────────────────────────────────────────────────────────── # ── POR QUÉ EXISTE ──────────────────────────────────────────────────────────────────────────────
# Desde el 2026-08-10 el toolchain del lab entra en `hash_inputs` (ver `hammer-core/src/lab.rs`): # Desde el 2026-08-10 el toolchain del lab entra en `hash_inputs` (ver `takana-core/src/lab.rs`):
# dos máquinas con distinto `rustc` sellan en direcciones distintas. Eso cierra un agujero real # dos máquinas con distinto `rustc` sellan en direcciones distintas. Eso cierra un agujero real
# —antes sellaban bytes distintos en la MISMA dirección— pero abre uno operativo: # —antes sellaban bytes distintos en la MISMA dirección— pero abre uno operativo:
# #
+2 -2
View File
@@ -4,12 +4,12 @@
# nixpkgs es el mayor set de recetas desde fuente; importamos la receta y hammer reconstruye. # nixpkgs es el mayor set de recetas desde fuente; importamos la receta y hammer reconstruye.
# #
# Requiere `nix` (con nix-command). Ej: scripts/nix-import.sh hello > recipes/hello.toml # Requiere `nix` (con nix-command). Ej: scripts/nix-import.sh hello > recipes/hello.toml
# Env: NIXPKGS=<flakeref> (default nixpkgs), HAMMER=<cmd> (default cargo run -q -p hammer-cli --) # Env: NIXPKGS=<flakeref> (default nixpkgs), HAMMER=<cmd> (default cargo run -q -p takana-cli --)
set -eu set -eu
ATTR="${1:?uso: nix-import.sh <attr-de-nixpkgs> (ej: hello, jq, ripgrep)}" ATTR="${1:?uso: nix-import.sh <attr-de-nixpkgs> (ej: hello, jq, ripgrep)}"
NIXPKGS="${NIXPKGS:-nixpkgs}" NIXPKGS="${NIXPKGS:-nixpkgs}"
HAMMER="${HAMMER:-cargo run -q -p hammer-cli --}" HAMMER="${HAMMER:-cargo run -q -p takana-cli --}"
# NIX_STORE: si el /nix/store del sistema no es escribible (install single-user incompleta), apuntá # NIX_STORE: si el /nix/store del sistema no es escribible (install single-user incompleta), apuntá
# a un store local, p.ej. NIX_STORE="local?root=$HOME/.nixstore". # a un store local, p.ej. NIX_STORE="local?root=$HOME/.nixstore".
STORE_OPT="" STORE_OPT=""

Some files were not shown because too many files have changed in this diff Show More