From 9bc518899e80b19fb156e950ac5899c57fd0f4a2 Mon Sep 17 00:00:00 2001 From: Sergio Date: Thu, 10 Sep 2026 21:33:45 +0000 Subject: [PATCH] =?UTF-8?q?swm:=20`strip=5Fdebug`=20no=20viajaba=20en=20el?= =?UTF-8?q?=20paquete=20=E2=80=94=20y=20es=20ENTRADA=20DE=20HASH,=20as?= =?UTF-8?q?=C3=AD=20que=2040=20recetas=20no=20instalaban?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Encontrado instalando `takana` desde su propio repo (SDD 28 §5), que es la primera vez que se hace el viaje completo receta → `.tkn` → `install --require-signed` sobre un paquete con `strip_debug`. Error: expected_hash no coincide: declarado = b3:941d1857e6e93ebbfbad8240a1c30405263e5ff8fc881a923aee312160563eea obtenido = b3:2727050ebdbd7817b53e79ffe9796569e9364454ed35474b51e881fcc6048766 `why-differs` sobre los dos artefactos lo nombró: las recetas selladas diferían en `version`, `license` y **`strip_debug`**. Los dos binarios pesaban EXACTAMENTE lo mismo (3.374.768 bytes) y sólo divergían en `.shstrtab` — la firma de un `strip` que corrió una vez y la otra no. `swm_bridge` re-inyecta con cuidado `flags`, `phases`, `zig_version`, `strip_components`, `patches`, `deps`, `evidence` y `slots` («fidelidad de reconstrucción», dice su comentario) y **se olvidaba de `strip_debug`**, que ni siquiera existía en `SwmBuild` ⇒ no viajaba en el `.swm` en absoluto. Como ENTRA EN `hash_inputs` (`recipe.rs:572`, con el comentario «cambia el CONTENIDO del artefacto, así que TIENE que entrar»), el receptor reconstruía con `None` y sellaba en otra dirección. **Alcance medido: 40 recetas del corpus lo usan, 18 de las 88 del perfil `servidor`.** Para todas, el `expected_hash` anclado por `pack --build` no coincidía NUNCA y `install --require-signed` abortaba. O sea: una quinta parte del repo no se podía instalar, y nadie lo sabía porque nunca se había cerrado el lazo. Por qué no lo cazó nadie antes: `tree` —sin `strip_debug`— da cache-hit y funciona perfecto. El bug sólo aparece en las recetas que lo declaran, y el dogfood previo no las tocaba. Test de regresión con su control: una receta CON `strip_debug` lo lleva en el `.swm`, y una SIN él no gana un `false` inventado. 221 tests de takana-core en verde. Verificado end-to-end tras el arreglo: `install takana --repo http:// --require-signed` da cache-hit en `b3:941d1857…` —el mismo hash anclado— e hidrata los 3 ficheros. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x --- crates/hammerd/src/bus.rs | 1 + crates/takana-build/src/swm_bridge.rs | 4 +++ crates/takana-cli/src/main.rs | 1 + crates/takana-core/src/compat.rs | 1 + crates/takana-core/src/swm.rs | 47 +++++++++++++++++++++++++++ 5 files changed, 54 insertions(+) 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,