> 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…
build/sandbox: fusionar deps en UNA capa overlay cuando son muchas (fix límite lowerdir)
overlayfs concatena todas las lowerdir en un string de opciones acotado a ~1 página (4096B). KIO
(~43 deps) desbordaba → bwrap 'Can't make overlay mount ... No such file or directory' (truncado).
bwrap CANONICALIZA cada --overlay-src a su ruta real, así que symlinks cortos no ayudan (probado).
Fix: cuando la suma de rutas supera ~3000B, se funden todas las deps en un dir real por hardlinks
(cp -alf, last-wins = misma semántica que el apilado overlay) y se pasa esa ÚNICA capa ⇒ lowerdir
constante, escala a cientos de deps (todo Plasma/apps). Cacheado por build con marker .done (una vez
aunque bwrap_args corra por fase). Pocas deps = ruta directa sin I/O (sandbox idéntico, tests intactos).
Validado: KIO configura con 43 deps visibles y compila. 57 tests verdes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
diff --git a/crates/hammer-build/src/sandbox.rs b/crates/hammer-build/src/sandbox.rs
index c33e46a6..f10cebdf 100644
--- a/crates/hammer-build/src/sandbox.rs
+++ b/crates/hammer-build/src/sandbox.rs
@@ -173,6 +173,54 @@ impl Sandbox {
})
}
+ /// Funde todas las `deps` en UN solo directorio real por hardlinks (`cp -al`, last-wins) y lo
+ /// devuelve para usarlo como capa overlay única. Ver [`Self::bwrap_args`]: bwrap canonicaliza los
+ /// `--overlay-src`, así que la única forma de no desbordar el `lowerdir` con muchas deps es
+ /// colapsarlas a una capa. El merge vive junto al store (mismo filesystem ⇒ hardlinks válidos) y
+ /// es único-por-build (id derivado del `src_dir`). Cacheado con marcador `.done`: se hace UNA vez
+ /// aunque `bwrap_args` se llame por cada fase (configure/compile/install). Devuelve `None` si
+ /// falla (el caller cae al apilado directo).
+ fn merge_deps_layer(&self) -> Option<PathBuf> {
+ use std::hash::{Hash, Hasher};
+ let mut h = std::collections::hash_map::DefaultHasher::new();
+ self.src_dir.hash(&mut h);
+ for d in &self.deps {
+ d.hash(&mut h);
+ }
+ // Junto al store para compartir filesystem (hardlinks no cruzan fs). El store suele estar en
+ // `<base>/store`; ubicamos el merge en `<base>/.dmerge/<id>`. Derivamos <base> del primer dep.
+ let base = self
+ .deps
+ .first()
+ .and_then(|d| std::fs::canonicalize(d).ok())
+ .and_then(|d| d.parent().and_then(|p| p.parent()).map(|p| p.to_path_buf()))
+ .unwrap_or_else(|| PathBuf::from("."));
+ let merged = base.join(".dmerge").join(format!("{:x}", h.finish()));
+ let done = merged.join(".done");
+ if done.exists() {
+ return Some(merged);
+ }
+ let _ = std::fs::remove_dir_all(&merged);
+ std::fs::create_dir_all(&merged).ok()?;
+ // `cp -al <dep>/. <merged>/` hardlinkea el árbol recursivamente; `-f` fuerza el last-wins en
+ // los pocos ficheros que coincidan (semántica de capa superior del overlay). Orden = orden de
+ // deps (la última gana), igual que el apilado `--overlay-src`.
+ for dep in &self.deps {
+ let real = std::fs::canonicalize(dep).ok()?;
+ let status = Command::new("cp")
+ .arg("-alf")
+ .arg(format!("{}/.", real.display()))
+ .arg(&merged)
+ .status()
+ .ok()?;
+ if !status.success() {
+ return None;
+ }
+ }
+ std::fs::write(&done, b"").ok()?;
+ Some(merged)
+ }
+
fn bwrap_args(&self, cmd: &str) -> Vec<String> {
let mut args: Vec<String> = vec![
"--overlay-src".into(),
@@ -181,9 +229,37 @@ impl Sandbox {
// Capas overlay de las deps de build, apiladas bajo el rootfs (mismo `--tmp-overlay /`). bwrap
// admite múltiples `--overlay-src` para un overlay; los `usr/{include,lib}` de cada dep se
// funden con los del rootfs en `/usr`. Sólo se emiten si hay deps (compat hacia atrás).
- for dep in &self.deps {
- args.push("--overlay-src".into());
- args.push(dep.display().to_string());
+ //
+ // LÍMITE DE PÁGINA (overlayfs): el kernel concatena todas las lowerdir en un string de
+ // opciones de mount acotado a ~1 página (4096B). Con muchas deps (KIO ~45, Plasma más), la
+ // suma de rutas largas `<store>/<64hex>-<name>` DESBORDA y el mount falla (bwrap: "Can't make
+ // overlay mount ... No such file or directory" por truncado). Mitigación: cuando la suma de
+ // longitudes se acerca al tope, symlinkeamos cada dep a un nombre corto (`<tmp>/hdl-<id>/<N>`)
+ // por-build y pasamos ESAS rutas → lowerdir compacto que escala a cientos de deps. Con pocas
+ // deps (caso común) usamos la ruta directa: cero I/O y sandbox idéntico al previo.
+ // bwrap CANONICALIZA cada `--overlay-src` a su ruta real para armar el `lowerdir=`, así que
+ // acortar con symlinks no sirve: el string sigue siendo la suma de rutas reales del store.
+ // La única forma de escalar a muchas deps es FUNDIRLAS en UN solo directorio real (hardlinks,
+ // `cp -al`, last-wins = misma semántica que el apilado overlay) y pasar esa ÚNICA capa ⇒
+ // lowerdir = rootfs + 1 merge, constante sin importar cuántas deps haya.
+ let total_len: usize = self.deps.iter().map(|d| d.as_os_str().len() + 16).sum();
+ if total_len > 3000 && self.deps.len() > 1 {
+ if let Some(merged) = self.merge_deps_layer() {
+ args.push("--overlay-src".into());
+ args.push(merged.display().to_string());
+ } else {
+ // Fallback: si la fusión falló, volvemos al apilado directo (puede desbordar, pero es
+ // mejor que abortar sin intentar).
+ for dep in &self.deps {
+ args.push("--overlay-src".into());
+ args.push(dep.display().to_string());
+ }
+ }
+ } else {
+ for dep in &self.deps {
+ args.push("--overlay-src".into());
+ args.push(dep.display().to_string());
+ }
}
args.extend([
"--tmp-overlay".into(),