Etapa F paquetería #1: hammer pack — receta del corpus → paquete .swm (source_patch)
Cierra la dirección forward que faltaba (SDD 06 §6 la marcaba "para más adelante"): una receta que el sistema ya sabe construir se vuelve un paquete distribuible y reproducible-desde-fuente, inversa de `hammer apply`. - hammer-core: `Swm::from_recipe(recipe, target_bin, patch_text, expected, distro)` (constructor puro: el caller lee los patches). `SwmBuild` gana `phases`+`zig_version` y `SourcePatch` gana `strip_components` (Option/skip ⇒ .swm viejos parsean igual) para reproducir con fidelidad el corpus real (22/34 recetas usan phases, 8 usan zig 0.13). - hammer-build/swm_bridge: la dirección inversa (source_patch→Recipe→build) ahora traslada phases/zig_version/strip_components a la receta efímera ⇒ apply rehace idéntico. - hammer-cli: `hammer pack <recipe> [--target-bin] [--out] [--expected|--build] [--sign]`. Concatena los patches inline; avisa si la receta declara deps (el source_patch aún no las modela = pieza posterior). `export` también enriquece su source_patch. - Validado en host: ripgrep (git+patch+install custom), openssl (tarball+zig 0.13+phases), coreutils (multicall), findutils firmado → swm-verify "trusted". Tests core+bridge verde. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -167,6 +167,36 @@ enum Cmd {
|
||||
#[arg(long)]
|
||||
since: Option<String>,
|
||||
},
|
||||
/// [Etapa F] Empaqueta una receta del corpus (`recipes/foo.toml`) como un `.swm` de un
|
||||
/// único `source_patch`: la dirección **forward** que vuelve una receta que el sistema ya
|
||||
/// sabe construir un paquete distribuible y reproducible-desde-fuente. Inversa de `apply`.
|
||||
Pack {
|
||||
/// Ruta a la receta a empaquetar (p. ej. `recipes/ripgrep.toml`).
|
||||
recipe: PathBuf,
|
||||
/// Ancla de sanity: ruta absoluta del binario que debe existir tras hidratar el
|
||||
/// artefacto. Default `/usr/bin/<name>`. (apply hidrata el artefacto ENTERO; esto sólo
|
||||
/// verifica que algo esperado quedó en su sitio.)
|
||||
#[arg(long)]
|
||||
target_bin: Option<String>,
|
||||
/// Archivo `.swm` de salida. Sin esto, se imprime a stdout.
|
||||
#[arg(long, short)]
|
||||
out: Option<PathBuf>,
|
||||
/// `distro_version` de la base del paquete (la base local del receptor debe coincidir).
|
||||
#[arg(long, default_value = "dev")]
|
||||
distro_version: String,
|
||||
/// `expected_hash` declarado (`b3:…`) — el ancla de "verificar, no confiar". Si se omite
|
||||
/// y no se pasa `--build`, el paquete sale sin ancla (el receptor reproduce igual, pero
|
||||
/// no hay hash autor contra el que comparar).
|
||||
#[arg(long)]
|
||||
expected: Option<String>,
|
||||
/// Construir la receta AHORA en el lab para sellar el `expected_hash` real (necesita
|
||||
/// store + sandbox). Mutuamente informativo con `--expected` (este último gana si ambos).
|
||||
#[arg(long)]
|
||||
build: bool,
|
||||
/// Firmar el `.swm` resultante con esta clave privada Ed25519 (como `swm-sign`).
|
||||
#[arg(long)]
|
||||
sign: Option<PathBuf>,
|
||||
},
|
||||
/// [Fase 6] Ejecuta el bucle agéntico: traduce una intención NL a un .swm vía catálogo
|
||||
/// mock y la aplica a un overlay (o prefix) sin promover al FHS. Imprime el Proposal.
|
||||
Ai {
|
||||
@@ -647,6 +677,26 @@ fn main() -> anyhow::Result<()> {
|
||||
Cmd::Export { base_ref, journal, since } => {
|
||||
run_export(base_ref.as_deref(), &journal, since.as_deref(), &cli.store)?;
|
||||
}
|
||||
Cmd::Pack {
|
||||
recipe,
|
||||
target_bin,
|
||||
out,
|
||||
distro_version,
|
||||
expected,
|
||||
build,
|
||||
sign,
|
||||
} => {
|
||||
run_pack(
|
||||
&recipe,
|
||||
target_bin.as_deref(),
|
||||
out.as_deref(),
|
||||
&distro_version,
|
||||
expected.as_deref(),
|
||||
build,
|
||||
sign.as_deref(),
|
||||
&cli.store,
|
||||
)?;
|
||||
}
|
||||
Cmd::Ai {
|
||||
intent,
|
||||
catalog,
|
||||
@@ -1038,6 +1088,114 @@ fn run_keygen(name: &str, out: Option<&std::path::Path>) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Empaqueta una receta como un `.swm` de un único `source_patch` (Etapa F). Lee la receta,
|
||||
/// materializa el texto de sus patches (resueltos contra `base_dir`, concatenados), opcionalmente
|
||||
/// construye para sellar el `expected_hash` real, firma si se pide, y emite el manifiesto.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn run_pack(
|
||||
recipe_path: &std::path::Path,
|
||||
target_bin: Option<&str>,
|
||||
out: Option<&std::path::Path>,
|
||||
distro_version: &str,
|
||||
expected: Option<&str>,
|
||||
do_build: bool,
|
||||
sign: Option<&std::path::Path>,
|
||||
store_path: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
let recipe = hammer_core::Recipe::load_from_path(recipe_path)
|
||||
.map_err(|e| anyhow::anyhow!("no pude cargar la receta {}: {e}", recipe_path.display()))?;
|
||||
|
||||
// El `source_patch` aún NO modela `deps` (resolución de dependencias entre paquetes = pieza
|
||||
// posterior de la paquetería). Una receta con build-deps se empaqueta igual, pero el receptor
|
||||
// necesitará esas deps ya presentes en su store para reproducir. Avisamos, no perdemos en silencio.
|
||||
if !recipe.deps.build.is_empty() || !recipe.deps.runtime.is_empty() {
|
||||
eprintln!(
|
||||
"⚠ la receta declara deps (build={:?} runtime={:?}) que el source_patch no transporta; \
|
||||
el receptor debe tenerlas en su store para reproducir (deps entre paquetes: pieza futura)",
|
||||
recipe.deps.build, recipe.deps.runtime
|
||||
);
|
||||
}
|
||||
|
||||
// Texto de los patches: cada uno se resuelve relativo al directorio de la receta (igual que
|
||||
// el lab) y se concatena. El `source_patch` del .swm modela un único `patch` inline; un diff
|
||||
// unificado multi-fichero concatenado se aplica hunk-a-hunk igual (git apply / patch -p1).
|
||||
let patch_text = if recipe.source.patches.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let mut buf = String::new();
|
||||
for p in &recipe.source.patches {
|
||||
let path = std::path::Path::new(p);
|
||||
let abs = if path.is_absolute() {
|
||||
path.to_path_buf()
|
||||
} else {
|
||||
recipe.base_dir.join(path)
|
||||
};
|
||||
let txt = std::fs::read_to_string(&abs)
|
||||
.map_err(|e| anyhow::anyhow!("leyendo patch {}: {e}", abs.display()))?;
|
||||
if !buf.is_empty() && !buf.ends_with('\n') {
|
||||
buf.push('\n');
|
||||
}
|
||||
buf.push_str(&txt);
|
||||
}
|
||||
Some(buf)
|
||||
};
|
||||
|
||||
let target_bin = target_bin
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| format!("/usr/bin/{}", recipe.name));
|
||||
|
||||
// expected_hash: `--expected` manda; si no, `--build` lo sella construyendo de verdad en el
|
||||
// lab; si ninguno, el paquete sale sin ancla de verificación (válido, sólo más débil).
|
||||
let expected_hash: Option<String> = if let Some(h) = expected {
|
||||
Some(h.to_string())
|
||||
} else if do_build {
|
||||
let store = hammer_core::Store::open(store_path)?;
|
||||
let cfg = hammer_build::BuildConfig::from_env_or_defaults(store.root());
|
||||
let hash = hammer_build::build(&recipe, &cfg, &store)
|
||||
.map_err(|e| anyhow::anyhow!("construyendo la receta para sellar expected_hash: {e}"))?;
|
||||
eprintln!("construida: {hash}");
|
||||
Some(hash.as_str().to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut swm = hammer_core::Swm::from_recipe(
|
||||
&recipe,
|
||||
target_bin,
|
||||
patch_text,
|
||||
expected_hash,
|
||||
distro_version,
|
||||
)?;
|
||||
|
||||
if let Some(key) = sign {
|
||||
let priv_b64 = std::fs::read_to_string(key)
|
||||
.map_err(|e| anyhow::anyhow!("no pude leer la clave {}: {e}", key.display()))?;
|
||||
let kp = hammer_core::KeyPair::from_private_b64(&priv_b64)?;
|
||||
let by = key
|
||||
.file_name()
|
||||
.map(|f| f.to_string_lossy().trim_end_matches(".ed25519").to_string())
|
||||
.unwrap_or_else(|| "anon".into());
|
||||
swm.signature = Some(kp.sign(&swm, &by));
|
||||
eprintln!("firmado por {by}");
|
||||
}
|
||||
|
||||
let yaml = swm.to_yaml()?;
|
||||
match out {
|
||||
Some(dest) => {
|
||||
std::fs::write(dest, &yaml)?;
|
||||
eprintln!(
|
||||
"empaquetada {} → {} ({} mutación source_patch{})",
|
||||
recipe.name,
|
||||
dest.display(),
|
||||
swm.mutations.len(),
|
||||
if swm.signature.is_some() { ", firmado" } else { "" },
|
||||
);
|
||||
}
|
||||
None => print!("{yaml}"),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_swm_sign(
|
||||
file: &str,
|
||||
key: &std::path::Path,
|
||||
@@ -1409,6 +1567,8 @@ fn build_export_mutations(
|
||||
commit,
|
||||
tarball,
|
||||
sha256,
|
||||
strip_components: (recipe.source.strip_components != 1)
|
||||
.then_some(recipe.source.strip_components),
|
||||
patch: patch_inline,
|
||||
patch_url: None,
|
||||
build: hammer_core::SwmBuild {
|
||||
@@ -1416,6 +1576,8 @@ fn build_export_mutations(
|
||||
target: recipe.build.target.clone(),
|
||||
link: recipe.build.link.as_str().to_string(),
|
||||
flags: recipe.build.flags.clone(),
|
||||
phases: recipe.build.phases.clone(),
|
||||
zig_version: recipe.build.zig_version.clone(),
|
||||
},
|
||||
target_bin,
|
||||
expected_hash: Some(art_hash),
|
||||
|
||||
Reference in New Issue
Block a user