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>
This commit is contained in:
2026-07-12 09:33:50 -04:00
co-authored by Claude Opus 4.8
parent 54d8b1953f
commit e69eaaaa3f
+79 -3
View File
@@ -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(),