qorpa prune: la poda nace con el subsistema, no después
Paso 7 del ADR 0015, sus dos mitades. PODA. Un rootfs son cientos de MB o varios GB y no lo alcanzan ni store-gc ni la caché .dmerge: es un tercer montón sin dueño, como ya lo fue work/sources. `hammer qorpa prune` barre restos de pulls a medias, imágenes que ninguna instancia usa —se re-traen por digest, que es justo la propiedad que da el pin— y, con --upper, las capas mutables, que por D3 siempre se pueden tirar. Con las dos cicatrices del repo cableadas: - SIN --yes es un simulacro. Y tras borrar COMPRUEBA que el directorio se fue, porque store-gc reportaba borrados que no ocurrían y eso se descubrió tarde. Si dice que sí y sigue ahí, sale ≠0 diciendo que el número de arriba no es lo que se liberó. - El tamaño de un árbol con directorios ilegibles sale MENOR de lo que es: un upper con ficheros de los subuid no se puede recorrer entero. Ahora se cuentan los directorios ciegos y la cifra se marca con `≥`. Un número silenciosamente bajo es peor que ninguno cuando con él se decide borrar. Y --upper deja la instancia USABLE: sin upper/ y work/ no vuelve a arrancar. LICENCIAS (SDD 20). Las imágenes ajenas quedan fuera del catálogo publicable y del reporte de licencias por escrito, y la razón no es pereza: no podemos enumerarlas — un `pacman -S` dentro de una instancia trae paquetes que nadie declaró acá, y afirmar una licencia sobre eso sería inventarla. Lo que sí se hace: contarlas aparte en clase `ajeno` (el riesgo real del ADR es que en seis meses alguien las cuente como corpus), y dejar dicho que si algún día se espejan hay que mirar licencia Y MARCA antes, igual que con Firefox. Más el corolario que faltaba escribir donde se lea: el claim «hammer reproduce bit a bit» hay que acotarlo desde el día que exista una instancia. 2 tests nuevos (la poda no toca una imagen en uso y sí los restos; con --upper la capa se va pero la instancia queda usable). 50/50. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U5cQtQrYNjWJVXVE6aEpQ2
This commit is contained in:
@@ -130,8 +130,22 @@ pub enum QorpaCmd {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
root: Option<PathBuf>,
|
root: Option<PathBuf>,
|
||||||
},
|
},
|
||||||
/// [ADR 0015 §Orden 7 — SIN IMPLEMENTAR] Poda imágenes sin instancia y `upper` descartables.
|
/// [ADR 0015 §Orden 7] Poda: restos de descargas a medias, imágenes que ninguna instancia usa
|
||||||
Prune,
|
/// y —si se pide— las capas mutables, que por D3 siempre se pueden tirar.
|
||||||
|
///
|
||||||
|
/// **Sin `--yes` no borra nada**: dice qué haría y cuánto liberaría. Este repo ya tuvo tres
|
||||||
|
/// emergencias de disco y un `store-gc` que REPORTABA borrados que no ocurrían.
|
||||||
|
Prune {
|
||||||
|
/// Además, tira la capa mutable de cada instancia. El manifiesto es la verdad y el `upper`
|
||||||
|
/// es caché (D3) — pero lo que instalaste a mano y no declaraste en `packages` se va.
|
||||||
|
#[arg(long)]
|
||||||
|
upper: bool,
|
||||||
|
/// Ejecuta de verdad. Sin esto es un simulacro.
|
||||||
|
#[arg(long)]
|
||||||
|
yes: bool,
|
||||||
|
#[arg(long)]
|
||||||
|
root: Option<PathBuf>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(cmd: QorpaCmd) -> Result<()> {
|
pub fn run(cmd: QorpaCmd) -> Result<()> {
|
||||||
@@ -150,10 +164,7 @@ pub fn run(cmd: QorpaCmd) -> Result<()> {
|
|||||||
QorpaCmd::Export { id, into, remove, root } => {
|
QorpaCmd::Export { id, into, remove, root } => {
|
||||||
export(&resolve_root(root), &id, into.as_deref(), remove)
|
export(&resolve_root(root), &id, into.as_deref(), remove)
|
||||||
}
|
}
|
||||||
QorpaCmd::Prune => bail!(
|
QorpaCmd::Prune { upper, yes, root } => prune(&resolve_root(root), upper, yes),
|
||||||
"sin implementar: la poda es el paso 7 del ADR 0015. Nace con el subsistema y no \
|
|
||||||
después — este repo ya tuvo tres emergencias de disco."
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -491,7 +502,15 @@ fn tree_state(tree: &Path) -> TreeState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn walk_tree(root: &Path) -> Result<(u64, u64)> {
|
fn walk_tree(root: &Path) -> Result<(u64, u64)> {
|
||||||
let (mut files, mut bytes) = (0u64, 0u64);
|
let (f, b, _) = walk_tree_ciegos(root)?;
|
||||||
|
Ok((f, b))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Igual, pero además cuenta los directorios que NO se pudieron leer. Sin ese tercer número, el
|
||||||
|
/// tamaño de un `upper` con ficheros de los subuid sale MENOR de lo que es y nadie lo nota: un
|
||||||
|
/// número silenciosamente bajo es peor que ninguno cuando lo que se decide con él es borrar.
|
||||||
|
fn walk_tree_ciegos(root: &Path) -> Result<(u64, u64, u64)> {
|
||||||
|
let (mut files, mut bytes, mut ciegos) = (0u64, 0u64, 0u64);
|
||||||
let mut stack = vec![root.to_path_buf()];
|
let mut stack = vec![root.to_path_buf()];
|
||||||
while let Some(d) = stack.pop() {
|
while let Some(d) = stack.pop() {
|
||||||
// Un rootfs ajeno trae directorios que NI SU DUEÑO puede leer (`/etc/sudoers.d` es 0750,
|
// Un rootfs ajeno trae directorios que NI SU DUEÑO puede leer (`/etc/sudoers.d` es 0750,
|
||||||
@@ -499,7 +518,7 @@ fn walk_tree(root: &Path) -> Result<(u64, u64)> {
|
|||||||
// Se cuentan como 0 y se sigue — abortar el pull por no poder contar sería absurdo.
|
// Se cuentan como 0 y se sigue — abortar el pull por no poder contar sería absurdo.
|
||||||
let rd = match std::fs::read_dir(&d) {
|
let rd = match std::fs::read_dir(&d) {
|
||||||
Ok(rd) => rd,
|
Ok(rd) => rd,
|
||||||
Err(_) => continue,
|
Err(_) => { ciegos += 1; continue }
|
||||||
};
|
};
|
||||||
for e in rd {
|
for e in rd {
|
||||||
let Ok(e) = e else { continue };
|
let Ok(e) = e else { continue };
|
||||||
@@ -512,7 +531,7 @@ fn walk_tree(root: &Path) -> Result<(u64, u64)> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok((files, bytes))
|
Ok((files, bytes, ciegos))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_manifest(dir: &Path, m: &ImageManifest) -> Result<()> {
|
fn write_manifest(dir: &Path, m: &ImageManifest) -> Result<()> {
|
||||||
@@ -1231,6 +1250,139 @@ fn run_instance(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── prune: la poda (§Orden 7) ───────────────────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Nace CON el subsistema y no después, a propósito: un rootfs son cientos de MB o varios GB, y no
|
||||||
|
// lo alcanzan ni `store-gc` ni la caché `.dmerge` — es un tercer montón sin dueño, como ya lo fue
|
||||||
|
// `work/sources`. La regla que la hace trivial es D3: **un `upper` siempre se puede borrar**.
|
||||||
|
//
|
||||||
|
// Dos cicatrices del repo, cableadas acá:
|
||||||
|
// · `store-gc` REPORTABA borrados que no ocurrían (corriendo en segundo plano). Por eso esto
|
||||||
|
// comprueba DESPUÉS de borrar que el directorio se fue, y si sigue ahí lo dice.
|
||||||
|
// · Un tamaño calculado sobre un árbol que no se puede leer entero sale MENOR de lo que es. Por
|
||||||
|
// eso se cuentan los directorios ciegos y la cifra se marca como cota inferior.
|
||||||
|
|
||||||
|
struct Candidato { que: String, ruta: PathBuf, bytes: u64, cota: bool }
|
||||||
|
|
||||||
|
fn prune(root: &Path, con_upper: bool, de_verdad: bool) -> Result<()> {
|
||||||
|
let images = root.join("images");
|
||||||
|
let mut cands: Vec<Candidato> = Vec::new();
|
||||||
|
|
||||||
|
// 1. Restos de descargas y desempacados a medias. Basura por definición: llevan el prefijo con
|
||||||
|
// punto justo para que `list` no los confunda con una imagen.
|
||||||
|
if let Ok(rd) = std::fs::read_dir(&images) {
|
||||||
|
for e in rd.flatten() {
|
||||||
|
let n = e.file_name().to_string_lossy().into_owned();
|
||||||
|
if !n.starts_with('.') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let (bytes, cota) = medir(&e.path());
|
||||||
|
cands.push(Candidato { que: format!("resto de un pull a medias ({n})"),
|
||||||
|
ruta: e.path(), bytes, cota });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Imágenes que ninguna instancia usa. Se pueden volver a traer por digest, así que borrarlas
|
||||||
|
// no pierde nada que no se pueda recuperar: es exactamente la propiedad que da el pin.
|
||||||
|
let usadas: std::collections::BTreeSet<String> = std::fs::read_dir(instances_dir(root))
|
||||||
|
.into_iter().flatten().flatten()
|
||||||
|
.filter_map(|e| {
|
||||||
|
let body = std::fs::read_to_string(e.path().join(INSTANCE_MANIFEST)).ok()?;
|
||||||
|
let i: Instance = toml::from_str(&body).ok()?;
|
||||||
|
Some(i.base.strip_prefix("sha256:").unwrap_or(&i.base).to_string())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if let Ok(rd) = std::fs::read_dir(&images) {
|
||||||
|
for e in rd.flatten() {
|
||||||
|
let n = e.file_name().to_string_lossy().into_owned();
|
||||||
|
if n.starts_with('.') || usadas.contains(&n) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let m: Option<ImageManifest> = std::fs::read_to_string(e.path().join(IMAGE_MANIFEST))
|
||||||
|
.ok().and_then(|s| toml::from_str(&s).ok());
|
||||||
|
// El tamaño ya está en el manifiesto (lo midió `pull` sobre el árbol recién
|
||||||
|
// desempacado, cuando todavía era legible entero): mejor dato y gratis.
|
||||||
|
let (bytes, cota) = match &m {
|
||||||
|
Some(m) if m.tree_bytes > 0 => (m.tree_bytes, false),
|
||||||
|
_ => medir(&e.path()),
|
||||||
|
};
|
||||||
|
let etiqueta = m.and_then(|m| m.label).unwrap_or_else(|| n[..12].to_string());
|
||||||
|
cands.push(Candidato { que: format!("imagen sin instancia ({etiqueta})"),
|
||||||
|
ruta: e.path(), bytes, cota });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Capas mutables. Sólo si se piden: son caché (D3), pero lo que instalaste a mano y no
|
||||||
|
// declaraste en `packages` no vuelve solo.
|
||||||
|
if con_upper {
|
||||||
|
for e in std::fs::read_dir(instances_dir(root)).into_iter().flatten().flatten() {
|
||||||
|
let id = e.file_name().to_string_lossy().into_owned();
|
||||||
|
let up = e.path().join("upper");
|
||||||
|
if !up.exists() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let (bytes, cota) = medir(&up);
|
||||||
|
cands.push(Candidato { que: format!("capa mutable de {id}"), ruta: up, bytes, cota });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cands.is_empty() {
|
||||||
|
println!("nada que podar en {}", root.display());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let total: u64 = cands.iter().map(|c| c.bytes).sum();
|
||||||
|
let alguna_cota = cands.iter().any(|c| c.cota);
|
||||||
|
for c in &cands {
|
||||||
|
println!(" {}{:<9} {}", if c.cota { "≥" } else { " " }, human(c.bytes), c.que);
|
||||||
|
}
|
||||||
|
println!("{}{} en {} elemento(s)", if alguna_cota { "≥ " } else { "" }, human(total), cands.len());
|
||||||
|
|
||||||
|
if !de_verdad {
|
||||||
|
println!("\nsimulacro: no se borró nada. Repetí con --yes.");
|
||||||
|
if !con_upper {
|
||||||
|
println!("(`--upper` añade las capas mutables, que por D3 siempre se pueden tirar)");
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let (mut ok, mut fallos) = (0u64, 0u64);
|
||||||
|
for c in &cands {
|
||||||
|
// `force_remove_dir_all` sabe abrir los directorios de sólo-lectura de un rootfs y, si el
|
||||||
|
// árbol tiene ficheros de los subuid, entrar al userns mapeado para borrarlos.
|
||||||
|
let r = if c.ruta.is_dir() { force_remove_dir_all(&c.ruta) } else { std::fs::remove_file(&c.ruta) };
|
||||||
|
// La cicatriz de store-gc: no basta con que la llamada devuelva Ok — se COMPRUEBA que se fue.
|
||||||
|
match (r, c.ruta.exists()) {
|
||||||
|
(Ok(()), false) => { ok += 1; }
|
||||||
|
(Ok(()), true) => { fallos += 1; eprintln!(" !! {} dijo que sí y SIGUE AHÍ", c.ruta.display()); }
|
||||||
|
(Err(e), _) => { fallos += 1; eprintln!(" !! {}: {e}", c.ruta.display()); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// La capa mutable es parte de la instancia: si se tiró, hay que dejarla utilizable.
|
||||||
|
if con_upper {
|
||||||
|
for e in std::fs::read_dir(instances_dir(root)).into_iter().flatten().flatten() {
|
||||||
|
let _ = std::fs::create_dir_all(e.path().join("upper"));
|
||||||
|
let _ = force_remove_dir_all(&e.path().join("work"));
|
||||||
|
let _ = std::fs::create_dir_all(e.path().join("work"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("podado: {ok} de {} elemento(s)", cands.len());
|
||||||
|
if fallos > 0 {
|
||||||
|
bail!("{fallos} no se pudieron borrar — el número de arriba NO es lo que se liberó");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tamaño de un árbol y si la cifra es cota inferior (hubo directorios ilegibles).
|
||||||
|
fn medir(p: &Path) -> (u64, bool) {
|
||||||
|
if p.is_file() {
|
||||||
|
return (std::fs::metadata(p).map(|m| m.len()).unwrap_or(0), false);
|
||||||
|
}
|
||||||
|
match walk_tree_ciegos(p) {
|
||||||
|
Ok((_, bytes, ciegos)) => (bytes, ciegos > 0),
|
||||||
|
Err(_) => (0, true),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── export: la capa de transparencia (D5) ───────────────────────────────────────────────────────
|
// ── export: la capa de transparencia (D5) ───────────────────────────────────────────────────────
|
||||||
//
|
//
|
||||||
// El objetivo es el poder de Bedrock Linux —apps de cualquier «stratum» disponibles en todos
|
// El objetivo es el poder de Bedrock Linux —apps de cualquier «stratum» disponibles en todos
|
||||||
@@ -1910,6 +2062,46 @@ mod tests {
|
|||||||
assert_eq!(home_de(&dir, &tree, 0), "/root");
|
assert_eq!(home_de(&dir, &tree, 0), "/root");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn la_poda_no_toca_una_imagen_con_instancia_y_si_los_restos() {
|
||||||
|
let d = tempfile::tempdir().unwrap();
|
||||||
|
let root = d.path();
|
||||||
|
let usada = "1".repeat(64);
|
||||||
|
let huerfana = "2".repeat(64);
|
||||||
|
imagen_falsa(root, &usada);
|
||||||
|
imagen_falsa(root, &huerfana);
|
||||||
|
create(root, "viva", &usada, None).unwrap();
|
||||||
|
// Un resto de pull a medias: lleva punto justo para no confundirse con una imagen.
|
||||||
|
std::fs::create_dir_all(root.join("images/.3333.tmp")).unwrap();
|
||||||
|
|
||||||
|
// Simulacro: NO borra nada. Es la diferencia entre una poda y un accidente.
|
||||||
|
prune(root, false, false).unwrap();
|
||||||
|
assert!(root.join("images").join(&huerfana).exists(), "el simulacro borró");
|
||||||
|
|
||||||
|
prune(root, false, true).unwrap();
|
||||||
|
assert!(root.join("images").join(&usada).exists(), "borró una imagen EN USO");
|
||||||
|
assert!(!root.join("images").join(&huerfana).exists(), "no borró la huérfana");
|
||||||
|
assert!(!root.join("images/.3333.tmp").exists(), "no borró el resto");
|
||||||
|
// La instancia sigue entera: la poda no la toca sin --upper.
|
||||||
|
assert!(instance_dir(root, "viva").join(INSTANCE_MANIFEST).exists());
|
||||||
|
assert!(instance_dir(root, "viva").join("upper").exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn con_upper_la_capa_se_va_pero_la_instancia_queda_usable() {
|
||||||
|
let d = tempfile::tempdir().unwrap();
|
||||||
|
let root = d.path();
|
||||||
|
let sha = "4".repeat(64);
|
||||||
|
imagen_falsa(root, &sha);
|
||||||
|
create(root, "i", &sha, None).unwrap();
|
||||||
|
std::fs::write(instance_dir(root, "i").join("upper/estado"), "x").unwrap();
|
||||||
|
prune(root, true, true).unwrap();
|
||||||
|
assert!(!instance_dir(root, "i").join("upper/estado").exists(), "el upper no es caché");
|
||||||
|
// Y queda utilizable: sin upper/ y work/ la instancia no vuelve a arrancar.
|
||||||
|
assert!(instance_dir(root, "i").join("upper").is_dir());
|
||||||
|
assert!(instance_dir(root, "i").join("work").is_dir());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sha_mal_formado_falla_antes_de_tocar_la_red() {
|
fn sha_mal_formado_falla_antes_de_tocar_la_red() {
|
||||||
assert!(normalize_sha256("abc").is_err());
|
assert!(normalize_sha256("abc").is_err());
|
||||||
|
|||||||
@@ -150,6 +150,28 @@ de nosotros la fuente correspondiente**. Hoy las recetas apuntan a URLs de terce
|
|||||||
Acá estamos inusualmente bien parados: cada receta pinea `tarball` + `sha256`, así que el espejo es
|
Acá estamos inusualmente bien parados: cada receta pinea `tarball` + `sha256`, así que el espejo es
|
||||||
mecánico **y encima verificable**. Pero tiene que existir **antes de la primera descarga**.
|
mecánico **y encima verificable**. Pero tiene que existir **antes de la primera descarga**.
|
||||||
|
|
||||||
|
### Imágenes ajenas (qorpa): fuera del catálogo, y por escrito
|
||||||
|
Desde el [ADR 0015](adr/0015-imagenes-ajenas.md) hay una segunda cadena de suministro: rootfs de
|
||||||
|
otras distros que corren enjaulados. **No entran en el catálogo publicable ni en el reporte de
|
||||||
|
licencias, y la razón no es pereza: no podemos enumerarlas.** Un `dnf install` o un `pacman -S`
|
||||||
|
dentro de una instancia trae paquetes que nadie declaró acá, y afirmar una licencia sobre eso sería
|
||||||
|
inventarla.
|
||||||
|
|
||||||
|
Lo que sí se hace, y alcanza:
|
||||||
|
- **Se cuentan aparte, en clase `ajeno`** (`docs/state/qorpa-ajenos.toml` → `build-state.json` →
|
||||||
|
«187/188 listo … + 1 ajenas»). El riesgo real de todo el ADR es que en seis meses alguien las
|
||||||
|
cuente como corpus; en otra clase no se pueden contar mal.
|
||||||
|
- **La licencia la pone quien las publica**, que no somos nosotros: el usuario trae la imagen por
|
||||||
|
URL + sha256 y la relación es con esa distro.
|
||||||
|
- **Si algún día se espejan** —el mecanismo lo permite gratis, porque son cuerpo inmutable
|
||||||
|
direccionado por contenido (ADR 0014)— ahí sí hay que mirar licencia **y marca** antes, igual que
|
||||||
|
con Firefox: redistribuir un rootfs de Ubuntu sin modificar suele estar permitido, pero la
|
||||||
|
política de marca es cosa aparte y **no es una pregunta técnica**.
|
||||||
|
|
||||||
|
⚠️ Y el corolario que hay que escribir donde se lea: **el claim «hammer reproduce bit a bit» hay que
|
||||||
|
acotarlo** desde el día que exista una instancia — *el sistema base reproduce; las instancias ajenas
|
||||||
|
no, y se declaran como tales*. Sin eso, la cultura de números honestos se erosiona sola.
|
||||||
|
|
||||||
### Marcas
|
### Marcas
|
||||||
Redistribuir Firefox con su nombre y logo exige cumplir la política de marca de Mozilla (por eso
|
Redistribuir Firefox con su nombre y logo exige cumplir la política de marca de Mozilla (por eso
|
||||||
Debian tuvo Iceweasel). Decidir por adelantado: cumplir o rebrandear. **No bloquea el primer
|
Debian tuvo Iceweasel). Decidir por adelantado: cumplir o rebrandear. **No bloquea el primer
|
||||||
|
|||||||
@@ -481,7 +481,13 @@ Se escriben acá para que no se descubran en producción.
|
|||||||
**Lo que NO se pudo, y por qué:** la máquina no tiene ninguna sesión gráfica (`/dev/dri` sí,
|
**Lo que NO se pudo, y por qué:** la máquina no tiene ninguna sesión gráfica (`/dev/dri` sí,
|
||||||
socket Wayland no), así que Steam no dibuja; y el runtime *sniper* sólo se baja al instalar un
|
socket Wayland no), así que Steam no dibuja; y el runtime *sniper* sólo se baja al instalar un
|
||||||
juego, lo que exige credenciales. **pressure-vessel con un juego real sigue sin ejercitarse.**
|
juego, lo que exige credenciales. **pressure-vessel con un juego real sigue sin ejercitarse.**
|
||||||
7. Poda (`hammer qorpa prune`) y renglón en SDD 20 sobre licencias.
|
7. ✅ **HECHO 2026-09-03.** `hammer qorpa prune` + el renglón en
|
||||||
|
[SDD 20](../20-catalogo-publicable-y-completa.md#imágenes-ajenas-qorpa-fuera-del-catálogo-y-por-escrito).
|
||||||
|
Poda restos de pulls a medias, imágenes que ninguna instancia usa (se re-traen por digest: es la
|
||||||
|
propiedad que da el pin) y, con `--upper`, las capas mutables. **Sin `--yes` es un simulacro**, y
|
||||||
|
tras borrar **comprueba que el directorio se fue**: la cicatriz de `store-gc`, que reportaba
|
||||||
|
borrados que no ocurrían. Los tamaños de un árbol con directorios ilegibles se marcan con `≥`,
|
||||||
|
porque un número silenciosamente bajo es peor que ninguno cuando con él se decide borrar.
|
||||||
8. Proxy filtrante de Wayland — ticket propio, el más valioso de la lista.
|
8. Proxy filtrante de Wayland — ticket propio, el más valioso de la lista.
|
||||||
|
|
||||||
## Nombre — ADOPTADO 2026-09-03
|
## Nombre — ADOPTADO 2026-09-03
|
||||||
|
|||||||
Reference in New Issue
Block a user