> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…
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.
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);