Etapa F dogfood: fix bug de patch en install (.swm con patch + build-dep)

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.
This commit is contained in:
2026-06-21 11:12:52 -04:00
parent 371d2f7cce
commit a9cba5002e
+42 -15
View File
@@ -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 // Patch bajo un nombre derivado del nombre-de-receta + clave de origen, para no colisionar
// entre `{dep}.patch` distintos en el mismo catálogo. // 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 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 { if let Some(text) = patch {
std::fs::write(&patch_path, text.as_bytes())?; std::fs::write(&patch_path, text.as_bytes())?;
} else if let Some(url) = patch_url { } else if let Some(url) = patch_url {
tracing::info!(%url, "fetch: descargando patch remoto"); tracing::info!(%url, "fetch: descargando patch remoto");
crate::download::fetch_url_to_file(url, &patch_path)?; 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)); 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 { if let Some(n) = strip_components {
recipe.source.strip_components = n; recipe.source.strip_components = n;
} }
// Resolución de patches relativos al recipe_dir donde acabamos de escribir los inline. // `patches` ya viene como nombres relativos a `base_dir` (o rutas absolutas que generamos
// Aceptamos rutas absolutas tal cual: las hemos generado nosotros. // nosotros): el lab los resuelve con `base_dir.join(p)` UNA vez al construir. NO los re-unimos
recipe.source.patches = patches // acá — hacerlo doblaba el prefijo `base_dir` y el join del build lo triplicaba (bug que el
.into_iter() // dogfood de `jq`, primer paquete con patch + build-dep instalado, destapó).
.map(|p| { recipe.source.patches = patches;
let path = Path::new(&p);
if path.is_absolute() {
p
} else {
base_dir.join(&p).to_string_lossy().into_owned()
}
})
.collect();
// Deps por NOMBRE (de la receta original): el lab las resolverá como `{dep}.toml` bajo el // 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 // `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 // 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] #[test]
fn parse_compiler_and_link() { fn parse_compiler_and_link() {
assert_eq!(parse_compiler("zig-cc").unwrap(), Compiler::ZigCc); assert_eq!(parse_compiler("zig-cc").unwrap(), Compiler::ZigCc);