swm: strip_debug no viajaba en el paquete — y es ENTRADA DE HASH, así que 40 recetas no instalaban
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://<caja> --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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
This commit is contained in:
@@ -378,6 +378,7 @@ fn run_compile(recipe: RecipeInline, store_root: PathBuf, tx: Sender<Event>) {
|
|||||||
flags: recipe.flags,
|
flags: recipe.flags,
|
||||||
phases: Default::default(),
|
phases: Default::default(),
|
||||||
zig_version: None,
|
zig_version: None,
|
||||||
|
strip_debug: None,
|
||||||
},
|
},
|
||||||
target_bin: format!("/usr/bin/{name}"),
|
target_bin: format!("/usr/bin/{name}"),
|
||||||
expected_hash: None,
|
expected_hash: None,
|
||||||
|
|||||||
@@ -218,6 +218,10 @@ flags = []
|
|||||||
// el .swm (si vienen) y se reinyectan en la receta efímera tal cual los tenía el original.
|
// 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.phases = build_cfg.phases.clone();
|
||||||
recipe.build.zig_version = build_cfg.zig_version.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 {
|
if let Some(n) = strip_components {
|
||||||
recipe.source.strip_components = n;
|
recipe.source.strip_components = n;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3020,6 +3020,7 @@ fn build_export_mutations(
|
|||||||
flags: recipe.build.flags.clone(),
|
flags: recipe.build.flags.clone(),
|
||||||
phases: recipe.build.phases.clone(),
|
phases: recipe.build.phases.clone(),
|
||||||
zig_version: recipe.build.zig_version.clone(),
|
zig_version: recipe.build.zig_version.clone(),
|
||||||
|
strip_debug: recipe.build.strip_debug,
|
||||||
},
|
},
|
||||||
target_bin,
|
target_bin,
|
||||||
expected_hash: Some(art_hash),
|
expected_hash: Some(art_hash),
|
||||||
|
|||||||
@@ -352,6 +352,7 @@ mod tests {
|
|||||||
flags: vec![],
|
flags: vec![],
|
||||||
phases: Default::default(),
|
phases: Default::default(),
|
||||||
zig_version: None,
|
zig_version: None,
|
||||||
|
strip_debug: None,
|
||||||
},
|
},
|
||||||
target_bin: "/usr/bin/app".into(),
|
target_bin: "/usr/bin/app".into(),
|
||||||
expected_hash: Some("b3:app".into()),
|
expected_hash: Some("b3:app".into()),
|
||||||
|
|||||||
@@ -157,6 +157,16 @@ pub struct SwmBuild {
|
|||||||
/// `None` ⇒ el zig por defecto del receptor.
|
/// `None` ⇒ el zig por defecto del receptor.
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub zig_version: Option<String>,
|
pub zig_version: Option<String>,
|
||||||
|
/// 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<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -292,6 +302,7 @@ impl Swm {
|
|||||||
flags: recipe.build.flags.clone(),
|
flags: recipe.build.flags.clone(),
|
||||||
phases: recipe.build.phases.clone(),
|
phases: recipe.build.phases.clone(),
|
||||||
zig_version: recipe.build.zig_version.clone(),
|
zig_version: recipe.build.zig_version.clone(),
|
||||||
|
strip_debug: recipe.build.strip_debug,
|
||||||
};
|
};
|
||||||
|
|
||||||
let swm = Swm {
|
let swm = Swm {
|
||||||
@@ -609,6 +620,7 @@ mutations:
|
|||||||
flags: vec![],
|
flags: vec![],
|
||||||
phases: Default::default(),
|
phases: Default::default(),
|
||||||
zig_version: None,
|
zig_version: None,
|
||||||
|
strip_debug: None,
|
||||||
},
|
},
|
||||||
target_bin: "/bin/x".into(),
|
target_bin: "/bin/x".into(),
|
||||||
expected_hash: None,
|
expected_hash: None,
|
||||||
@@ -641,6 +653,7 @@ mutations:
|
|||||||
flags: vec![],
|
flags: vec![],
|
||||||
phases: Default::default(),
|
phases: Default::default(),
|
||||||
zig_version: None,
|
zig_version: None,
|
||||||
|
strip_debug: None,
|
||||||
},
|
},
|
||||||
target_bin: "/bin/grep".into(),
|
target_bin: "/bin/grep".into(),
|
||||||
expected_hash: None,
|
expected_hash: None,
|
||||||
@@ -670,6 +683,7 @@ mutations:
|
|||||||
flags: vec![],
|
flags: vec![],
|
||||||
phases: Default::default(),
|
phases: Default::default(),
|
||||||
zig_version: None,
|
zig_version: None,
|
||||||
|
strip_debug: None,
|
||||||
},
|
},
|
||||||
target_bin: "/bin/x".into(),
|
target_bin: "/bin/x".into(),
|
||||||
expected_hash: None,
|
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]
|
#[test]
|
||||||
fn verify_schema_source_patch_rechaza_sin_origen() {
|
fn verify_schema_source_patch_rechaza_sin_origen() {
|
||||||
let m = Mutation::SourcePatch {
|
let m = Mutation::SourcePatch {
|
||||||
@@ -772,6 +818,7 @@ zig_version = "0.13.0"
|
|||||||
flags: vec![],
|
flags: vec![],
|
||||||
phases: Default::default(),
|
phases: Default::default(),
|
||||||
zig_version: None,
|
zig_version: None,
|
||||||
|
strip_debug: None,
|
||||||
},
|
},
|
||||||
target_bin: "/bin/x".into(),
|
target_bin: "/bin/x".into(),
|
||||||
expected_hash: None,
|
expected_hash: None,
|
||||||
|
|||||||
Reference in New Issue
Block a user