diff --git a/crates/hammer-build/src/lib.rs b/crates/hammer-build/src/lib.rs index 1c9ee52d..f40b4d0f 100644 --- a/crates/hammer-build/src/lib.rs +++ b/crates/hammer-build/src/lib.rs @@ -1,6 +1,7 @@ //! El laboratorio: compila una receta de forma hermética y la sella en el store; e hidrata //! artefactos al FHS. Ver `docs/02-build-lab.md` y `docs/03-hydration.md`. +use std::collections::HashMap; use std::path::{Path, PathBuf}; use hammer_core::{ArtifactHash, Compiler, LinkMode, Phases, Recipe, Store}; @@ -30,10 +31,21 @@ pub use swm_bridge::build_source_patch; /// fallan con un mensaje que nombra la cadena. pub fn artifact_hash(recipe: &Recipe, _store: &Store) -> hammer_core::Result { let mut stack: Vec = Vec::new(); - artifact_hash_rec(recipe, &mut stack) + // Memo por path canónico de receta: el grafo de build es un DAG-diamante (p.ej. TODO KF6/Qt + // converge en qtbase/kcoreaddons, alcanzables por decenas de rutas). Sin memoizar, el walk + // recursivo es exponencial en NÚMERO DE CAMINOS y cuelga en paquetes grandes (plasma-workspace, + // ~90 deps). El hash de una receta es función pura de su subárbol ⇒ computar una vez por nodo. + // Clave = path resuelto (no `name`): dos variantes homónimas (incoming-kde/libxml2 vs canónica) + // no deben colisionar. Ver granja-promote-colisiones. + let mut memo: HashMap = HashMap::new(); + artifact_hash_rec(recipe, &mut stack, &mut memo) } -fn artifact_hash_rec(recipe: &Recipe, stack: &mut Vec) -> hammer_core::Result { +fn artifact_hash_rec( + recipe: &Recipe, + stack: &mut Vec, + memo: &mut HashMap, +) -> hammer_core::Result { if stack.iter().any(|n| n == &recipe.name) { stack.push(recipe.name.clone()); return Err(hammer_core::Error::Recipe(format!( @@ -42,7 +54,7 @@ fn artifact_hash_rec(recipe: &Recipe, stack: &mut Vec) -> hammer_core::R ))); } - let dep_hashes = resolve_build_dep_hashes(recipe, stack)?; + let dep_hashes = resolve_build_dep_hashes(recipe, stack, memo)?; let inputs = recipe.hash_inputs(&dep_hashes)?; let refs: Vec<&[u8]> = inputs.iter().map(|v| v.as_slice()).collect(); Ok(ArtifactHash::of_inputs(&refs)) @@ -73,6 +85,7 @@ fn resolve_dep_path(base_dir: &Path, dep: &str) -> PathBuf { fn resolve_build_dep_hashes( recipe: &Recipe, stack: &mut Vec, + memo: &mut HashMap, ) -> hammer_core::Result> { if recipe.deps.build.is_empty() { return Ok(Vec::new()); @@ -89,6 +102,12 @@ fn resolve_build_dep_hashes( let mut hashes = Vec::with_capacity(recipe.deps.build.len()); for dep in &recipe.deps.build { let dep_path = resolve_dep_path(&recipe.base_dir, dep); + // Clave del memo: path canónico (colapsa `./a/../b` y symlinks a una identidad estable). + let key = dep_path.canonicalize().unwrap_or_else(|_| dep_path.clone()); + if let Some(h) = memo.get(&key) { + hashes.push(h.clone()); + continue; + } let dep_recipe = Recipe::load_from_path(&dep_path).map_err(|e| { hammer_core::Error::Recipe(format!( "no pude cargar la dep de build '{dep}' de '{}' ({}): {e}", @@ -96,7 +115,9 @@ fn resolve_build_dep_hashes( dep_path.display() )) })?; - hashes.push(artifact_hash_rec(&dep_recipe, stack)?); + let h = artifact_hash_rec(&dep_recipe, stack, memo)?; + memo.insert(key, h.clone()); + hashes.push(h); } stack.pop(); Ok(hashes)