deb: change ui/ux of pkh deb
This commit is contained in:
+126
-3
@@ -13,11 +13,16 @@
|
||||
//! **stale download lockfiles** (`~/.cache/pkh/*.lock`).
|
||||
//! - **The shared apt keyring directory** (`/tmp/pkh-keyrings`), used by
|
||||
//! mmdebstrap runs.
|
||||
//! - **Build logs** (`~/.cache/pkh/logs/deb-*.log`) written by `pkh deb`. By
|
||||
//! default only logs beyond a small retention window (the newest
|
||||
//! [`KEEP_LOGS`] are kept) are removed; pass [`PruneOptions::all`] to remove
|
||||
//! them all.
|
||||
//!
|
||||
//! The [`prune()`] function discovers and removes all of the above. By
|
||||
//! default it removes everything that is cheap to regenerate (residual
|
||||
//! directories, keyrings, stale lockfiles); pass [`PruneOptions::all`] to also
|
||||
//! discard the cached chroot tarballs, which are expensive to re-download.
|
||||
//! directories, keyrings, stale lockfiles, old build logs); pass
|
||||
//! [`PruneOptions::all`] to also discard the cached chroot tarballs and all
|
||||
//! build logs, which are expensive to re-create.
|
||||
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -54,6 +59,9 @@ impl PruneReport {
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of newest build logs kept when `--all` is not passed.
|
||||
pub(crate) const KEEP_LOGS: usize = 10;
|
||||
|
||||
/// One discovered artifact that prune can act on.
|
||||
#[derive(Debug, Clone)]
|
||||
enum Artifact {
|
||||
@@ -65,6 +73,8 @@ enum Artifact {
|
||||
LockFile(PathBuf),
|
||||
/// A cached chroot tarball.
|
||||
Tarball(PathBuf),
|
||||
/// A build log file under `<cache>/logs`.
|
||||
LogFile(PathBuf),
|
||||
}
|
||||
|
||||
impl Artifact {
|
||||
@@ -73,7 +83,8 @@ impl Artifact {
|
||||
Artifact::TempDir(p)
|
||||
| Artifact::KeyringDir(p)
|
||||
| Artifact::LockFile(p)
|
||||
| Artifact::Tarball(p) => p,
|
||||
| Artifact::Tarball(p)
|
||||
| Artifact::LogFile(p) => p,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +94,7 @@ impl Artifact {
|
||||
Artifact::KeyringDir(_) => "apt keyring cache",
|
||||
Artifact::LockFile(_) => "stale lockfile",
|
||||
Artifact::Tarball(_) => "cached chroot tarball",
|
||||
Artifact::LogFile(_) => "build log",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -197,9 +209,45 @@ fn discover_artifacts(temp_dir: &Path, cache_dir: Option<&Path>) -> Vec<Artifact
|
||||
}
|
||||
}
|
||||
|
||||
// Build logs live in a dedicated subdirectory of the cache dir
|
||||
if let Some(cache) = cache_dir {
|
||||
let logs_dir = cache.join("logs");
|
||||
if let Ok(entries) = fs::read_dir(&logs_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
let name = entry.file_name().to_string_lossy().into_owned();
|
||||
if path.is_file() && name.starts_with("deb-") && name.ends_with(".log") {
|
||||
artifacts.push(Artifact::LogFile(path));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
artifacts
|
||||
}
|
||||
|
||||
/// Compute the set of build log paths that should be removed for the given
|
||||
/// options: all of them with `--all`, otherwise every log except the
|
||||
/// [`KEEP_LOGS`] newest ones (filenames embed timestamps, so lexicographic
|
||||
/// order is chronological).
|
||||
fn removable_logs(artifacts: &[Artifact], all: bool) -> std::collections::HashSet<&Path> {
|
||||
let mut logs: Vec<&Path> = artifacts
|
||||
.iter()
|
||||
.filter_map(|a| match a {
|
||||
Artifact::LogFile(p) => Some(p.as_path()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
logs.sort_unstable();
|
||||
|
||||
if all {
|
||||
return logs.into_iter().collect();
|
||||
}
|
||||
|
||||
let keep = KEEP_LOGS.min(logs.len());
|
||||
logs[..logs.len() - keep].iter().copied().collect()
|
||||
}
|
||||
|
||||
/// Remove a path, first attempting a direct removal and only escalating to a
|
||||
/// privileged `rm -rf` when the direct attempt fails.
|
||||
///
|
||||
@@ -284,6 +332,8 @@ pub fn prune_in(
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let removable_logs = removable_logs(&artifacts, options.all);
|
||||
|
||||
for artifact in &artifacts {
|
||||
// Cached tarballs are only removed when --all is requested: they are
|
||||
// expensive to re-download.
|
||||
@@ -291,6 +341,13 @@ pub fn prune_in(
|
||||
continue;
|
||||
}
|
||||
|
||||
// Build logs follow the retention policy computed above.
|
||||
if let Artifact::LogFile(p) = artifact
|
||||
&& !removable_logs.contains(p.as_path())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let path = artifact.path();
|
||||
log::info!("{}: {}", artifact.kind(), path.display());
|
||||
|
||||
@@ -411,24 +468,90 @@ none /tmp/other proc rw 0 0
|
||||
fs::write(cache_path.join("noble-buildd.tar.lock"), "lock").unwrap();
|
||||
fs::write(cache_path.join("stray.txt"), "ignore me").unwrap();
|
||||
|
||||
// Build logs.
|
||||
let logs_dir = cache_path.join("logs");
|
||||
fs::create_dir_all(&logs_dir).unwrap();
|
||||
fs::write(logs_dir.join("deb-hello-20260101T000000.log"), "log").unwrap();
|
||||
fs::write(logs_dir.join("not-a-build.txt"), "ignore me").unwrap();
|
||||
|
||||
let artifacts = discover_artifacts(temp_path, Some(cache_path));
|
||||
|
||||
let mut temp_dirs = 0;
|
||||
let mut keyring = false;
|
||||
let mut locks = 0;
|
||||
let mut tarballs = 0;
|
||||
let mut logs = 0;
|
||||
for a in &artifacts {
|
||||
match a {
|
||||
Artifact::TempDir(_) => temp_dirs += 1,
|
||||
Artifact::KeyringDir(_) => keyring = true,
|
||||
Artifact::LockFile(_) => locks += 1,
|
||||
Artifact::Tarball(_) => tarballs += 1,
|
||||
Artifact::LogFile(_) => logs += 1,
|
||||
}
|
||||
}
|
||||
assert_eq!(temp_dirs, 2);
|
||||
assert!(keyring);
|
||||
assert_eq!(locks, 1);
|
||||
assert_eq!(tarballs, 2);
|
||||
assert_eq!(logs, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prune_log_retention() {
|
||||
let temp = tempdir().unwrap();
|
||||
let cache = tempdir().unwrap();
|
||||
let cache_path = cache.path();
|
||||
let logs_dir = cache_path.join("logs");
|
||||
fs::create_dir_all(&logs_dir).unwrap();
|
||||
|
||||
// Create KEEP_LOGS + 3 logs; the 3 oldest should be pruned by default
|
||||
let total = KEEP_LOGS + 3;
|
||||
for i in 0..total {
|
||||
fs::write(
|
||||
logs_dir.join(format!("deb-pkg-20260101T{:06}.log", i)),
|
||||
"log",
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let report = prune_in(
|
||||
temp.path(),
|
||||
Some(cache_path),
|
||||
PruneOptions {
|
||||
dry_run: false,
|
||||
all: false,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let removed_logs = report
|
||||
.removed
|
||||
.iter()
|
||||
.filter(|p| p.starts_with(&logs_dir))
|
||||
.count();
|
||||
assert_eq!(removed_logs, 3);
|
||||
|
||||
let remaining = fs::read_dir(&logs_dir).unwrap().count();
|
||||
assert_eq!(remaining, KEEP_LOGS);
|
||||
|
||||
// --all removes every remaining log
|
||||
let report = prune_in(
|
||||
temp.path(),
|
||||
Some(cache_path),
|
||||
PruneOptions {
|
||||
dry_run: false,
|
||||
all: true,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let removed_logs = report
|
||||
.removed
|
||||
.iter()
|
||||
.filter(|p| p.starts_with(&logs_dir))
|
||||
.count();
|
||||
assert_eq!(removed_logs, KEEP_LOGS);
|
||||
assert_eq!(fs::read_dir(&logs_dir).unwrap().count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user