prune,ui,docs: color the digest, add -f/--force and a sizing spinner
- -f/--force skips the size pass and the confirmation prompt: remove
immediately, reporting counts but no sizes. The fastest cleanup
path; --dry-run still previews.
- The default path shows the grouped digest and asks before removing
(the raw-mode yes/no prompt; non-interactive sessions keep their
headless path, so scripts reach for --force).
- Sizes are computed under a spinner ("Computing sizes of
directories to prune..."), and the digest colors the counts (bold)
and the sizes (cyan) on terminals only.
This commit is contained in:
+117
-42
@@ -60,9 +60,14 @@ pub struct PruneOptions {
|
||||
/// When true, do not remove anything; only report what would be removed.
|
||||
pub dry_run: bool,
|
||||
/// When true, also remove cached chroot tarballs (which are expensive to
|
||||
/// re-download). Without this, tarballs are kept and only stale lockfiles
|
||||
/// are removed from the cache directory.
|
||||
/// re-download) and recent build records with their staged trees.
|
||||
/// Without this, only artifacts beyond their retention windows are
|
||||
/// removed.
|
||||
pub all: bool,
|
||||
/// When true, skip the size computation and the confirmation prompt:
|
||||
/// remove immediately, reporting counts but no sizes. A dry run
|
||||
/// still wins over this.
|
||||
pub force: bool,
|
||||
}
|
||||
|
||||
/// Summary of a prune run.
|
||||
@@ -769,7 +774,9 @@ pub fn prune_in(
|
||||
cache_dir: Option<&Path>,
|
||||
options: PruneOptions,
|
||||
) -> Result<PruneReport, Box<dyn std::error::Error>> {
|
||||
prune_in_roots(temp_dir, cache_dir, None, None, None, options)
|
||||
Ok(apply(plan_in_roots(
|
||||
temp_dir, cache_dir, None, None, None, options,
|
||||
)))
|
||||
}
|
||||
|
||||
/// Execute the prune operation against explicit roots.
|
||||
@@ -786,6 +793,46 @@ pub fn prune_in_roots(
|
||||
apt_cache_dir: Option<&Path>,
|
||||
options: PruneOptions,
|
||||
) -> Result<PruneReport, Box<dyn std::error::Error>> {
|
||||
Ok(apply(plan_in_roots(
|
||||
temp_dir,
|
||||
cache_dir,
|
||||
sessions_dir,
|
||||
tests_dir,
|
||||
apt_cache_dir,
|
||||
options,
|
||||
)))
|
||||
}
|
||||
|
||||
/// A plan produced by [`plan()`] or [`plan_in_roots()`]: the digest
|
||||
/// report — groups with sizes, unless [`PruneOptions::force`] skipped
|
||||
/// the sizing — and the artifacts [`apply()`] will remove.
|
||||
pub struct PrunePlan {
|
||||
report: PruneReport,
|
||||
actionable: Vec<(Artifact, u64)>,
|
||||
}
|
||||
|
||||
impl PrunePlan {
|
||||
/// The digest: what would be removed, grouped by kind with sizes
|
||||
/// (counts only with [`PruneOptions::force`]).
|
||||
pub fn report(&self) -> &PruneReport {
|
||||
&self.report
|
||||
}
|
||||
}
|
||||
|
||||
/// Discover and size the prunable artifacts against the given temp and
|
||||
/// cache roots, removing nothing. This is the testable core of [`plan()`]:
|
||||
/// callers pass in the temp directory and optional cache directory to
|
||||
/// scan. Build sessions are deliberately out of scope here — tests scan
|
||||
/// controlled roots, and a concurrent test suite must never touch the
|
||||
/// live sessions under the real sessions root. Pair with [`apply()`].
|
||||
pub fn plan_in_roots(
|
||||
temp_dir: &Path,
|
||||
cache_dir: Option<&Path>,
|
||||
sessions_dir: Option<&Path>,
|
||||
tests_dir: Option<&Path>,
|
||||
apt_cache_dir: Option<&Path>,
|
||||
options: PruneOptions,
|
||||
) -> PrunePlan {
|
||||
let mut artifacts = discover_artifacts(temp_dir, cache_dir);
|
||||
if let Some(sessions_dir) = sessions_dir {
|
||||
artifacts.extend(discover_sessions(sessions_dir, options.all));
|
||||
@@ -804,8 +851,6 @@ pub fn prune_in_roots(
|
||||
|
||||
let removable_logs = removable_logs(&artifacts, options.all);
|
||||
|
||||
// The actionable subset with sizes measured BEFORE anything is
|
||||
// removed: a removed tree has no size left to report.
|
||||
let mut filtered: Vec<&Artifact> = Vec::new();
|
||||
for artifact in &artifacts {
|
||||
// Cached tarballs are only removed when --all is requested: they are
|
||||
@@ -823,17 +868,53 @@ pub fn prune_in_roots(
|
||||
|
||||
filtered.push(artifact);
|
||||
}
|
||||
let sizes = size_artifacts(&filtered);
|
||||
let actionable: Vec<(&Artifact, u64)> = filtered.iter().copied().zip(sizes).collect();
|
||||
|
||||
for (artifact, _) in &actionable {
|
||||
let path = artifact.path();
|
||||
log::debug!("{}: {}", artifact.kind(), path.display());
|
||||
// Sizes are measured during the plan (a removed tree has no size left
|
||||
// to report) — unless --force skipped them: the plan then keeps the
|
||||
// counts and loses the sizes, for a fast no-questions cleanup.
|
||||
let sizes = if options.force {
|
||||
vec![0; filtered.len()]
|
||||
} else {
|
||||
size_artifacts(&filtered)
|
||||
};
|
||||
|
||||
if options.dry_run {
|
||||
report.removed.push(path.to_path_buf());
|
||||
continue;
|
||||
let mut actionable: Vec<(Artifact, u64)> = Vec::new();
|
||||
for (artifact, bytes) in filtered.iter().zip(sizes) {
|
||||
report.removed.push(artifact.path().to_path_buf());
|
||||
actionable.push(((*artifact).clone(), bytes));
|
||||
// Group by kind in first-seen order.
|
||||
if let Some(group) = report
|
||||
.groups
|
||||
.iter_mut()
|
||||
.find(|group| group.kind == artifact.kind())
|
||||
{
|
||||
group.count += 1;
|
||||
group.bytes += bytes;
|
||||
} else {
|
||||
report.groups.push(PruneGroup {
|
||||
kind: artifact.kind(),
|
||||
count: 1,
|
||||
bytes,
|
||||
});
|
||||
}
|
||||
}
|
||||
report.total_bytes = report.groups.iter().map(|group| group.bytes).sum();
|
||||
|
||||
PrunePlan { report, actionable }
|
||||
}
|
||||
|
||||
/// Execute a plan: remove the planned artifacts (unmounting the
|
||||
/// filesystems under residual directories and session roots first) and
|
||||
/// report what was removed and what failed. A dry-run plan removes
|
||||
/// nothing and returns its digest unchanged.
|
||||
pub fn apply(plan: PrunePlan) -> PruneReport {
|
||||
if plan.report.dry_run {
|
||||
return plan.report;
|
||||
}
|
||||
let mut report = plan.report;
|
||||
report.removed = Vec::new();
|
||||
for (artifact, _) in &plan.actionable {
|
||||
let path = artifact.path();
|
||||
|
||||
// Residual build directories may hold bind-mounted /proc and overlay
|
||||
// filesystems that must be unmounted before the directory can be
|
||||
@@ -854,39 +935,15 @@ pub fn prune_in_roots(
|
||||
Err(e) => report.failed.push((path.to_path_buf(), e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
// Group by kind in first-seen order.
|
||||
for (artifact, bytes) in &actionable {
|
||||
if let Some(group) = report
|
||||
.groups
|
||||
.iter_mut()
|
||||
.find(|group| group.kind == artifact.kind())
|
||||
{
|
||||
group.count += 1;
|
||||
group.bytes += bytes;
|
||||
} else {
|
||||
report.groups.push(PruneGroup {
|
||||
kind: artifact.kind(),
|
||||
count: 1,
|
||||
bytes: *bytes,
|
||||
});
|
||||
}
|
||||
}
|
||||
report.total_bytes = report.groups.iter().map(|group| group.bytes).sum();
|
||||
|
||||
Ok(report)
|
||||
report
|
||||
}
|
||||
|
||||
/// Execute the prune operation, removing residual pkh artifacts.
|
||||
///
|
||||
/// Convenience wrapper around [`prune_in()`] that uses the system temp
|
||||
/// directory and the pkh cache directory. See the
|
||||
/// [module documentation](self) for what is removed. Returns a [`PruneReport`]
|
||||
/// describing what was removed (or, in dry-run mode, what would be removed).
|
||||
pub fn prune(options: PruneOptions) -> Result<PruneReport, Box<dyn std::error::Error>> {
|
||||
/// Plan the prune operation against the real roots: the digest without
|
||||
/// removing anything. Pair with [`apply()`].
|
||||
pub fn plan(options: PruneOptions) -> PrunePlan {
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let cache = cache_dir();
|
||||
prune_in_roots(
|
||||
plan_in_roots(
|
||||
&temp_dir,
|
||||
cache.as_deref(),
|
||||
Some(&crate::deb::session::sessions_root()),
|
||||
@@ -896,6 +953,16 @@ pub fn prune(options: PruneOptions) -> Result<PruneReport, Box<dyn std::error::E
|
||||
)
|
||||
}
|
||||
|
||||
/// Execute the prune operation, removing residual pkh artifacts.
|
||||
///
|
||||
/// Convenience wrapper that uses the system temp directory and the pkh
|
||||
/// cache directory. See the [module documentation](self) for what is
|
||||
/// removed. Returns a [`PruneReport`] describing what was removed (or, in
|
||||
/// dry-run mode, what would be removed).
|
||||
pub fn prune(options: PruneOptions) -> Result<PruneReport, Box<dyn std::error::Error>> {
|
||||
Ok(apply(plan(options)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -1133,6 +1200,7 @@ none /tmp/other proc rw 0 0
|
||||
PruneOptions {
|
||||
dry_run: false,
|
||||
all: false,
|
||||
force: false,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1160,6 +1228,7 @@ none /tmp/other proc rw 0 0
|
||||
PruneOptions {
|
||||
dry_run: false,
|
||||
all: true,
|
||||
force: false,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1208,6 +1277,7 @@ none /tmp/other proc rw 0 0
|
||||
PruneOptions {
|
||||
dry_run: false,
|
||||
all: false,
|
||||
force: false,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1258,6 +1328,7 @@ none /tmp/other proc rw 0 0
|
||||
PruneOptions {
|
||||
dry_run: true,
|
||||
all: false,
|
||||
force: false,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1331,6 +1402,7 @@ none /tmp/other proc rw 0 0
|
||||
PruneOptions {
|
||||
dry_run: false,
|
||||
all: false,
|
||||
force: false,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1381,6 +1453,7 @@ none /tmp/other proc rw 0 0
|
||||
PruneOptions {
|
||||
dry_run: false,
|
||||
all: true,
|
||||
force: false,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1430,6 +1503,7 @@ none /tmp/other proc rw 0 0
|
||||
PruneOptions {
|
||||
dry_run: false,
|
||||
all: false,
|
||||
force: false,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1448,6 +1522,7 @@ none /tmp/other proc rw 0 0
|
||||
PruneOptions {
|
||||
dry_run: true,
|
||||
all: false,
|
||||
force: false,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
Reference in New Issue
Block a user