diff --git a/crates/hammerd/src/bus.rs b/crates/hammerd/src/bus.rs index 803ec0c7..5d036511 100644 --- a/crates/hammerd/src/bus.rs +++ b/crates/hammerd/src/bus.rs @@ -378,6 +378,7 @@ fn run_compile(recipe: RecipeInline, store_root: PathBuf, tx: Sender) { flags: recipe.flags, phases: Default::default(), zig_version: None, + strip_debug: None, }, target_bin: format!("/usr/bin/{name}"), expected_hash: None, diff --git a/crates/takana-build/src/swm_bridge.rs b/crates/takana-build/src/swm_bridge.rs index c49ddacc..349cb58b 100644 --- a/crates/takana-build/src/swm_bridge.rs +++ b/crates/takana-build/src/swm_bridge.rs @@ -218,6 +218,10 @@ flags = [] // el .swm (si vienen) y se reinyectan en la receta efímera tal cual los tenía el original. recipe.build.phases = build_cfg.phases.clone(); recipe.build.zig_version = build_cfg.zig_version.clone(); + // `strip_debug` ENTRA EN `hash_inputs`: sin esta línea el receptor reconstruye con `None`, + // sella en otra dirección y el `expected_hash` anclado no coincide NUNCA. Ver la doc del campo + // en `swm::SwmBuild::strip_debug`. + recipe.build.strip_debug = build_cfg.strip_debug; if let Some(n) = strip_components { recipe.source.strip_components = n; } diff --git a/crates/takana-cli/src/main.rs b/crates/takana-cli/src/main.rs index ce3df589..10bbae3f 100644 --- a/crates/takana-cli/src/main.rs +++ b/crates/takana-cli/src/main.rs @@ -3020,6 +3020,7 @@ fn build_export_mutations( flags: recipe.build.flags.clone(), phases: recipe.build.phases.clone(), zig_version: recipe.build.zig_version.clone(), + strip_debug: recipe.build.strip_debug, }, target_bin, expected_hash: Some(art_hash), diff --git a/crates/takana-core/src/compat.rs b/crates/takana-core/src/compat.rs index 2b45863f..8ed53a45 100644 --- a/crates/takana-core/src/compat.rs +++ b/crates/takana-core/src/compat.rs @@ -352,6 +352,7 @@ mod tests { flags: vec![], phases: Default::default(), zig_version: None, + strip_debug: None, }, target_bin: "/usr/bin/app".into(), expected_hash: Some("b3:app".into()), diff --git a/crates/takana-core/src/swm.rs b/crates/takana-core/src/swm.rs index 4ac9cfc9..c952d378 100644 --- a/crates/takana-core/src/swm.rs +++ b/crates/takana-core/src/swm.rs @@ -157,6 +157,16 @@ pub struct SwmBuild { /// `None` ⇒ el zig por defecto del receptor. #[serde(default, skip_serializing_if = "Option::is_none")] pub zig_version: Option, + /// Split de la info de depuración (`Build::strip_debug`, SDD 23 etapa 4). + /// + /// **Tiene que viajar, y que faltara era un bug de reproducibilidad medible**: `strip_debug` + /// ENTRA EN `hash_inputs` (cambia el contenido del artefacto), así que un `.swm` que no lo + /// llevara hacía que el receptor reconstruyera con `strip_debug` en `None` y sellara en OTRA + /// dirección. El `expected_hash` anclado por `pack --build` no coincidía nunca y + /// `install --require-signed` abortaba — para 40 recetas del corpus, 18 de las 88 del perfil + /// `servidor`. Encontrado el 2026-09-10 instalando `takana` desde su propio repo (SDD 28 §5). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub strip_debug: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -292,6 +302,7 @@ impl Swm { flags: recipe.build.flags.clone(), phases: recipe.build.phases.clone(), zig_version: recipe.build.zig_version.clone(), + strip_debug: recipe.build.strip_debug, }; let swm = Swm { @@ -609,6 +620,7 @@ mutations: flags: vec![], phases: Default::default(), zig_version: None, + strip_debug: None, }, target_bin: "/bin/x".into(), expected_hash: None, @@ -641,6 +653,7 @@ mutations: flags: vec![], phases: Default::default(), zig_version: None, + strip_debug: None, }, target_bin: "/bin/grep".into(), expected_hash: None, @@ -670,6 +683,7 @@ mutations: flags: vec![], phases: Default::default(), zig_version: None, + strip_debug: None, }, target_bin: "/bin/x".into(), expected_hash: None, @@ -755,6 +769,38 @@ zig_version = "0.13.0" } } + /// `strip_debug` ENTRA EN `hash_inputs`, así que un `.swm` que no lo llevara hacía que el + /// receptor sellara en OTRA dirección y `install --require-signed` abortara siempre. Medido el + /// 2026-09-10 instalando `takana` desde su propio repo: declarado `b3:941d1857…`, obtenido + /// `b3:2727050e…`. Afectaba a 40 recetas del corpus. Este test es el que faltaba. + #[test] + fn from_recipe_lleva_strip_debug_porque_es_entrada_de_hash() { + let con = crate::Recipe::from_toml( + "name = \"takana\"\nversion = \"0.0.1\"\n[source]\nrepo = \"ssh://x/y.git\"\ncommit = \"abc\"\n[build]\nstrip_debug = true\n", + ) + .unwrap(); + let swm = Swm::from_recipe(&con, "/usr/bin/takana", None, None, "dev").unwrap(); + match &swm.mutations[0] { + Mutation::SourcePatch { build, .. } => { + assert_eq!(build.strip_debug, Some(true), "strip_debug tiene que viajar en el .swm"); + } + other => panic!("esperaba SourcePatch, obtuve {other:?}"), + } + + // Y el control que hace que el test valga: sin el campo, no se inventa un `false`. + let sin = crate::Recipe::from_toml( + "name = \"tree\"\nversion = \"2.3.2\"\n[source]\nrepo = \"ssh://x/y.git\"\ncommit = \"abc\"\n[build]\n", + ) + .unwrap(); + let swm2 = Swm::from_recipe(&sin, "/usr/bin/tree", None, None, "dev").unwrap(); + match &swm2.mutations[0] { + Mutation::SourcePatch { build, .. } => { + assert_eq!(build.strip_debug, None, "una receta sin strip_debug no debe ganar uno"); + } + other => panic!("esperaba SourcePatch, obtuve {other:?}"), + } + } + #[test] fn verify_schema_source_patch_rechaza_sin_origen() { let m = Mutation::SourcePatch { @@ -772,6 +818,7 @@ zig_version = "0.13.0" flags: vec![], phases: Default::default(), zig_version: None, + strip_debug: None, }, target_bin: "/bin/x".into(), expected_hash: None,