From a9cba5002ef45ce9155852b7f2a0d86f6bb3e255 Mon Sep 17 00:00:00 2001 From: sergio Date: Sun, 21 Jun 2026 11:12:52 -0400 Subject: [PATCH] Etapa F dogfood: fix bug de patch en install (.swm con patch + build-dep) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dogfoodear el corpus entero (build-repo.sh: 55/56 con expected_hash anclado, repo firmado Ed25519) destapó un bug latente en la reconstruccion desde .swm: el path del patch se prefijaba con el catalogo (swm-recipes/) TRES veces, y el install abortaba con 'No such file'. jq es el PRIMER paquete con patch + build-dep que se instala (fd/bwrap no tienen patch) -> nunca se habia ejercido ese camino. Causa: recipe_from_source_patch metia catalog_dir en el path Y synthesize_recipe lo re-unia a base_dir, redundante con el base_dir.join(p) que el lab hace al construir (fetch::apply_patches / Recipe::hash). Fix: source.patches guarda solo el NOMBRE relativo a base_dir; un unico join lo resuelve. El hash usa el CONTENIDO del patch, no el string del path -> el expected_hash anclado sigue casando. Verificado e2e: install jq desde el repo firmado -> trusted -> reproduce desde fuente (hash e770e04d = el anclado) -> patch aplicado -> 18 ficheros hidratados -> registrado en DB -> jq corre. + test de regresion inline_patch_resuelve_con_un_solo_join. --- crates/hammer-build/src/swm_bridge.rs | 57 ++++++++++++++++++++------- 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/crates/hammer-build/src/swm_bridge.rs b/crates/hammer-build/src/swm_bridge.rs index b7e91779..302bd797 100644 --- a/crates/hammer-build/src/swm_bridge.rs +++ b/crates/hammer-build/src/swm_bridge.rs @@ -107,14 +107,18 @@ pub fn recipe_from_source_patch( // Patch bajo un nombre derivado del nombre-de-receta + clave de origen, para no colisionar // entre `{dep}.patch` distintos en el mismo catálogo. let label = name.map(|n| n.to_string()).unwrap_or_else(|| derive_name(target_bin)); - let patch_path = catalog_dir.join(format!("{label}-{source_key}.patch")); + // `source.patches` guarda el NOMBRE relativo a `base_dir` (= catalog_dir); el lab resuelve + // `base_dir.join(nombre)` UNA vez al construir (fetch::apply_patches / Recipe hash). Guardar el + // path completo acá lo haría re-unir a base_dir aguas abajo (doble/triple prefijo). + let filename = format!("{label}-{source_key}.patch"); + let patch_path = catalog_dir.join(&filename); if let Some(text) = patch { std::fs::write(&patch_path, text.as_bytes())?; } else if let Some(url) = patch_url { tracing::info!(%url, "fetch: descargando patch remoto"); crate::download::fetch_url_to_file(url, &patch_path)?; } - patches.push(patch_path.to_string_lossy().into_owned()); + patches.push(filename); } let name = name.map(|n| n.to_string()).unwrap_or_else(|| derive_name(target_bin)); @@ -194,19 +198,11 @@ flags = [] if let Some(n) = strip_components { recipe.source.strip_components = n; } - // Resolución de patches relativos al recipe_dir donde acabamos de escribir los inline. - // Aceptamos rutas absolutas tal cual: las hemos generado nosotros. - recipe.source.patches = patches - .into_iter() - .map(|p| { - let path = Path::new(&p); - if path.is_absolute() { - p - } else { - base_dir.join(&p).to_string_lossy().into_owned() - } - }) - .collect(); + // `patches` ya viene como nombres relativos a `base_dir` (o rutas absolutas que generamos + // nosotros): el lab los resuelve con `base_dir.join(p)` UNA vez al construir. NO los re-unimos + // acá — hacerlo doblaba el prefijo `base_dir` y el join del build lo triplicaba (bug que el + // dogfood de `jq`, primer paquete con patch + build-dep instalado, destapó). + recipe.source.patches = patches; // Deps por NOMBRE (de la receta original): el lab las resolverá como `{dep}.toml` bajo el // `base_dir` — por eso base_dir = el catálogo (no "/"): los patches son absolutos y no lo // necesitan, pero la resolución de build-deps SÍ. `install` puebla el catálogo con los @@ -314,6 +310,37 @@ mod tests { } } + #[test] + fn inline_patch_resuelve_con_un_solo_join_de_base_dir() { + // Regresión: `source.patches` debe guardar el NOMBRE relativo a base_dir, de modo que el + // lab resuelva `base_dir.join(p)` UNA sola vez. El bug doblaba/triplicaba el prefijo + // `swm-recipes/` (lo destapó el dogfood de `jq`, primer paquete con patch instalado). + let d = tempfile::tempdir().unwrap(); + let catalog = d.path().join("swm-recipes"); + let m = Mutation::SourcePatch { + repo: None, + commit: None, + tarball: Some("https://x/jq-1.8.1.tar.gz".into()), + sha256: Some("2be64e7129cecb11d5906290eba10af694fb9e3e7f9fc208a311dc33ca837eb0".into()), + strip_components: None, + patch: Some("--- a\n+++ b\n".into()), + patch_url: None, + build: fake_swm_build(), + target_bin: "/usr/bin/jq".into(), + expected_hash: None, + deps: Default::default(), + }; + let recipe = recipe_from_source_patch(&m, None, &catalog).unwrap(); + assert_eq!(recipe.source.patches.len(), 1); + let p = &recipe.source.patches[0]; + // El nombre NO debe llevar embebido el dir del catálogo (eso causaba el doble prefijo). + assert!(!p.contains("swm-recipes"), "patch debe ser relativo a base_dir, no llevar el catálogo: {p}"); + // Y base_dir.join(p) — el ÚNICO join que hace el lab — debe dar el fichero real escrito. + let resolved = recipe.base_dir.join(p); + assert!(resolved.is_file(), "base_dir.join(patch) debe existir en un solo paso: {}", resolved.display()); + assert_eq!(std::fs::read(&resolved).unwrap(), b"--- a\n+++ b\n"); + } + #[test] fn parse_compiler_and_link() { assert_eq!(parse_compiler("zig-cc").unwrap(), Compiler::ZigCc);