deb: shrink records to upper, journal and log; keep metadata for every build

Records carried a full chroot each, and space turned out to be the hard
constraint: five records per target multiplied by a multi-GB chroot does
not fit. The chroot is now per-run scratch — every build extracts a
fresh one from the sha-verified mmdebstrap tarball cache into its
record's chroot/ dir and removes it at the end (success, failure and
interrupt included); resume_chroot, the .pkh-ready probe and
chroot_is_usable are gone, and crashed-run leftovers are swept when the
next run of that identity starts.

What a record keeps instead: manifest.json (renamed, now with the
tarball sha it used, a kept_upper flag and a tree-only journal — the
environment phases rerun in every fresh chroot), a log/build.log full
tee of the build output (kept for EVERY build; plain runs become
metadata-only records, so keep-iff-incremental now applies to the
staged tree alone), and — for --incremental runs — tree-upper/ with its
host-files.list. The fork gate carries a base's staged tree only while
its recorded tarball sha still matches the cache and the changelog
version is unchanged; a fork candidate is any record with a staged tree.

The keep-last-5 cap now applies to staged trees: a sixth retention
degrades the oldest record to metadata-only instead of deleting it.
New `pkh deb log [-f] [<id>]` prints (or follows) a record's build log,
and `pkh deb list` shows the staged-tree state and plain-run records
too. README rewritten around the shrunk model.
This commit is contained in:
2026-09-29 14:00:34 +02:00
parent 81f0d52f85
commit dd2634abe3
7 changed files with 1287 additions and 946 deletions
+25 -57
View File
@@ -27,35 +27,12 @@ fn cap<'a>(
cmd
}
/// The session journal stamps this build works with: the input stamps of
/// the patch and build-dependency phases, computed from the host tree and
/// the resolved selectors.
struct JournalStamps {
patches: Option<String>,
control: Option<String>,
}
/// Compute the journal stamps for a session build; `None` without one.
fn journal_stamps(
session: &session::Session,
arch: &str,
series: &str,
pocket: Option<&str>,
cross: bool,
ppa: &[String],
inject_packages: &[String],
) -> JournalStamps {
/// The input stamp of the patch phase, computed from the host tree: the
/// only tree phase the record journal tracks (the environment phases
/// rerun in every fresh chroot and are not recorded).
fn patch_stamp(session: &session::Session) -> Option<String> {
let host_tree = PathBuf::from(session.manifest().host_tree);
let selectors = format!(
"{arch}\n{cross}\n{series}\n{}\n{}\n{}",
pocket.unwrap_or(""),
ppa.join(","),
inject_packages.join(",")
);
JournalStamps {
patches: session::patches_stamp(&host_tree),
control: session::control_stamp(&host_tree, &selectors),
}
session::patches_stamp(&host_tree)
}
#[allow(clippy::too_many_arguments)]
@@ -74,13 +51,12 @@ pub async fn build(
view: &dyn BuildView,
jobs: Option<usize>,
session: Option<&session::Session>,
resume: bool,
build_resume: bool,
) -> Result<Vec<PathBuf>, Box<dyn Error>> {
let sink: Option<Arc<dyn LineSink>> = view.sink();
// Journal stamps for a session build; recording is a no-op without one.
let stamps =
session.map(|s| journal_stamps(s, arch, series, pocket, cross, ppa, inject_packages));
// Journal stamp for a record build; recording is a no-op without one.
let stamps = session.and_then(patch_stamp);
// Environment
let mut env = HashMap::<String, String>::new();
@@ -245,9 +221,6 @@ pub async fn build(
or re-run with RUST_LOG=debug for more details."
.into());
}
if let Some(s) = session {
s.record_phase("apt_update", None);
}
// Install essential packages
log::debug!("Installing essential packages for local build...");
@@ -272,9 +245,6 @@ pub async fn build(
if !status.success() {
return Err("Could not install essential packages for the build".into());
}
if let Some(s) = session {
s.record_phase("essentials", None);
}
// The package directory was resolved by the caller (the staged copy of
// the tree the user pointed at, or the name-pattern search fallback)
@@ -294,10 +264,11 @@ pub async fn build(
}
// Apply quilt patches if the package provides a patch series. On a
// resumed session the previous attempt's patches may still be applied:
// pop them first when the patch tree changed since.
let patch_stamp = stamps.as_ref().and_then(|s| s.patches.clone());
let pop_patches = resume && {
// build resuming over a carried upperdir the previous attempt's
// patches may still be applied: pop them first when the patch tree
// changed since.
let patch_stamp = stamps.clone();
let pop_patches = build_resume && {
let previous = session
.and_then(|s| s.manifest().phases.get("patches").map(|p| p.stamp.clone()))
.unwrap_or(None);
@@ -333,20 +304,17 @@ pub async fn build(
view,
&sink,
)?;
if let Some(s) = session {
s.record_phase(
"build_deps",
stamps.as_ref().and_then(|st| st.control.clone()),
);
}
// (The build-dependency phase reruns in every fresh chroot: nothing
// to journal.)
// A resumed build continues in the previous attempt's tree. Packages
// whose debian/rules keeps an input-untracked stamp cache (quirk:
// `resume_clear`, e.g. the kernels' `debian/stamps`, whose flavour
// config rule exports .config with no config prerequisite) would build
// from the previous attempt's configuration: drop the listed caches so
// the wrapper steps re-run over the incremental inner builds.
if resume {
// A build resuming over a carried upperdir continues in the previous
// attempt's tree. Packages whose `debian/rules` keeps an
// input-untracked stamp cache (quirk: `resume_clear`, e.g. the
// kernels' `debian/stamps`, whose flavour config rule exports .config
// with no config prerequisite) would build from the previous
// attempt's configuration: drop the listed caches so the wrapper
// steps re-run over the incremental inner builds.
if build_resume {
for rel in crate::quirks::get_resume_clear_paths(package, series) {
let path = Path::new(package_dir_str).join(&rel);
if !ctx.exists(&path).unwrap_or(false) {
@@ -383,10 +351,10 @@ pub async fn build(
s.record_phase("build", None);
}
// On a resumed session the previous attempt's debian/files would make
// On a resumed build the previous attempt's debian/files would make
// the artifact collection below surface its outputs: only the new
// attempt's registration may remain.
if resume {
if build_resume {
let _ = ctx
.command("rm")
.arg("-f")