deb: change ui/ux of pkh deb
CI / build (push) Successful in 2m54s
CI / test (push) Skipped
CI / snap (push) Failing after 12s

This commit is contained in:
2026-08-22 22:26:20 +02:00
parent e2d201d815
commit e5adf600c3
16 changed files with 1899 additions and 199 deletions
+126 -3
View File
@@ -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]