Scaffold inicial: workspace Rust + SDDs completos
Arranque del proyecto hammer (distro AI-nativa: laboratorio funcional en el sótano, terminal mutable clásica arriba, integración de IA programadora). - Workspace Rust (compila, tests verdes): hammer-core, hammer-build, hammer-cli (bin `hammer`), hammerd. - SDDs 00-10 + 6 ADRs en docs/ con toda la arquitectura. - Esqueletos navegables mapeados a las fases del roadmap; Fase 0/1 listas para implementar el sandbox real. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "hammer-build"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
description = "El laboratorio de hammer: sandbox de build (bubblewrap + zig cc) e hidratación."
|
||||
|
||||
[dependencies]
|
||||
hammer-core.workspace = true
|
||||
anyhow.workspace = true
|
||||
thiserror.workspace = true
|
||||
tracing.workspace = true
|
||||
@@ -0,0 +1,55 @@
|
||||
//! El laboratorio: compila una receta de forma hermética y la sella en el store; e hidrata
|
||||
//! artefactos al FHS. Ver `docs/02-build-lab.md` y `docs/03-hydration.md`.
|
||||
//!
|
||||
//! Esqueleto de Fase 0. Las funciones públicas fijan el contrato; la implementación del
|
||||
//! sandbox (bubblewrap) y de patchelf se irá rellenando.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use hammer_core::{ArtifactHash, LinkMode, Recipe, Store};
|
||||
|
||||
pub mod sandbox;
|
||||
|
||||
/// Calcula el `ArtifactHash` de una receta, resolviendo recursivamente sus deps de build.
|
||||
/// Ver `docs/02-build-lab.md` §2 y §5.
|
||||
pub fn artifact_hash(recipe: &Recipe, store: &Store) -> hammer_core::Result<ArtifactHash> {
|
||||
let dep_hashes: Vec<ArtifactHash> = Vec::new();
|
||||
for dep_name in &recipe.deps.build {
|
||||
// TODO(fase-0): cargar la receta de la dep y recursar. Por ahora se documenta el
|
||||
// contrato; el grafo real se implementa con `hammer build`.
|
||||
let _ = dep_name;
|
||||
}
|
||||
let inputs = recipe.hash_inputs(&dep_hashes);
|
||||
let refs: Vec<&[u8]> = inputs.iter().map(|v| v.as_slice()).collect();
|
||||
let _ = store;
|
||||
Ok(ArtifactHash::of_inputs(&refs))
|
||||
}
|
||||
|
||||
/// Compila una receta y devuelve el hash del artefacto sellado en el store.
|
||||
/// Si el hash ya existe en el store, devuelve sin recompilar (caché).
|
||||
pub fn build(recipe: &Recipe, store: &Store) -> hammer_core::Result<ArtifactHash> {
|
||||
let h = artifact_hash(recipe, store)?;
|
||||
if store.has(&h, &recipe.name) {
|
||||
tracing::info!(hash = %h, name = %recipe.name, "caché: artefacto ya en el store");
|
||||
return Ok(h);
|
||||
}
|
||||
// TODO(fase-0): resolver deps → make_sandbox → run_phases(patch/configure/compile/install)
|
||||
// → store.seal(out, &h, &recipe.name).
|
||||
tracing::warn!("build real pendiente (Fase 0): sandbox bubblewrap + zig cc");
|
||||
Err(hammer_core::Error::Other(anyhow::anyhow!(
|
||||
"build no implementado todavía; ver docs/10-roadmap.md (Fase 0)"
|
||||
)))
|
||||
}
|
||||
|
||||
/// Proyecta un artefacto del store al FHS (real o de overlay). Ver `docs/03-hydration.md`.
|
||||
pub fn hydrate(
|
||||
_h: &ArtifactHash,
|
||||
_store: &Store,
|
||||
_target_fhs: &Path,
|
||||
_mode: LinkMode,
|
||||
) -> hammer_core::Result<()> {
|
||||
// TODO(fase-1): hardlink del store al FHS; patchelf (set-interpreter/set-rpath) si Dynamic.
|
||||
Err(hammer_core::Error::Other(anyhow::anyhow!(
|
||||
"hidratación no implementada todavía; ver docs/10-roadmap.md (Fase 1)"
|
||||
)))
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
//! El sandbox hermético de build (bubblewrap). Ver `docs/02-build-lab.md` §3 y §4.
|
||||
//!
|
||||
//! Esqueleto de Fase 0. Define la forma del aislamiento que implementaremos: raíz tmpfs,
|
||||
//! compilador read-only inyectado, fuentes/deps read-only, sin red, salida vía DESTDIR.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Descripción de un sandbox de build a montar con `bwrap`.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Sandbox {
|
||||
/// Binds read-only: (origen_host, destino_sandbox).
|
||||
pub ro_binds: Vec<(PathBuf, PathBuf)>,
|
||||
/// Directorio de salida (DESTDIR) dentro del sandbox.
|
||||
pub out: PathBuf,
|
||||
/// Aislar red (hermeticidad: el build no descarga nada no declarado).
|
||||
pub isolate_net: bool,
|
||||
}
|
||||
|
||||
impl Sandbox {
|
||||
/// Construye los argumentos para `bwrap`. TODO(fase-0): completar y ejecutar.
|
||||
pub fn bwrap_args(&self) -> Vec<String> {
|
||||
let mut args = vec![
|
||||
"--unshare-all".into(),
|
||||
"--tmpfs".into(),
|
||||
"/".into(),
|
||||
"--proc".into(),
|
||||
"/proc".into(),
|
||||
"--dev".into(),
|
||||
"/dev".into(),
|
||||
];
|
||||
if !self.isolate_net {
|
||||
args.push("--share-net".into());
|
||||
}
|
||||
for (src, dst) in &self.ro_binds {
|
||||
args.push("--ro-bind".into());
|
||||
args.push(src.display().to_string());
|
||||
args.push(dst.display().to_string());
|
||||
}
|
||||
args
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "hammer-cli"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
description = "El binario `hammer`: orquesta build, hydrate, try/commit, apply/export y ctl."
|
||||
|
||||
[[bin]]
|
||||
name = "hammer"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
hammer-core.workspace = true
|
||||
hammer-build.workspace = true
|
||||
anyhow.workspace = true
|
||||
clap.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
@@ -0,0 +1,99 @@
|
||||
//! `hammer` — el punto de entrada de todo flujo manual. Ver `docs/`.
|
||||
//!
|
||||
//! Los subcomandos están mapeados a las fases del roadmap (`docs/10-roadmap.md`). Los que aún
|
||||
//! no están implementados devuelven un error claro indicando su fase, para que el esqueleto
|
||||
//! sea navegable desde el día uno.
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
const DEFAULT_STORE: &str = "/store";
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
name = "hammer",
|
||||
version,
|
||||
about = "Laboratorio funcional en el sótano, terminal mutable arriba.",
|
||||
long_about = "hammer: distro AI-nativa. Ver docs/ para el diseño completo."
|
||||
)]
|
||||
struct Cli {
|
||||
/// Ruta del content-addressed store.
|
||||
#[arg(long, default_value = DEFAULT_STORE, global = true)]
|
||||
store: String,
|
||||
|
||||
#[command(subcommand)]
|
||||
cmd: Cmd,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Cmd {
|
||||
/// [Fase 0] Compila una receta y la sella en el store.
|
||||
Build {
|
||||
/// Ruta a la receta TOML.
|
||||
recipe: String,
|
||||
},
|
||||
/// [Fase 1] Proyecta un artefacto del store al FHS (real o de overlay).
|
||||
Hydrate {
|
||||
hash: String,
|
||||
#[arg(long, default_value = "/")]
|
||||
into: String,
|
||||
},
|
||||
/// [Fase 2] Monta un overlay de experimentación sobre los directorios del sistema.
|
||||
Try,
|
||||
/// [Fase 2] Fusiona el overlay activo al FHS real (registra en el diario).
|
||||
Commit,
|
||||
/// [Fase 2] Descarta el overlay activo; vuelve al estado base.
|
||||
Discard,
|
||||
/// [Fase 2] Muestra el estado de overlays activos.
|
||||
Status,
|
||||
/// [Fase 3] Ver/seguir el diario de mutaciones.
|
||||
Journal,
|
||||
/// [Fase 4] Aplica un manifiesto .swm (reproduce y lo deja en un overlay).
|
||||
Apply { file: String },
|
||||
/// [Fase 4] Exporta el delta del sistema como manifiesto .swm.
|
||||
Export {
|
||||
#[arg(long)]
|
||||
base: Option<String>,
|
||||
},
|
||||
/// [Fase 5] Envía un comando al init (proxy a /run/init.control).
|
||||
Ctl { line: String },
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| "info".into()),
|
||||
)
|
||||
.init();
|
||||
|
||||
let cli = Cli::parse();
|
||||
let store = hammer_core::Store::open(&cli.store)?;
|
||||
|
||||
match cli.cmd {
|
||||
Cmd::Build { recipe } => {
|
||||
let text = std::fs::read_to_string(&recipe)?;
|
||||
let recipe = hammer_core::Recipe::from_toml(&text)?;
|
||||
let hash = hammer_build::build(&recipe, &store)?;
|
||||
println!("{hash}");
|
||||
}
|
||||
Cmd::Hydrate { hash, into } => {
|
||||
println!("[fase 1 pendiente] hydrate {hash} into {into}");
|
||||
}
|
||||
Cmd::Try | Cmd::Commit | Cmd::Discard | Cmd::Status => {
|
||||
println!("[fase 2 pendiente] overlay — ver docs/04-overlay.md");
|
||||
}
|
||||
Cmd::Journal => {
|
||||
println!("[fase 3 pendiente] diario — ver docs/05-journal.md");
|
||||
}
|
||||
Cmd::Apply { file } => {
|
||||
println!("[fase 4 pendiente] apply {file} — ver docs/06-swm-format.md");
|
||||
}
|
||||
Cmd::Export { base } => {
|
||||
println!("[fase 4 pendiente] export base={base:?} — ver docs/05-journal.md");
|
||||
}
|
||||
Cmd::Ctl { line } => {
|
||||
println!("[fase 5 pendiente] ctl {line:?} — ver docs/07-agent-bus.md");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "hammer-core"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Tipos compartidos de hammer: Recipe, Swm, hashing CAS y el store."
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
thiserror.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_yaml.workspace = true
|
||||
blake3.workspace = true
|
||||
@@ -0,0 +1,66 @@
|
||||
//! Direccionamiento por contenido (CAS). Ver `docs/02-build-lab.md` §2.
|
||||
//!
|
||||
//! El `ArtifactHash` identifica un artefacto por TODO lo que influye en su salida:
|
||||
//! commit fuente + parches + flags/compilador/target + hashes de dependencias.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Hash BLAKE3 de un artefacto, con prefijo legible `b3:`.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct ArtifactHash(String);
|
||||
|
||||
impl ArtifactHash {
|
||||
/// Construye desde bytes ya hasheados (representación hex).
|
||||
pub fn from_hex(hex: impl Into<String>) -> Self {
|
||||
ArtifactHash(format!("b3:{}", hex.into()))
|
||||
}
|
||||
|
||||
/// Hashea un conjunto ordenado de entradas. El llamador es responsable de pasar las
|
||||
/// entradas en orden canónico y estable (ver `docs/02-build-lab.md` §2).
|
||||
pub fn of_inputs(inputs: &[&[u8]]) -> Self {
|
||||
let mut hasher = blake3::Hasher::new();
|
||||
for chunk in inputs {
|
||||
// length-prefijado para evitar colisiones por concatenación ambigua.
|
||||
hasher.update(&(chunk.len() as u64).to_le_bytes());
|
||||
hasher.update(chunk);
|
||||
}
|
||||
ArtifactHash(format!("b3:{}", hasher.finalize().to_hex()))
|
||||
}
|
||||
|
||||
/// Forma corta para directorios del store: `<hash-sin-prefijo>-<name>`.
|
||||
pub fn store_dir_name(&self, name: &str) -> String {
|
||||
let bare = self.0.strip_prefix("b3:").unwrap_or(&self.0);
|
||||
format!("{bare}-{name}")
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ArtifactHash {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn deterministic_and_order_sensitive() {
|
||||
let a = ArtifactHash::of_inputs(&[b"grep", b"abc123", b"--static"]);
|
||||
let b = ArtifactHash::of_inputs(&[b"grep", b"abc123", b"--static"]);
|
||||
assert_eq!(a, b, "misma entrada ⇒ mismo hash");
|
||||
|
||||
let c = ArtifactHash::of_inputs(&[b"abc123", b"grep", b"--static"]);
|
||||
assert_ne!(a, c, "orden distinto ⇒ hash distinto");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn store_dir_name_strips_prefix() {
|
||||
let h = ArtifactHash::from_hex("deadbeef");
|
||||
assert_eq!(h.store_dir_name("grep"), "deadbeef-grep");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//! Tipos núcleo de hammer, compartidos por el lab, la CLI y el daemon.
|
||||
//!
|
||||
//! Ver `docs/01-architecture.md` y siguientes. Esto es el esqueleto de Fase 0: los tipos y
|
||||
//! contratos están definidos; la lógica pesada (sandbox, fanotify, bus) vive en los otros
|
||||
//! crates y se irá rellenando por fase.
|
||||
|
||||
pub mod hash;
|
||||
pub mod recipe;
|
||||
pub mod store;
|
||||
pub mod swm;
|
||||
|
||||
pub use hash::ArtifactHash;
|
||||
pub use recipe::{Compiler, LinkMode, Recipe};
|
||||
pub use store::Store;
|
||||
pub use swm::Swm;
|
||||
|
||||
/// Error común del ecosistema hammer.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("io: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("serialización: {0}")]
|
||||
Serde(String),
|
||||
#[error("receta inválida: {0}")]
|
||||
Recipe(String),
|
||||
#[error("store: {0}")]
|
||||
Store(String),
|
||||
#[error(transparent)]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -0,0 +1,109 @@
|
||||
//! La `Recipe`: descripción pura de un build. Ver `docs/02-build-lab.md` §1.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::hash::ArtifactHash;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Recipe {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub source: Source,
|
||||
pub build: Build,
|
||||
#[serde(default)]
|
||||
pub deps: Deps,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Source {
|
||||
pub repo: String,
|
||||
/// Commit FIJADO. Nunca "HEAD". Ver ADR 0006.
|
||||
pub commit: String,
|
||||
#[serde(default)]
|
||||
pub patches: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Build {
|
||||
#[serde(default)]
|
||||
pub compiler: Compiler,
|
||||
#[serde(default = "default_target")]
|
||||
pub target: String,
|
||||
#[serde(default)]
|
||||
pub link: LinkMode,
|
||||
#[serde(default)]
|
||||
pub flags: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct Deps {
|
||||
#[serde(default)]
|
||||
pub build: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub runtime: Vec<String>,
|
||||
}
|
||||
|
||||
/// Compilador del lab, POR RECETA (no global). `zig-cc` por defecto; escotilla a clang/gcc
|
||||
/// para paquetes con gcc-ismos. Ver ADR 0003.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum Compiler {
|
||||
#[default]
|
||||
ZigCc,
|
||||
Clang,
|
||||
Gcc,
|
||||
}
|
||||
|
||||
impl Compiler {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Compiler::ZigCc => "zig-cc",
|
||||
Compiler::Clang => "clang",
|
||||
Compiler::Gcc => "gcc",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum LinkMode {
|
||||
#[default]
|
||||
Static,
|
||||
Dynamic,
|
||||
}
|
||||
|
||||
fn default_target() -> String {
|
||||
"x86_64-linux-musl".to_string()
|
||||
}
|
||||
|
||||
impl Recipe {
|
||||
pub fn from_toml(_s: &str) -> crate::Result<Recipe> {
|
||||
// TODO(fase-0): parsear TOML. Por ahora el parsing real lo añadiremos al implementar
|
||||
// `hammer build`; el contrato (esta struct) ya está fijado.
|
||||
Err(crate::Error::Recipe(
|
||||
"parsing de receta TOML pendiente (Fase 0)".into(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Las entradas canónicas que alimentan el `ArtifactHash` de esta receta.
|
||||
/// NOTA: el hash final también incorpora los hashes de las deps de build (recursivo),
|
||||
/// que el lab resuelve antes de llamar aquí. Ver `docs/02-build-lab.md` §2 y §5.
|
||||
pub fn hash_inputs(&self, dep_hashes: &[ArtifactHash]) -> Vec<Vec<u8>> {
|
||||
let mut v: Vec<Vec<u8>> = vec![
|
||||
self.source.commit.as_bytes().to_vec(),
|
||||
self.build.compiler.as_str().as_bytes().to_vec(),
|
||||
self.build.target.as_bytes().to_vec(),
|
||||
format!("{:?}", self.build.link).into_bytes(),
|
||||
];
|
||||
for p in &self.source.patches {
|
||||
v.push(p.as_bytes().to_vec());
|
||||
}
|
||||
for f in &self.build.flags {
|
||||
v.push(f.as_bytes().to_vec());
|
||||
}
|
||||
for d in dep_hashes {
|
||||
v.push(d.as_str().as_bytes().to_vec());
|
||||
}
|
||||
v
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
//! El content-addressed store. Ver `docs/03-hydration.md` §1.
|
||||
//!
|
||||
//! Inmutable, append-only, direccionado por `ArtifactHash`. Esqueleto de Fase 0: la API está
|
||||
//! fijada; el sellado real (mover el árbol de salida del sandbox e idealmente hacerlo de sólo
|
||||
//! lectura) se completa al implementar el builder.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::hash::ArtifactHash;
|
||||
|
||||
pub struct Store {
|
||||
root: PathBuf,
|
||||
}
|
||||
|
||||
impl Store {
|
||||
/// Abre (o prepara) un store en `root` (p. ej. `/store`).
|
||||
pub fn open(root: impl Into<PathBuf>) -> crate::Result<Self> {
|
||||
let root = root.into();
|
||||
std::fs::create_dir_all(&root)?;
|
||||
Ok(Store { root })
|
||||
}
|
||||
|
||||
pub fn root(&self) -> &Path {
|
||||
&self.root
|
||||
}
|
||||
|
||||
/// Ruta del artefacto en el store para un hash + nombre legible.
|
||||
pub fn path_of(&self, h: &ArtifactHash, name: &str) -> PathBuf {
|
||||
self.root.join(h.store_dir_name(name))
|
||||
}
|
||||
|
||||
/// ¿Ya existe el artefacto? (caché del lab; ver `docs/02-build-lab.md` §5).
|
||||
pub fn has(&self, h: &ArtifactHash, name: &str) -> bool {
|
||||
self.path_of(h, name).is_dir()
|
||||
}
|
||||
|
||||
/// Sella el árbol de salida de un build en el store bajo su hash.
|
||||
/// TODO(fase-0): mover `out_dir` a la ruta del store y marcarlo read-only.
|
||||
pub fn seal(
|
||||
&self,
|
||||
_out_dir: &Path,
|
||||
_h: &ArtifactHash,
|
||||
_name: &str,
|
||||
) -> crate::Result<PathBuf> {
|
||||
Err(crate::Error::Store(
|
||||
"sellado en el store pendiente (Fase 0)".into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
//! El manifiesto `.swm` (Software Mutación). Ver `docs/06-swm-format.md`.
|
||||
//!
|
||||
//! Es la unidad de intercambio: receta de transformación sobre fuente pública + ediciones de
|
||||
//! config. NUNCA transporta binarios cocidos (salvo `FileDrop` con hash declarado).
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Swm {
|
||||
pub swm_version: u32,
|
||||
pub base: Base,
|
||||
pub mutations: Vec<Mutation>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub signature: Option<Signature>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Base {
|
||||
pub distro_version: String,
|
||||
#[serde(default)]
|
||||
pub pins: std::collections::BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum Mutation {
|
||||
SourcePatch {
|
||||
repo: String,
|
||||
commit: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
patch: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
patch_url: Option<String>,
|
||||
build: SwmBuild,
|
||||
target_bin: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
expected_hash: Option<String>,
|
||||
},
|
||||
ConfigEdit {
|
||||
file: String,
|
||||
inline_diff: String,
|
||||
},
|
||||
InitRule {
|
||||
action: String,
|
||||
service: String,
|
||||
command: String,
|
||||
},
|
||||
FileDrop {
|
||||
path: String,
|
||||
/// hash BLAKE3 del contenido, declarado para verificación.
|
||||
content_hash: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
content_url: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SwmBuild {
|
||||
#[serde(default = "default_compiler")]
|
||||
pub compiler: String,
|
||||
#[serde(default = "default_target")]
|
||||
pub target: String,
|
||||
#[serde(default = "default_link")]
|
||||
pub link: String,
|
||||
#[serde(default)]
|
||||
pub flags: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Signature {
|
||||
pub by: String,
|
||||
pub alg: String,
|
||||
pub sig: String,
|
||||
}
|
||||
|
||||
fn default_compiler() -> String {
|
||||
"zig-cc".into()
|
||||
}
|
||||
fn default_target() -> String {
|
||||
"x86_64-linux-musl".into()
|
||||
}
|
||||
fn default_link() -> String {
|
||||
"static".into()
|
||||
}
|
||||
|
||||
impl Swm {
|
||||
pub fn from_yaml(s: &str) -> crate::Result<Swm> {
|
||||
serde_yaml::from_str(s).map_err(|e| crate::Error::Serde(e.to_string()))
|
||||
}
|
||||
|
||||
pub fn to_yaml(&self) -> crate::Result<String> {
|
||||
serde_yaml::to_string(self).map_err(|e| crate::Error::Serde(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn roundtrip_yaml() {
|
||||
let yaml = r#"
|
||||
swm_version: 1
|
||||
base:
|
||||
distro_version: "2026-06-06"
|
||||
pins:
|
||||
grep: "a1b2c3d"
|
||||
mutations:
|
||||
- type: config_edit
|
||||
file: "/etc/network.conf"
|
||||
inline_diff: |
|
||||
- DHCP=yes
|
||||
+ IP=192.168.1.100
|
||||
"#;
|
||||
let swm = Swm::from_yaml(yaml).expect("parse");
|
||||
assert_eq!(swm.swm_version, 1);
|
||||
assert_eq!(swm.mutations.len(), 1);
|
||||
let back = swm.to_yaml().expect("serialize");
|
||||
assert!(back.contains("config_edit"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "hammerd"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Daemon de hammer: bus de agente (/run/agent.sock) y diario de mutaciones (fanotify)."
|
||||
|
||||
[[bin]]
|
||||
name = "hammerd"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
hammer-core.workspace = true
|
||||
anyhow.workspace = true
|
||||
clap.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
@@ -0,0 +1,44 @@
|
||||
//! `hammerd` — daemon de hammer. Dos responsabilidades:
|
||||
//! 1. Bus de agente: /run/agent.sock (JSON-líneas, SO_PEERCRED). Ver `docs/07-agent-bus.md`.
|
||||
//! 2. Diario de mutaciones: fanotify sobre /bin,/sbin,/lib,/etc. Ver `docs/05-journal.md`.
|
||||
//!
|
||||
//! Esqueleto de arranque. Las dos subsistemas (Fases 3 y 5) se implementarán por separado;
|
||||
//! aquí queda el binario navegable y el cableado básico de logging/argumentos.
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "hammerd", version, about = "Daemon de hammer: bus de agente + diario.")]
|
||||
struct Args {
|
||||
/// Socket del bus de agente.
|
||||
#[arg(long, default_value = "/run/agent.sock")]
|
||||
agent_sock: String,
|
||||
/// FIFO de control humano del init.
|
||||
#[arg(long, default_value = "/run/init.control")]
|
||||
init_control: String,
|
||||
/// Directorio del diario de mutaciones.
|
||||
#[arg(long, default_value = "/var/lib/hammer/journal")]
|
||||
journal: String,
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| "info".into()),
|
||||
)
|
||||
.init();
|
||||
|
||||
let args = Args::parse();
|
||||
tracing::info!(
|
||||
agent_sock = %args.agent_sock,
|
||||
init_control = %args.init_control,
|
||||
journal = %args.journal,
|
||||
"hammerd: arranque (esqueleto)"
|
||||
);
|
||||
|
||||
// TODO(fase-3): iniciar el watcher fanotify → diario.
|
||||
// TODO(fase-5): servir el bus de agente en agent_sock (JSON-líneas, SO_PEERCRED).
|
||||
tracing::warn!("subsistemas pendientes: diario (Fase 3) y bus de agente (Fase 5)");
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user