deb: do not retain staged trees over the retention size cap

The per-target retention kept the newest five staged trees however
large they are: five carried kernel trees are three-digit GiB, most of
it compiled objects whose resume value does not scale with their size.

A staged tree larger than the retention cap (a user setting, see
below) is now dropped at the end of its own build — the record
degrades to metadata-only and the next build re-stages from scratch —
and the retention sweep also degrades older oversized trees, covering
records kept by older pkh versions and interrupted builds. The cap of
0 disables the size rule; the count cap is unchanged.

The cap is the first setting of a user configuration file,
~/.config/pkh/config.json (the directory already holds contexts.json):
missing file and missing keys mean defaults, a corrupt file is backed
up and the defaults apply, and PKH_STAGED_MAXSIZE overrides the file.
The default is 20G, the scale where a carried kernel tree stops
earning its disk.

Every retained tree's size is measured once at retention (the same
concurrent, mount-crossing-safe walker the prune report uses) and
stored in the record manifest, so sweeps reuse it instead of re-walking
the trees on every build.
This commit is contained in:
2026-10-03 18:28:12 +02:00
parent ed6d88008d
commit 93943dcce0
5 changed files with 407 additions and 49 deletions
+102
View File
@@ -0,0 +1,102 @@
//! User settings: the optional `config.json` in the pkh configuration
//! directory (the same `~/.config/pkh/` that holds `contexts.json`),
//! its defaults, and the environment overrides.
//!
//! A missing file carries the defaults — the file is written only when
//! a user creates it, never generated. A file that cannot be read or
//! parsed must not take the whole program down: like the context
//! configuration, it is backed up to `<path>.bak` (best effort) so a
//! later save cannot silently destroy it, and the defaults apply. Every
//! field is individually defaulted (`#[serde(default)]`), so any
//! hand-written file may carry a subset of the settings.
use serde::Deserialize;
use std::path::PathBuf;
/// The pkh configuration directory (the one holding `contexts.json`
/// and `config.json`).
pub fn config_dir() -> Option<PathBuf> {
directories::ProjectDirs::from("com", "pkh", "pkh").map(|dirs| dirs.config_dir().to_path_buf())
}
/// The settings file in [`config_dir`].
fn config_path() -> Option<PathBuf> {
config_dir().map(|dir| dir.join("config.json"))
}
/// The user-adjustable settings, as stored in `config.json`. New fields
/// must default — the file always may carry a subset.
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(default)]
pub struct Settings {
/// The retention size cap of a staged build tree: trees larger than
/// this are not retained after a build (the record degrades to
/// metadata-only), and existing oversized trees degrade at the next
/// build's retention. A size string (`20G`, `512M`, bytes); `0`
/// disables the cap.
pub staged_maxsize: String,
}
/// The staged-tree retention cap in bytes: `PKH_STAGED_MAXSIZE` overrides
/// the settings file, which overrides the default (20 GiB — the scale
/// where a carried kernel tree stops earning its disk).
pub fn staged_maxsize() -> u64 {
if let Ok(text) = std::env::var("PKH_STAGED_MAXSIZE")
&& let Some(bytes) = crate::utils::size::parse_size(&text)
{
return bytes;
}
file_staged_maxsize()
}
/// The settings-file layer of [`staged_maxsize`]: the stored value
/// parsed, the default when unset, unparseable, or the file is missing
/// or corrupt (a corrupt file is backed up, as with the context
/// configuration).
fn file_staged_maxsize() -> u64 {
const DEFAULT: u64 = 20 * 1024 * 1024 * 1024;
let Some(path) = config_path() else {
return DEFAULT;
};
let Ok(text) = std::fs::read_to_string(&path) else {
return DEFAULT;
};
let settings: Settings = match serde_json::from_str(&text) {
Ok(settings) => settings,
Err(e) => {
log::error!(
"Settings file {} is corrupt ({e}); using the defaults",
path.display()
);
backup(&path);
return DEFAULT;
}
};
crate::utils::size::parse_size(&settings.staged_maxsize).unwrap_or_else(|| {
log::warn!(
"Settings file {} carries an unparseable staged_maxsize \
'{}'; using the default",
path.display(),
settings.staged_maxsize
);
DEFAULT
})
}
/// Back up a corrupt settings file next to itself (best effort), so a
/// later save cannot silently destroy its content.
fn backup(path: &std::path::Path) {
let mut os = path.as_os_str().to_os_string();
os.push(".bak");
let backup_path = PathBuf::from(os);
match std::fs::copy(path, &backup_path) {
Ok(_) => log::warn!(
"Corrupt settings file backed up to {}",
backup_path.display()
),
Err(e) => log::warn!(
"Could not back up corrupt settings file to {}: {e}",
backup_path.display()
),
}
}