feat(swm): source_patch modela tarballs además de git (Fase 4)
Mutation::SourcePatch y RecipeInline ganan campos repo/commit XOR tarball/sha256 (Option, serde-default ⇒ compat con .swm git existentes), resueltos por swm::swm_source_kind con la misma regla que recipe::Source::kind. swm_bridge sintetiza la receta según el modo; hammer export reconstruye fuentes tarball como source_patch en vez de caer a file_drop (provenance fina recuperada). Prompt del traductor y roadmap actualizados. Tests nuevos para tarball + XOR. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
7c099afe4f
commit
0f42c38106
@@ -154,6 +154,8 @@ impl<T: IntentTranslator> Orchestrator<T> {
|
|||||||
Mutation::SourcePatch {
|
Mutation::SourcePatch {
|
||||||
repo,
|
repo,
|
||||||
commit,
|
commit,
|
||||||
|
tarball,
|
||||||
|
sha256,
|
||||||
patch,
|
patch,
|
||||||
patch_url: _,
|
patch_url: _,
|
||||||
build,
|
build,
|
||||||
@@ -176,6 +178,8 @@ impl<T: IntentTranslator> Orchestrator<T> {
|
|||||||
name: name.clone(),
|
name: name.clone(),
|
||||||
repo: repo.clone(),
|
repo: repo.clone(),
|
||||||
commit: commit.clone(),
|
commit: commit.clone(),
|
||||||
|
tarball: tarball.clone(),
|
||||||
|
sha256: sha256.clone(),
|
||||||
patch: patch.clone(),
|
patch: patch.clone(),
|
||||||
compiler: build.compiler.clone(),
|
compiler: build.compiler.clone(),
|
||||||
target: build.target.clone(),
|
target: build.target.clone(),
|
||||||
@@ -567,8 +571,10 @@ mod tests {
|
|||||||
swm_version: 1,
|
swm_version: 1,
|
||||||
base: Base { distro_version: "2026-06-06".into(), pins: BTreeMap::new() },
|
base: Base { distro_version: "2026-06-06".into(), pins: BTreeMap::new() },
|
||||||
mutations: vec![Mutation::SourcePatch {
|
mutations: vec![Mutation::SourcePatch {
|
||||||
repo: "git://x".into(),
|
repo: Some("git://x".into()),
|
||||||
commit: "abc".into(),
|
commit: Some("abc".into()),
|
||||||
|
tarball: None,
|
||||||
|
sha256: None,
|
||||||
patch: None,
|
patch: None,
|
||||||
patch_url: None,
|
patch_url: None,
|
||||||
build: hammer_core::swm::SwmBuild {
|
build: hammer_core::swm::SwmBuild {
|
||||||
|
|||||||
@@ -146,6 +146,8 @@ const SYSTEM_PROMPT: &str = r#"Eres un traductor que convierte intenciones human
|
|||||||
|
|
||||||
3. source_patch — compila desde fuente y la inyecta.
|
3. source_patch — compila desde fuente y la inyecta.
|
||||||
{"type":"source_patch","repo":"git://...","commit":"<sha>","build":{"compiler":"zig-cc","target":"x86_64-linux-musl","link":"static","flags":[]},"target_bin":"/usr/bin/foo"}
|
{"type":"source_patch","repo":"git://...","commit":"<sha>","build":{"compiler":"zig-cc","target":"x86_64-linux-musl","link":"static","flags":[]},"target_bin":"/usr/bin/foo"}
|
||||||
|
- Origen git (repo+commit) O tarball (tarball+sha256), exactamente uno; p. ej.
|
||||||
|
{"type":"source_patch","tarball":"https://ftp.gnu.org/gnu/grep/grep-3.11.tar.gz","sha256":"<hex>","build":{...},"target_bin":"/usr/bin/grep"}
|
||||||
- `target_bin` ruta absoluta del binario producido.
|
- `target_bin` ruta absoluta del binario producido.
|
||||||
|
|
||||||
4. init_rule — registra un servicio.
|
4. init_rule — registra un servicio.
|
||||||
|
|||||||
@@ -84,7 +84,10 @@ fn stub_serve(stream: UnixStream, rx_async: std::sync::mpsc::Receiver<Event>) {
|
|||||||
let response = match cmd {
|
let response = match cmd {
|
||||||
Command::Compile { recipe } => Event::BuildReady {
|
Command::Compile { recipe } => Event::BuildReady {
|
||||||
recipe: recipe.name.clone(),
|
recipe: recipe.name.clone(),
|
||||||
artifact: format!("b3:stub-{}", recipe.commit),
|
artifact: format!(
|
||||||
|
"b3:stub-{}",
|
||||||
|
recipe.commit.as_deref().or(recipe.sha256.as_deref()).unwrap_or("nosrc")
|
||||||
|
),
|
||||||
},
|
},
|
||||||
Command::Query { what, .. } => Event::QueryResult {
|
Command::Query { what, .. } => Event::QueryResult {
|
||||||
what,
|
what,
|
||||||
@@ -133,8 +136,10 @@ fn client_handshake_compile_and_async_modified() {
|
|||||||
|
|
||||||
let recipe = RecipeInline {
|
let recipe = RecipeInline {
|
||||||
name: "grep".into(),
|
name: "grep".into(),
|
||||||
repo: "git://example/grep.git".into(),
|
repo: Some("git://example/grep.git".into()),
|
||||||
commit: "abc123".into(),
|
commit: Some("abc123".into()),
|
||||||
|
tarball: None,
|
||||||
|
sha256: None,
|
||||||
patch: None,
|
patch: None,
|
||||||
compiler: "zig-cc".into(),
|
compiler: "zig-cc".into(),
|
||||||
target: "x86_64-linux-musl".into(),
|
target: "x86_64-linux-musl".into(),
|
||||||
|
|||||||
@@ -27,33 +27,54 @@ pub fn build_source_patch(
|
|||||||
store: &Store,
|
store: &Store,
|
||||||
scratch_root: Option<&Path>,
|
scratch_root: Option<&Path>,
|
||||||
) -> hammer_core::Result<ArtifactHash> {
|
) -> hammer_core::Result<ArtifactHash> {
|
||||||
let (repo, commit, patch, patch_url, build_cfg, target_bin, expected) = match mutation {
|
let (repo, commit, tarball, sha256, patch, patch_url, build_cfg, target_bin, expected) =
|
||||||
Mutation::SourcePatch {
|
match mutation {
|
||||||
repo,
|
Mutation::SourcePatch {
|
||||||
commit,
|
repo,
|
||||||
patch,
|
commit,
|
||||||
patch_url,
|
tarball,
|
||||||
build,
|
sha256,
|
||||||
target_bin,
|
patch,
|
||||||
expected_hash,
|
patch_url,
|
||||||
} => (repo, commit, patch, patch_url, build, target_bin, expected_hash),
|
build,
|
||||||
_ => {
|
target_bin,
|
||||||
return Err(hammer_core::Error::Recipe(
|
expected_hash,
|
||||||
"build_source_patch: la mutación no es 'source_patch'".into(),
|
} => (
|
||||||
));
|
repo, commit, tarball, sha256, patch, patch_url, build, target_bin, expected_hash,
|
||||||
}
|
),
|
||||||
};
|
_ => {
|
||||||
|
return Err(hammer_core::Error::Recipe(
|
||||||
|
"build_source_patch: la mutación no es 'source_patch'".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// git xor tarball — misma regla que el schema (`swm_source_kind`).
|
||||||
|
let source_kind = hammer_core::swm::swm_source_kind(
|
||||||
|
repo.as_deref(),
|
||||||
|
commit.as_deref(),
|
||||||
|
tarball.as_deref(),
|
||||||
|
sha256.as_deref(),
|
||||||
|
)
|
||||||
|
.map_err(hammer_core::Error::Recipe)?;
|
||||||
|
|
||||||
let scratch = scratch_root.unwrap_or(&cfg.work_root).to_path_buf();
|
let scratch = scratch_root.unwrap_or(&cfg.work_root).to_path_buf();
|
||||||
let recipe_dir = scratch.join("swm-recipes");
|
let recipe_dir = scratch.join("swm-recipes");
|
||||||
std::fs::create_dir_all(&recipe_dir)?;
|
std::fs::create_dir_all(&recipe_dir)?;
|
||||||
|
|
||||||
// Materializamos el patch a disco bajo un nombre derivado del commit (estable: si el .swm
|
// Clave estable del origen para nombrar artefactos a disco: el commit (git) o el
|
||||||
// se reaplica, reusamos el mismo archivo y el hash de la receta no depende de
|
// sha256 (tarball). Idéntica reaplicación ⇒ mismo nombre ⇒ hash de receta determinista.
|
||||||
|
let source_key: &str = match &source_kind {
|
||||||
|
hammer_core::SourceKind::Git { commit, .. } => commit,
|
||||||
|
hammer_core::SourceKind::Tarball { sha256, .. } => sha256,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Materializamos el patch a disco bajo un nombre derivado de la clave de origen (estable:
|
||||||
|
// si el .swm se reaplica, reusamos el mismo archivo y el hash de la receta no depende de
|
||||||
// aleatoriedad). Origen: inline (`patch`) o remoto (`patch_url`); el schema garantiza que
|
// aleatoriedad). Origen: inline (`patch`) o remoto (`patch_url`); el schema garantiza que
|
||||||
// no vengan ambos.
|
// no vengan ambos.
|
||||||
let mut patches: Vec<String> = Vec::new();
|
let mut patches: Vec<String> = Vec::new();
|
||||||
let patch_path = recipe_dir.join(format!("{commit}.patch"));
|
let patch_path = recipe_dir.join(format!("{source_key}.patch"));
|
||||||
if let Some(text) = patch {
|
if let Some(text) = patch {
|
||||||
std::fs::write(&patch_path, text.as_bytes())?;
|
std::fs::write(&patch_path, text.as_bytes())?;
|
||||||
patches.push(patch_path.to_string_lossy().into_owned());
|
patches.push(patch_path.to_string_lossy().into_owned());
|
||||||
@@ -64,14 +85,7 @@ pub fn build_source_patch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let name = derive_name(target_bin);
|
let name = derive_name(target_bin);
|
||||||
let recipe = synthesize_recipe(
|
let recipe = synthesize_recipe(&name, &source_kind, build_cfg, patches, &recipe_dir)?;
|
||||||
&name,
|
|
||||||
commit,
|
|
||||||
repo,
|
|
||||||
build_cfg,
|
|
||||||
patches,
|
|
||||||
&recipe_dir,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let hash = build(&recipe, cfg, store)?;
|
let hash = build(&recipe, cfg, store)?;
|
||||||
if let Some(want) = expected {
|
if let Some(want) = expected {
|
||||||
@@ -102,24 +116,33 @@ fn derive_name(target_bin: &str) -> String {
|
|||||||
|
|
||||||
fn synthesize_recipe(
|
fn synthesize_recipe(
|
||||||
name: &str,
|
name: &str,
|
||||||
commit: &str,
|
source: &hammer_core::SourceKind<'_>,
|
||||||
repo: &str,
|
|
||||||
build_cfg: &SwmBuild,
|
build_cfg: &SwmBuild,
|
||||||
patches: Vec<String>,
|
patches: Vec<String>,
|
||||||
base_dir: &Path,
|
base_dir: &Path,
|
||||||
) -> hammer_core::Result<Recipe> {
|
) -> hammer_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
|
||||||
|
// `Source::kind` ya garantiza git xor tarball; acá sólo emitimos el TOML correspondiente.
|
||||||
|
let (version_key, source_block) = match source {
|
||||||
|
hammer_core::SourceKind::Git { repo, commit } => (
|
||||||
|
*commit,
|
||||||
|
format!("repo = \"{repo}\"\ncommit = \"{commit}\"\n"),
|
||||||
|
),
|
||||||
|
hammer_core::SourceKind::Tarball { url, sha256 } => (
|
||||||
|
*sha256,
|
||||||
|
format!("tarball = \"{url}\"\nsha256 = \"{sha256}\"\n"),
|
||||||
|
),
|
||||||
|
};
|
||||||
// Construimos la receta vía TOML para reusar las defaults y el validador de `Source`.
|
// Construimos la receta vía TOML para reusar las defaults y el validador de `Source`.
|
||||||
let toml_text = format!(
|
let toml_text = format!(
|
||||||
r#"
|
r#"
|
||||||
name = "{name}"
|
name = "{name}"
|
||||||
version = "swm-{commit_short}"
|
version = "swm-{version_short}"
|
||||||
|
|
||||||
[source]
|
[source]
|
||||||
repo = "{repo}"
|
{source_block}
|
||||||
commit = "{commit}"
|
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
compiler = "{compiler}"
|
compiler = "{compiler}"
|
||||||
target = "{target}"
|
target = "{target}"
|
||||||
@@ -127,9 +150,8 @@ link = "{link}"
|
|||||||
flags = []
|
flags = []
|
||||||
"#,
|
"#,
|
||||||
name = name,
|
name = name,
|
||||||
commit_short = &commit[..commit.len().min(12)],
|
version_short = &version_key[..version_key.len().min(12)],
|
||||||
repo = repo,
|
source_block = source_block,
|
||||||
commit = commit,
|
|
||||||
compiler = compiler.as_str(),
|
compiler = compiler.as_str(),
|
||||||
target = build_cfg.target,
|
target = build_cfg.target,
|
||||||
link = link.as_str(),
|
link = link.as_str(),
|
||||||
@@ -199,8 +221,10 @@ mod tests {
|
|||||||
let d = tempfile::tempdir().unwrap();
|
let d = tempfile::tempdir().unwrap();
|
||||||
let r = synthesize_recipe(
|
let r = synthesize_recipe(
|
||||||
"grep",
|
"grep",
|
||||||
"a1b2c3d4e5f6a7b8c9",
|
&hammer_core::SourceKind::Git {
|
||||||
"git://example/grep.git",
|
repo: "git://example/grep.git",
|
||||||
|
commit: "a1b2c3d4e5f6a7b8c9",
|
||||||
|
},
|
||||||
&fake_swm_build(),
|
&fake_swm_build(),
|
||||||
vec![],
|
vec![],
|
||||||
d.path(),
|
d.path(),
|
||||||
@@ -218,6 +242,31 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn synthesize_recipe_tarball() {
|
||||||
|
let d = tempfile::tempdir().unwrap();
|
||||||
|
let r = synthesize_recipe(
|
||||||
|
"grep",
|
||||||
|
&hammer_core::SourceKind::Tarball {
|
||||||
|
url: "https://ftp.gnu.org/gnu/grep/grep-3.11.tar.gz",
|
||||||
|
sha256: "abcdef0123456789",
|
||||||
|
},
|
||||||
|
&fake_swm_build(),
|
||||||
|
vec![],
|
||||||
|
d.path(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(r.name, "grep");
|
||||||
|
assert!(r.version.starts_with("swm-abcdef012345"));
|
||||||
|
match r.source.kind().unwrap() {
|
||||||
|
hammer_core::SourceKind::Tarball { url, sha256 } => {
|
||||||
|
assert_eq!(url, "https://ftp.gnu.org/gnu/grep/grep-3.11.tar.gz");
|
||||||
|
assert_eq!(sha256, "abcdef0123456789");
|
||||||
|
}
|
||||||
|
_ => panic!("modo tarball esperado"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_compiler_and_link() {
|
fn parse_compiler_and_link() {
|
||||||
assert_eq!(parse_compiler("zig-cc").unwrap(), Compiler::ZigCc);
|
assert_eq!(parse_compiler("zig-cc").unwrap(), Compiler::ZigCc);
|
||||||
@@ -249,8 +298,10 @@ mod tests {
|
|||||||
std::fs::write(&patch_src, b"--- a\n+++ b\n").unwrap();
|
std::fs::write(&patch_src, b"--- a\n+++ b\n").unwrap();
|
||||||
let commit = "deadbeefcafe";
|
let commit = "deadbeefcafe";
|
||||||
let m = Mutation::SourcePatch {
|
let m = Mutation::SourcePatch {
|
||||||
repo: "git://x".into(),
|
repo: Some("git://x".into()),
|
||||||
commit: commit.into(),
|
commit: Some(commit.into()),
|
||||||
|
tarball: None,
|
||||||
|
sha256: None,
|
||||||
patch: None,
|
patch: None,
|
||||||
patch_url: Some(format!("file://{}", patch_src.display())),
|
patch_url: Some(format!("file://{}", patch_src.display())),
|
||||||
build: fake_swm_build(),
|
build: fake_swm_build(),
|
||||||
|
|||||||
@@ -1054,25 +1054,14 @@ fn build_export_mutations(
|
|||||||
.map(|p| p.display().to_string())
|
.map(|p| p.display().to_string())
|
||||||
.unwrap_or_else(|| "/".into());
|
.unwrap_or_else(|| "/".into());
|
||||||
|
|
||||||
// Convertimos la Recipe a los campos que SourcePatch necesita.
|
// Convertimos la Recipe a los campos de origen que SourcePatch necesita
|
||||||
let (repo, commit) = match recipe.source.kind() {
|
// (git xor tarball — ambos se modelan ya, sin caer a file_drop).
|
||||||
|
let (repo, commit, tarball, sha256) = match recipe.source.kind() {
|
||||||
Ok(hammer_core::SourceKind::Git { repo, commit }) => {
|
Ok(hammer_core::SourceKind::Git { repo, commit }) => {
|
||||||
(repo.to_string(), commit.to_string())
|
(Some(repo.to_string()), Some(commit.to_string()), None, None)
|
||||||
}
|
}
|
||||||
Ok(hammer_core::SourceKind::Tarball { url, sha256 }) => {
|
Ok(hammer_core::SourceKind::Tarball { url, sha256 }) => {
|
||||||
// SourcePatch sólo modela el modo git. Para tarballs caemos al
|
(None, None, Some(url.to_string()), Some(sha256.to_string()))
|
||||||
// fallback file_drop por evento — no perdemos completeness, sólo
|
|
||||||
// provenance fina. El humano lo ve por stderr.
|
|
||||||
eprintln!(
|
|
||||||
"warning: artefacto {art_hash} proviene de tarball ({url} sha256={sha256}); \
|
|
||||||
`source_patch` aún no modela tarballs, hago file_drop"
|
|
||||||
);
|
|
||||||
for p in &paths {
|
|
||||||
if let Some(ev) = last_by_path.get(p) {
|
|
||||||
fallback.push((p.clone(), *ev));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
stats.warnings += 1;
|
stats.warnings += 1;
|
||||||
@@ -1105,6 +1094,8 @@ fn build_export_mutations(
|
|||||||
mutations.push(hammer_core::Mutation::SourcePatch {
|
mutations.push(hammer_core::Mutation::SourcePatch {
|
||||||
repo,
|
repo,
|
||||||
commit,
|
commit,
|
||||||
|
tarball,
|
||||||
|
sha256,
|
||||||
patch: patch_inline,
|
patch: patch_inline,
|
||||||
patch_url: None,
|
patch_url: None,
|
||||||
build: hammer_core::SwmBuild {
|
build: hammer_core::SwmBuild {
|
||||||
@@ -1613,8 +1604,8 @@ flags = ["--enable-foo"]
|
|||||||
expected_hash,
|
expected_hash,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
assert_eq!(repo, "git://example/grep.git");
|
assert_eq!(repo.as_deref(), Some("git://example/grep.git"));
|
||||||
assert_eq!(commit, "deadbeef");
|
assert_eq!(commit.as_deref(), Some("deadbeef"));
|
||||||
assert_eq!(build.compiler, "zig-cc");
|
assert_eq!(build.compiler, "zig-cc");
|
||||||
assert_eq!(build.link, "static");
|
assert_eq!(build.link, "static");
|
||||||
assert_eq!(build.flags, vec!["--enable-foo".to_string()]);
|
assert_eq!(build.flags, vec!["--enable-foo".to_string()]);
|
||||||
|
|||||||
@@ -63,8 +63,17 @@ pub enum Command {
|
|||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct RecipeInline {
|
pub struct RecipeInline {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub repo: String,
|
// Origen git (modo histórico) **o** tarball — exactamente uno, igual que
|
||||||
pub commit: String,
|
// `swm::Mutation::SourcePatch`. `Option` con `serde(default)` para compat con clientes
|
||||||
|
// que sólo mandan `repo`+`commit`.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub repo: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub commit: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub tarball: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub sha256: Option<String>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub patch: Option<String>,
|
pub patch: Option<String>,
|
||||||
#[serde(default = "default_compiler")]
|
#[serde(default = "default_compiler")]
|
||||||
@@ -208,8 +217,10 @@ mod tests {
|
|||||||
let c = Command::Compile {
|
let c = Command::Compile {
|
||||||
recipe: RecipeInline {
|
recipe: RecipeInline {
|
||||||
name: "grep".into(),
|
name: "grep".into(),
|
||||||
repo: "git://x/grep.git".into(),
|
repo: Some("git://x/grep.git".into()),
|
||||||
commit: "abc".into(),
|
commit: Some("abc".into()),
|
||||||
|
tarball: None,
|
||||||
|
sha256: None,
|
||||||
patch: None,
|
patch: None,
|
||||||
compiler: "zig-cc".into(),
|
compiler: "zig-cc".into(),
|
||||||
target: "x86_64-linux-musl".into(),
|
target: "x86_64-linux-musl".into(),
|
||||||
|
|||||||
+133
-10
@@ -70,8 +70,19 @@ pub struct PinDiff {
|
|||||||
#[serde(tag = "type", rename_all = "snake_case")]
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
pub enum Mutation {
|
pub enum Mutation {
|
||||||
SourcePatch {
|
SourcePatch {
|
||||||
repo: String,
|
// Origen git (modo histórico) **o** tarball — exactamente uno, igual que
|
||||||
commit: String,
|
// [`crate::recipe::Source`]. Antes sólo se modelaba git y los tarballs caían a
|
||||||
|
// `file_drop`; ahora `hammer export` los reconstruye como source_patch. Los campos
|
||||||
|
// son `Option` (con `serde(default)`) para que los `.swm` git existentes —que sólo
|
||||||
|
// traen `repo`+`commit`— sigan parseando sin cambios.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
repo: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
commit: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
tarball: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
sha256: Option<String>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
patch: Option<String>,
|
patch: Option<String>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
@@ -200,18 +211,58 @@ impl Swm {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resuelve el modo de origen de un `source_patch` (git **xor** tarball) desde sus campos
|
||||||
|
/// opcionales, con la misma regla que [`crate::recipe::Source::kind`]. Centraliza la
|
||||||
|
/// validación para que `verify_schema`, el lab y el export compartan una sola fuente de verdad.
|
||||||
|
pub fn swm_source_kind<'a>(
|
||||||
|
repo: Option<&'a str>,
|
||||||
|
commit: Option<&'a str>,
|
||||||
|
tarball: Option<&'a str>,
|
||||||
|
sha256: Option<&'a str>,
|
||||||
|
) -> Result<crate::recipe::SourceKind<'a>, String> {
|
||||||
|
use crate::recipe::SourceKind;
|
||||||
|
match (repo, commit, tarball, sha256) {
|
||||||
|
(Some(repo), Some(commit), None, None) => Ok(SourceKind::Git { repo, commit }),
|
||||||
|
(None, None, Some(url), Some(sha256)) => Ok(SourceKind::Tarball { url, sha256 }),
|
||||||
|
(Some(_), Some(_), Some(_), _) | (Some(_), Some(_), _, Some(_)) => {
|
||||||
|
Err("source_patch: usa repo+commit O tarball+sha256, no ambos".into())
|
||||||
|
}
|
||||||
|
_ => Err("source_patch: faltan campos; necesito (repo+commit) o (tarball+sha256)".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Mutation {
|
impl Mutation {
|
||||||
/// Sanity por mutación. Cada `type` tiene precondiciones distintas; la validación cruzada
|
/// Sanity por mutación. Cada `type` tiene precondiciones distintas; la validación cruzada
|
||||||
/// (p. ej. "el `target_bin` apunta a un dir gestionado") la decide quien aplica, no el
|
/// (p. ej. "el `target_bin` apunta a un dir gestionado") la decide quien aplica, no el
|
||||||
/// schema.
|
/// schema.
|
||||||
pub fn verify_schema(&self) -> Result<(), String> {
|
pub fn verify_schema(&self) -> Result<(), String> {
|
||||||
|
use crate::recipe::SourceKind;
|
||||||
match self {
|
match self {
|
||||||
Mutation::SourcePatch { repo, commit, patch, patch_url, target_bin, .. } => {
|
Mutation::SourcePatch {
|
||||||
if repo.is_empty() {
|
repo, commit, tarball, sha256, patch, patch_url, target_bin, ..
|
||||||
return Err("source_patch: 'repo' vacío".into());
|
} => {
|
||||||
}
|
match swm_source_kind(
|
||||||
if commit.is_empty() {
|
repo.as_deref(),
|
||||||
return Err("source_patch: 'commit' vacío".into());
|
commit.as_deref(),
|
||||||
|
tarball.as_deref(),
|
||||||
|
sha256.as_deref(),
|
||||||
|
)? {
|
||||||
|
SourceKind::Git { repo, commit } => {
|
||||||
|
if repo.is_empty() {
|
||||||
|
return Err("source_patch: 'repo' vacío".into());
|
||||||
|
}
|
||||||
|
if commit.is_empty() {
|
||||||
|
return Err("source_patch: 'commit' vacío".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SourceKind::Tarball { url, sha256 } => {
|
||||||
|
if url.is_empty() {
|
||||||
|
return Err("source_patch: 'tarball' vacío".into());
|
||||||
|
}
|
||||||
|
if sha256.is_empty() {
|
||||||
|
return Err("source_patch: 'sha256' vacío".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if patch.is_some() && patch_url.is_some() {
|
if patch.is_some() && patch_url.is_some() {
|
||||||
return Err(
|
return Err(
|
||||||
@@ -422,8 +473,10 @@ mutations:
|
|||||||
swm_version: 1,
|
swm_version: 1,
|
||||||
base: base_v("x", &[]),
|
base: base_v("x", &[]),
|
||||||
mutations: vec![Mutation::SourcePatch {
|
mutations: vec![Mutation::SourcePatch {
|
||||||
repo: "git://x".into(),
|
repo: Some("git://x".into()),
|
||||||
commit: "abc".into(),
|
commit: Some("abc".into()),
|
||||||
|
tarball: None,
|
||||||
|
sha256: None,
|
||||||
patch: None,
|
patch: None,
|
||||||
patch_url: None,
|
patch_url: None,
|
||||||
build: SwmBuild {
|
build: SwmBuild {
|
||||||
@@ -439,4 +492,74 @@ mutations:
|
|||||||
};
|
};
|
||||||
swm.verify_schema().unwrap();
|
swm.verify_schema().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verify_schema_source_patch_tarball() {
|
||||||
|
let swm = Swm {
|
||||||
|
swm_version: 1,
|
||||||
|
base: base_v("x", &[]),
|
||||||
|
mutations: vec![Mutation::SourcePatch {
|
||||||
|
repo: None,
|
||||||
|
commit: None,
|
||||||
|
tarball: Some("https://ftp.gnu.org/gnu/grep/grep-3.11.tar.gz".into()),
|
||||||
|
sha256: Some("deadbeef".into()),
|
||||||
|
patch: None,
|
||||||
|
patch_url: None,
|
||||||
|
build: SwmBuild {
|
||||||
|
compiler: "zig-cc".into(),
|
||||||
|
target: "x86_64-linux-musl".into(),
|
||||||
|
link: "static".into(),
|
||||||
|
flags: vec![],
|
||||||
|
},
|
||||||
|
target_bin: "/bin/grep".into(),
|
||||||
|
expected_hash: None,
|
||||||
|
}],
|
||||||
|
signature: None,
|
||||||
|
};
|
||||||
|
swm.verify_schema().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verify_schema_source_patch_rechaza_git_y_tarball_mezclados() {
|
||||||
|
let m = Mutation::SourcePatch {
|
||||||
|
repo: Some("git://x".into()),
|
||||||
|
commit: Some("abc".into()),
|
||||||
|
tarball: Some("https://x/a.tar.gz".into()),
|
||||||
|
sha256: Some("deadbeef".into()),
|
||||||
|
patch: None,
|
||||||
|
patch_url: None,
|
||||||
|
build: SwmBuild {
|
||||||
|
compiler: "zig-cc".into(),
|
||||||
|
target: "x86_64-linux-musl".into(),
|
||||||
|
link: "static".into(),
|
||||||
|
flags: vec![],
|
||||||
|
},
|
||||||
|
target_bin: "/bin/x".into(),
|
||||||
|
expected_hash: None,
|
||||||
|
};
|
||||||
|
let err = m.verify_schema().unwrap_err();
|
||||||
|
assert!(err.contains("no ambos"), "mensaje inesperado: {err}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verify_schema_source_patch_rechaza_sin_origen() {
|
||||||
|
let m = Mutation::SourcePatch {
|
||||||
|
repo: None,
|
||||||
|
commit: None,
|
||||||
|
tarball: None,
|
||||||
|
sha256: None,
|
||||||
|
patch: None,
|
||||||
|
patch_url: None,
|
||||||
|
build: SwmBuild {
|
||||||
|
compiler: "zig-cc".into(),
|
||||||
|
target: "x86_64-linux-musl".into(),
|
||||||
|
link: "static".into(),
|
||||||
|
flags: vec![],
|
||||||
|
},
|
||||||
|
target_bin: "/bin/x".into(),
|
||||||
|
expected_hash: None,
|
||||||
|
};
|
||||||
|
let err = m.verify_schema().unwrap_err();
|
||||||
|
assert!(err.contains("faltan campos"), "mensaje inesperado: {err}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -362,6 +362,8 @@ fn run_compile(recipe: RecipeInline, store_root: PathBuf, tx: Sender<Event>) {
|
|||||||
let mutation = hammer_core::swm::Mutation::SourcePatch {
|
let mutation = hammer_core::swm::Mutation::SourcePatch {
|
||||||
repo: recipe.repo,
|
repo: recipe.repo,
|
||||||
commit: recipe.commit,
|
commit: recipe.commit,
|
||||||
|
tarball: recipe.tarball,
|
||||||
|
sha256: recipe.sha256,
|
||||||
patch: recipe.patch,
|
patch: recipe.patch,
|
||||||
patch_url: None,
|
patch_url: None,
|
||||||
build: hammer_core::swm::SwmBuild {
|
build: hammer_core::swm::SwmBuild {
|
||||||
@@ -493,8 +495,10 @@ caps = ["query", "compile", "inject", "inject-real", "init"]
|
|||||||
t_of(&Command::Compile {
|
t_of(&Command::Compile {
|
||||||
recipe: RecipeInline {
|
recipe: RecipeInline {
|
||||||
name: "x".into(),
|
name: "x".into(),
|
||||||
repo: "g".into(),
|
repo: Some("g".into()),
|
||||||
commit: "c".into(),
|
commit: Some("c".into()),
|
||||||
|
tarball: None,
|
||||||
|
sha256: None,
|
||||||
patch: None,
|
patch: None,
|
||||||
compiler: "zig-cc".into(),
|
compiler: "zig-cc".into(),
|
||||||
target: "t".into(),
|
target: "t".into(),
|
||||||
|
|||||||
@@ -138,8 +138,10 @@ fn missing_cap_returns_error_no_cap() {
|
|||||||
&Command::Compile {
|
&Command::Compile {
|
||||||
recipe: RecipeInline {
|
recipe: RecipeInline {
|
||||||
name: "x".into(),
|
name: "x".into(),
|
||||||
repo: "git://x".into(),
|
repo: Some("git://x".into()),
|
||||||
commit: "abc".into(),
|
commit: Some("abc".into()),
|
||||||
|
tarball: None,
|
||||||
|
sha256: None,
|
||||||
patch: None,
|
patch: None,
|
||||||
compiler: "zig-cc".into(),
|
compiler: "zig-cc".into(),
|
||||||
target: "x86_64-linux-musl".into(),
|
target: "x86_64-linux-musl".into(),
|
||||||
|
|||||||
+4
-2
@@ -94,8 +94,10 @@ pre-requisito de validación.
|
|||||||
- [x] Provenance en `export`: mapa artefacto→receta vía sidecar `.hammer/recipe.toml` que
|
- [x] Provenance en `export`: mapa artefacto→receta vía sidecar `.hammer/recipe.toml` que
|
||||||
`hammer-build::build` escribe dentro del artefacto antes de sellar. `hammer export`
|
`hammer-build::build` escribe dentro del artefacto antes de sellar. `hammer export`
|
||||||
agrupa eventos por `artifact_hash` y emite UN `source_patch` por grupo cuya receta
|
agrupa eventos por `artifact_hash` y emite UN `source_patch` por grupo cuya receta
|
||||||
sea recuperable; lo demás cae al fallback `file_drop`. Source `git` modelado;
|
sea recuperable; lo demás cae al fallback `file_drop`. Source `git` **y `tarball`**
|
||||||
`tarball` cae a file_drop con warning (pendiente extender SourcePatch).
|
modelados: `Mutation::SourcePatch` (y `RecipeInline`) llevan campos `repo`/`commit`
|
||||||
|
**xor** `tarball`/`sha256` (resueltos por `swm::swm_source_kind`, misma regla que
|
||||||
|
`recipe::Source::kind`); `hammer export` reconstruye ambos sin caer a `file_drop`.
|
||||||
- [x] Firma `signature` (ed25519) y `TrustStore` local. `hammer_core::sign`: `KeyPair`
|
- [x] Firma `signature` (ed25519) y `TrustStore` local. `hammer_core::sign`: `KeyPair`
|
||||||
(genera/carga/escribe claves), `TrustStore::load(dir)` (lee `*.ed25519.pub`),
|
(genera/carga/escribe claves), `TrustStore::load(dir)` (lee `*.ed25519.pub`),
|
||||||
`Swm::verify_signature(&trust) → SigStatus` (`trusted`/`unknown-key`/`bad-sig`/
|
`Swm::verify_signature(&trust) → SigStatus` (`trusted`/`unknown-key`/`bad-sig`/
|
||||||
|
|||||||
Reference in New Issue
Block a user