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:
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user