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:
Sergio
2026-06-14 03:06:35 +00:00
co-authored by Claude Opus 4.8
parent 7c099afe4f
commit 0f42c38106
10 changed files with 280 additions and 83 deletions
+91 -40
View File
@@ -27,33 +27,54 @@ pub fn build_source_patch(
store: &Store,
scratch_root: Option<&Path>,
) -> hammer_core::Result<ArtifactHash> {
let (repo, commit, patch, patch_url, build_cfg, target_bin, expected) = match mutation {
Mutation::SourcePatch {
repo,
commit,
patch,
patch_url,
build,
target_bin,
expected_hash,
} => (repo, commit, 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(),
));
}
};
let (repo, commit, tarball, sha256, patch, patch_url, build_cfg, target_bin, expected) =
match mutation {
Mutation::SourcePatch {
repo,
commit,
tarball,
sha256,
patch,
patch_url,
build,
target_bin,
expected_hash,
} => (
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 recipe_dir = scratch.join("swm-recipes");
std::fs::create_dir_all(&recipe_dir)?;
// Materializamos el patch a disco bajo un nombre derivado del commit (estable: si el .swm
// se reaplica, reusamos el mismo archivo y el hash de la receta no depende de
// Clave estable del origen para nombrar artefactos a disco: el commit (git) o el
// 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
// no vengan ambos.
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 {
std::fs::write(&patch_path, text.as_bytes())?;
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 recipe = synthesize_recipe(
&name,
commit,
repo,
build_cfg,
patches,
&recipe_dir,
)?;
let recipe = synthesize_recipe(&name, &source_kind, build_cfg, patches, &recipe_dir)?;
let hash = build(&recipe, cfg, store)?;
if let Some(want) = expected {
@@ -102,24 +116,33 @@ fn derive_name(target_bin: &str) -> String {
fn synthesize_recipe(
name: &str,
commit: &str,
repo: &str,
source: &hammer_core::SourceKind<'_>,
build_cfg: &SwmBuild,
patches: Vec<String>,
base_dir: &Path,
) -> hammer_core::Result<Recipe> {
let compiler = parse_compiler(&build_cfg.compiler)?;
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`.
let toml_text = format!(
r#"
name = "{name}"
version = "swm-{commit_short}"
version = "swm-{version_short}"
[source]
repo = "{repo}"
commit = "{commit}"
{source_block}
[build]
compiler = "{compiler}"
target = "{target}"
@@ -127,9 +150,8 @@ link = "{link}"
flags = []
"#,
name = name,
commit_short = &commit[..commit.len().min(12)],
repo = repo,
commit = commit,
version_short = &version_key[..version_key.len().min(12)],
source_block = source_block,
compiler = compiler.as_str(),
target = build_cfg.target,
link = link.as_str(),
@@ -199,8 +221,10 @@ mod tests {
let d = tempfile::tempdir().unwrap();
let r = synthesize_recipe(
"grep",
"a1b2c3d4e5f6a7b8c9",
"git://example/grep.git",
&hammer_core::SourceKind::Git {
repo: "git://example/grep.git",
commit: "a1b2c3d4e5f6a7b8c9",
},
&fake_swm_build(),
vec![],
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]
fn parse_compiler_and_link() {
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();
let commit = "deadbeefcafe";
let m = Mutation::SourcePatch {
repo: "git://x".into(),
commit: commit.into(),
repo: Some("git://x".into()),
commit: Some(commit.into()),
tarball: None,
sha256: None,
patch: None,
patch_url: Some(format!("file://{}", patch_src.display())),
build: fake_swm_build(),