Etapa F refinamientos: uninstall poda dirs vacíos + install --require-signed
Dos bordes ásperos de la paquetería: - uninstall ahora retira los directorios que quedaron VACÍOS por el borrado (rmdir de abajo arriba; remove_dir sólo borra dirs vacíos ⇒ se detiene solo al toparse con contenido de otro paquete). Antes dejaba /usr/bin, etc. huérfanos. - `install --require-signed`: modo estricto que ABORTA si el release no está firmado por una clave confiada (Unsigned o UnknownKey ⇒ error). No basta con que el .swm reproduzca: exige autoría verificada del catálogo. Default off (no rompe flujos sin firma). Validado E2E: uninstall bwrap poda 4 dirs; --require-signed aborta sin firma y procede con release trusted. 31 suites verde. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -230,6 +230,10 @@ enum Cmd {
|
||||
/// Base de datos de instalados donde registrar el paquete (para `uninstall`).
|
||||
#[arg(long, default_value = hammer_core::installed::DEFAULT_DB)]
|
||||
db: PathBuf,
|
||||
/// Modo estricto: abortar si el release del repo no está firmado por una clave confiada
|
||||
/// (no basta con que el `.swm` reproduzca — exige autoría del catálogo verificada).
|
||||
#[arg(long)]
|
||||
require_signed: bool,
|
||||
},
|
||||
/// [Etapa F] Desinstala un paquete: borra los ficheros que registró (refcount: respeta los
|
||||
/// que otro paquete instalado también aporta) y lo quita de la DB de instalados.
|
||||
@@ -791,6 +795,7 @@ fn main() -> anyhow::Result<()> {
|
||||
trust,
|
||||
state_root,
|
||||
db,
|
||||
require_signed,
|
||||
} => {
|
||||
run_install(
|
||||
&cli.store,
|
||||
@@ -802,6 +807,7 @@ fn main() -> anyhow::Result<()> {
|
||||
trust.as_deref(),
|
||||
state_root.as_deref(),
|
||||
&db,
|
||||
require_signed,
|
||||
)?;
|
||||
}
|
||||
Cmd::Uninstall { name, db } => run_uninstall(&name, &db)?,
|
||||
@@ -1426,6 +1432,7 @@ fn run_install(
|
||||
trust: Option<&std::path::Path>,
|
||||
state_root: Option<&std::path::Path>,
|
||||
db_path: &std::path::Path,
|
||||
require_signed: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
let source = RepoSource::parse(repo);
|
||||
let index = source.read_index()?;
|
||||
@@ -1439,12 +1446,20 @@ fn run_install(
|
||||
match index.verify_signature(&store) {
|
||||
hammer_core::SigStatus::Trusted { by } => eprintln!("release: trusted (by {by})"),
|
||||
hammer_core::SigStatus::UnknownKey { by } => {
|
||||
eprintln!("release: unknown-key (by {by}) — autoría del catálogo no confiada")
|
||||
eprintln!("release: unknown-key (by {by}) — autoría del catálogo no confiada");
|
||||
if require_signed {
|
||||
anyhow::bail!("--require-signed: el release está firmado por '{by}', clave no confiada en {}", trust_dir.display());
|
||||
}
|
||||
}
|
||||
hammer_core::SigStatus::BadSig { by, reason } => {
|
||||
anyhow::bail!("release: BAD-SIG (by {by}): {reason} — el índice fue manipulado")
|
||||
}
|
||||
hammer_core::SigStatus::Unsigned => eprintln!("release: sin firmar"),
|
||||
hammer_core::SigStatus::Unsigned => {
|
||||
eprintln!("release: sin firmar");
|
||||
if require_signed {
|
||||
anyhow::bail!("--require-signed: el repo no tiene firma de release (firmalo con `hammer repo sign`)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1592,6 +1607,7 @@ fn run_uninstall(name: &str, db_path: &std::path::Path) -> anyhow::Result<()> {
|
||||
};
|
||||
|
||||
let (mut removed, mut kept) = (0usize, 0usize);
|
||||
let mut touched_dirs: Vec<PathBuf> = Vec::new();
|
||||
for f in &pkg.files {
|
||||
// Refcount: no borramos un fichero que otro paquete instalado también aporta.
|
||||
if idb.owned_by_others(name, f) {
|
||||
@@ -1599,17 +1615,40 @@ fn run_uninstall(name: &str, db_path: &std::path::Path) -> anyhow::Result<()> {
|
||||
continue;
|
||||
}
|
||||
match std::fs::remove_file(f) {
|
||||
Ok(()) => removed += 1,
|
||||
Ok(()) => {
|
||||
removed += 1;
|
||||
if let Some(parent) = std::path::Path::new(f).parent() {
|
||||
touched_dirs.push(parent.to_path_buf());
|
||||
}
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} // ya no estaba
|
||||
Err(e) => eprintln!(" aviso: no pude borrar {f}: {e}"),
|
||||
}
|
||||
}
|
||||
// Recoge los directorios que quedaron vacíos por el borrado (y sus padres, hacia arriba):
|
||||
// `remove_dir` sólo borra un dir VACÍO ⇒ se detiene solo al toparse con uno que aún tiene
|
||||
// contenido de otros paquetes. Procesamos de más profundo a menos para vaciar de abajo arriba.
|
||||
let mut pruned = 0usize;
|
||||
touched_dirs.sort_by_key(|d| std::cmp::Reverse(d.components().count()));
|
||||
touched_dirs.dedup();
|
||||
for dir in touched_dirs {
|
||||
let mut cur = dir;
|
||||
while std::fs::remove_dir(&cur).is_ok() {
|
||||
pruned += 1;
|
||||
match cur.parent() {
|
||||
Some(p) if !p.as_os_str().is_empty() => cur = p.to_path_buf(),
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
idb.remove(name);
|
||||
idb.save(db_path)?;
|
||||
eprintln!(
|
||||
"desinstalado {name} {} — {removed} fichero(s) borrado(s){}",
|
||||
"desinstalado {name} {} — {removed} fichero(s) borrado(s){}{}",
|
||||
pkg.version,
|
||||
if kept > 0 { format!(", {kept} conservado(s) (los aporta otro paquete)") } else { String::new() }
|
||||
if kept > 0 { format!(", {kept} conservado(s) (los aporta otro paquete)") } else { String::new() },
|
||||
if pruned > 0 { format!(", {pruned} dir(s) vacío(s) retirado(s)") } else { String::new() },
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user