deb: rework build sessions into immutable per-build records
The single mutable session per identity loses the last-good state: a failed run after a good one adopts the session in place and leaves only the failed state behind, so "continue from the last success" was impossible. Each build now records an immutable record under /var/tmp/pkh/sessions/<slug>/<id>/ (chroot, tree-upper, journal, manifest), and --incremental forks a NEW record — a full copy of the base's chroot and, at the same changelog version, its staged artifacts — under the base's flock, never mutating the base itself. Selection picks the newest successful record of the identity among the last five usable, falling back to the newest when none succeeded, and --incremental <id> pins an exact record (failed ones allowed: their environment may be fine). The lifecycle is strict keep-iff-incremental: plain runs build in a throwaway record discarded on success, failure and interrupt alike (the SIGINT cleanup hook follows the same policy), while --incremental records are always kept. Each identity keeps its newest five records; pkh prune's 7-day sweep and orphaned-lock cleanup follow the nested layout. --resume and --keep are removed (no aliases): their adoption-in-place and keep-on-success semantics are exactly what the record model replaces. pkh deb list shows id, outcome, tree version, fork lineage and age. README rewritten around the record workflow.
This commit is contained in:
+79
-63
@@ -22,10 +22,12 @@ use xz2::read::XzDecoder;
|
||||
// /proc and any overlayfs mounts.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Interrupt-time teardown of a chroot tree. When `keep_tree` is set the
|
||||
/// tree itself is left in place after unmounting (a resumable build
|
||||
/// session: the interrupt is a pause, not a discard); otherwise this is
|
||||
/// the historical full cleanup.
|
||||
/// Interrupt-time teardown of a build tree. When `keep_tree` is set the
|
||||
/// tree itself is left in place after unmounting (an `--incremental`
|
||||
/// record: the interrupt is a pause, not a discard); otherwise this is
|
||||
/// the historical full cleanup. For a plain `pkh deb` the `path` is the
|
||||
/// whole throwaway record root, so the record is removed — the record
|
||||
/// model's keep-iff-incremental policy applies to interrupts too.
|
||||
///
|
||||
/// Unlike [`EphemeralContextGuard::drop`], this deliberately does NOT go
|
||||
/// through the context manager, the ephemeral context's driver (whose
|
||||
@@ -64,7 +66,7 @@ fn sigint_cleanup_chroot(chroot_path: &Path, keep_tree: bool) {
|
||||
|
||||
if keep_tree {
|
||||
log::debug!(
|
||||
"Interrupted build session kept at {} (resume with `pkh deb --resume`)",
|
||||
"Interrupted build record kept at {} (fork it with `pkh deb --incremental`)",
|
||||
chroot_path.display()
|
||||
);
|
||||
return;
|
||||
@@ -129,8 +131,17 @@ pub(crate) fn privileged_command(program: &str, is_root: bool) -> Command {
|
||||
}
|
||||
|
||||
/// Privileged `rm -rf` of a tree (residual chroots hold root-owned device
|
||||
/// nodes); shared with the session removal in [`crate::deb::session`].
|
||||
/// nodes); shared with the record removal in [`crate::deb::session`].
|
||||
///
|
||||
/// A tree the current user can fully remove needs no escalation, so the
|
||||
/// plain removal runs first (record forks and discarded records carry
|
||||
/// user-owned state); escalation only happens when it fails.
|
||||
pub(crate) fn privileged_remove(path: &Path) -> std::io::Result<()> {
|
||||
match fs::remove_dir_all(path) {
|
||||
Ok(()) => return Ok(()),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
|
||||
Err(_) => {}
|
||||
}
|
||||
let is_root = crate::utils::root::is_root().unwrap_or(false);
|
||||
let status = privileged_command("rm", is_root)
|
||||
.arg("-rf")
|
||||
@@ -294,10 +305,10 @@ fn unescape_mount_field(field: &str) -> String {
|
||||
/// for building packages with unshare permissions.
|
||||
///
|
||||
/// In session mode ([`EphemeralContextGuard::new_for_session`]) the chroot
|
||||
/// lives inside a [`Session`]: a previous attempt's chroot is reused when
|
||||
/// the caller verified it, teardown keeps the tree for a later `--resume`,
|
||||
/// and a successful build removes the session unless `keep_on_success` is
|
||||
/// set.
|
||||
/// lives inside a [`Session`] record: a forked record's chroot is reused
|
||||
/// when the caller verified it, and the keep policy is strict — the record
|
||||
/// is kept exactly for `--incremental` runs (`persist_record`), discarded
|
||||
/// for plain ones whatever the outcome.
|
||||
pub struct EphemeralContextGuard {
|
||||
/// The ephemeral build context this guard created (an unshare context
|
||||
/// bound to the chroot, parented on the base context). Held explicitly so
|
||||
@@ -314,10 +325,10 @@ pub struct EphemeralContextGuard {
|
||||
/// Registration of the interrupt-time cleanup hook; deregistered when
|
||||
/// this guard drops, so the hook can never fire after the normal cleanup
|
||||
cleanup_hook: Option<CleanupHookGuard>,
|
||||
/// The resumable build session this guard works in, when any.
|
||||
/// The build record this guard works in, when any.
|
||||
session: Option<Session>,
|
||||
/// Whether a successful build keeps the session (`pkh deb --keep`).
|
||||
keep_on_success: bool,
|
||||
/// Whether the record is kept after the build (`pkh deb --incremental`).
|
||||
persist_record: bool,
|
||||
}
|
||||
|
||||
impl EphemeralContextGuard {
|
||||
@@ -340,17 +351,18 @@ impl EphemeralContextGuard {
|
||||
/// Create the ephemeral build context inside `session`'s chroot.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `session` - The resumable session owning the chroot (locked by the
|
||||
/// * `session` - The build record owning the chroot (locked by the
|
||||
/// caller)
|
||||
/// * `series` - The distribution series (e.g., "noble", "sid")
|
||||
/// * `arch` - Optional target architecture, as for
|
||||
/// [`EphemeralContextGuard::new_with_context`]
|
||||
/// * `base_ctx` - The base context to use for chroot work
|
||||
/// * `reuse_chroot` - Adopt the previous attempt's bootstrapped chroot
|
||||
/// * `reuse_chroot` - Reuse the forked record's bootstrapped chroot
|
||||
/// instead of re-extracting the tarball; the caller must only set
|
||||
/// this after the session's chroot passed the integrity probe
|
||||
/// * `keep_on_success` - Keep the session after a successful build
|
||||
/// (`pkh deb --keep`)
|
||||
/// this after the record's chroot passed the integrity probe
|
||||
/// * `persist_record` - Keep the record after the build
|
||||
/// (`pkh deb --incremental`); a plain run's record is discarded
|
||||
/// whatever the outcome
|
||||
pub async fn new_for_session(
|
||||
session: Session,
|
||||
series: &str,
|
||||
@@ -358,7 +370,7 @@ impl EphemeralContextGuard {
|
||||
base_ctx: Arc<Context>,
|
||||
view: &dyn BuildView,
|
||||
reuse_chroot: bool,
|
||||
keep_on_success: bool,
|
||||
persist_record: bool,
|
||||
) -> Result<Self, Box<dyn Error>> {
|
||||
Self::new_internal(
|
||||
Some(session),
|
||||
@@ -367,7 +379,7 @@ impl EphemeralContextGuard {
|
||||
base_ctx,
|
||||
view,
|
||||
reuse_chroot,
|
||||
keep_on_success,
|
||||
persist_record,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -379,7 +391,7 @@ impl EphemeralContextGuard {
|
||||
base_ctx: Arc<Context>,
|
||||
view: &dyn BuildView,
|
||||
reuse_chroot: bool,
|
||||
keep_on_success: bool,
|
||||
persist_record: bool,
|
||||
) -> Result<Self, Box<dyn Error>> {
|
||||
// Save the globally-installed context so Drop can restore exactly
|
||||
// this handle: concurrent builds install their own ephemeral
|
||||
@@ -387,7 +399,7 @@ impl EphemeralContextGuard {
|
||||
// before this guard swapped anything in.
|
||||
let previous_context = context::current();
|
||||
|
||||
// In session mode the chroot lives inside the session root (and is
|
||||
// In session mode the chroot lives inside the record root (and is
|
||||
// reused when the caller says so); otherwise it is a fresh temp dir.
|
||||
let chroot_path = match &session {
|
||||
Some(s) => {
|
||||
@@ -412,14 +424,21 @@ impl EphemeralContextGuard {
|
||||
// base context: the hook must be self-contained (stored path +
|
||||
// direct umount/rm subprocesses) and cannot go through `base_ctx`.
|
||||
// For remote or nested bases the chroot lives elsewhere, and
|
||||
// leftovers stay handled by `pkh prune` as before. A session tree is
|
||||
// kept on interrupt (a pause, not a discard); a plain temp chroot is
|
||||
// still removed.
|
||||
// leftovers stay handled by `pkh prune` as before. An
|
||||
// `--incremental` record is kept on interrupt (a pause, not a
|
||||
// discard); a plain run's whole record root and a plain temp chroot
|
||||
// are removed.
|
||||
let cleanup_hook = if matches!(base_ctx.config, ContextConfig::Local) {
|
||||
Some(crate::interrupt::register_cleanup_hook(Box::new({
|
||||
let chroot_path = chroot_path.clone();
|
||||
let keep_tree = session.is_some();
|
||||
move || sigint_cleanup_chroot(&chroot_path, keep_tree)
|
||||
// Session mode tears the whole record root down (the
|
||||
// manifest is part of the record); plain temp chroots
|
||||
// only ever had the chroot dir.
|
||||
let cleanup_path = match &session {
|
||||
Some(s) => s.root().to_path_buf(),
|
||||
None => chroot_path.clone(),
|
||||
};
|
||||
let keep_tree = persist_record;
|
||||
move || sigint_cleanup_chroot(&cleanup_path, keep_tree)
|
||||
})))
|
||||
} else {
|
||||
log::debug!(
|
||||
@@ -429,7 +448,7 @@ impl EphemeralContextGuard {
|
||||
None
|
||||
};
|
||||
|
||||
// Download and extract the chroot tarball (or reuse the session's
|
||||
// Download and extract the chroot tarball (or reuse the record's
|
||||
// prepared chroot)
|
||||
if let Err(e) = Self::download_and_extract_chroot(
|
||||
series,
|
||||
@@ -483,7 +502,7 @@ impl EphemeralContextGuard {
|
||||
base_ctx,
|
||||
cleanup_hook,
|
||||
session,
|
||||
keep_on_success,
|
||||
persist_record,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -506,19 +525,19 @@ impl EphemeralContextGuard {
|
||||
session: Option<&Session>,
|
||||
reuse_chroot: bool,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
// Session resume: the previous attempt's chroot passed the caller's
|
||||
// checks — skip the tarball extraction entirely, the environment
|
||||
// (apt lists, installed build-deps) is still in there.
|
||||
// Record fork: the base's chroot passed the caller's checks —
|
||||
// skip the tarball extraction entirely, the environment (apt
|
||||
// lists, installed build-deps) is still in there.
|
||||
if let Some(s) = session {
|
||||
if reuse_chroot && s.chroot_is_usable() {
|
||||
log::info!(
|
||||
"Reusing the prepared chroot of session {} (bootstrap skipped)",
|
||||
"Reusing the prepared chroot of record {} (bootstrap skipped)",
|
||||
s.manifest().id
|
||||
);
|
||||
Self::finalize_chroot(chroot_path, ctx, view)?;
|
||||
return Ok(());
|
||||
}
|
||||
// Not reusing (fresh session or failed probe): never extract
|
||||
// Not reusing (fresh record or failed probe): never extract
|
||||
// over stale content.
|
||||
clear_dir(chroot_path)?;
|
||||
}
|
||||
@@ -600,8 +619,8 @@ impl EphemeralContextGuard {
|
||||
// Create device nodes in the chroot and bind-mount /proc
|
||||
Self::finalize_chroot(chroot_path, ctx_for_devices, view)?;
|
||||
|
||||
// Record the bootstrap in the session: marker file (the resume
|
||||
// integrity probe) and the tarball the chroot came from.
|
||||
// Record the bootstrap in the record: marker file (the integrity
|
||||
// probe) and the tarball the chroot came from.
|
||||
if let Some(s) = session {
|
||||
std::fs::write(chroot_path.join(".pkh-ready"), b"")?;
|
||||
let metadata = fs::metadata(&tarball_path)?;
|
||||
@@ -619,7 +638,7 @@ impl EphemeralContextGuard {
|
||||
tarball_sha256,
|
||||
ready: true,
|
||||
});
|
||||
log::debug!("Session {} chroot is ready", s.manifest().id);
|
||||
log::debug!("Record {} chroot is ready", s.manifest().id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -864,7 +883,7 @@ impl EphemeralContextGuard {
|
||||
}
|
||||
|
||||
/// The staged build root inside the chroot: the stable
|
||||
/// `/tmp/pkh-build` for session builds (the overlay upperdir mirrors
|
||||
/// `/tmp/pkh-build` for record builds (the overlay upperdir mirrors
|
||||
/// paths relative to the merge point, so the staged tree must live at
|
||||
/// the same in-chroot path on every attempt for the build artifacts to
|
||||
/// be reusable), a fresh timestamped temp dir otherwise.
|
||||
@@ -923,10 +942,11 @@ impl Drop for EphemeralContextGuard {
|
||||
// touches it.
|
||||
context::manager().set_current_ephemeral(self.previous_context.clone());
|
||||
|
||||
// Session mode: the tree is a resumable build session. The /proc
|
||||
// bind mount must come down in every case (a kept session has to be
|
||||
// unmount-clean for the next resume); the tree itself stays unless
|
||||
// a successful build without `--keep` consumes it.
|
||||
// Session mode: the tree is a build record. The /proc bind mount
|
||||
// must come down in every case (a kept record has to be
|
||||
// unmount-clean for the next fork); the record itself follows the
|
||||
// keep policy — kept exactly for `--incremental` runs, discarded
|
||||
// for plain ones whatever the outcome.
|
||||
if let Some(session) = &self.session {
|
||||
let proc_path = self.chroot_path.join("proc");
|
||||
let is_root = crate::utils::root::is_root().unwrap_or(false);
|
||||
@@ -940,26 +960,7 @@ impl Drop for EphemeralContextGuard {
|
||||
.status()
|
||||
};
|
||||
|
||||
if self.build_succeeded {
|
||||
if self.keep_on_success {
|
||||
session.set_outcome(session::OUTCOME_SUCCESS);
|
||||
log::info!(
|
||||
"Build session {} kept (resume with `pkh deb --resume`)",
|
||||
session.manifest().id
|
||||
);
|
||||
} else {
|
||||
log::debug!(
|
||||
"Build succeeded, removing build session {}",
|
||||
session.root().display()
|
||||
);
|
||||
session.remove();
|
||||
}
|
||||
} else {
|
||||
log::debug!(
|
||||
"Build did not succeed: session {} kept (resume with `pkh deb --resume`)",
|
||||
session.manifest().id
|
||||
);
|
||||
}
|
||||
session.dispose_after_build(self.persist_record, self.build_succeeded);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1085,6 +1086,21 @@ mod chroot_cleanup_tests {
|
||||
assert!(!chroot.exists());
|
||||
}
|
||||
|
||||
/// The interrupt teardown of a kept tree (an `--incremental` record)
|
||||
/// unmounts and leaves the tree in place — the pause, not the discard.
|
||||
#[test]
|
||||
fn sigint_cleanup_keeps_the_tree_when_asked() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let record_root = dir.path().join("record");
|
||||
std::fs::create_dir_all(record_root.join("chroot/rootfs")).unwrap();
|
||||
std::fs::write(record_root.join("session.json"), "{}").unwrap();
|
||||
|
||||
sigint_cleanup_chroot(&record_root, true);
|
||||
|
||||
assert!(record_root.join("chroot/rootfs").exists());
|
||||
assert!(record_root.join("session.json").is_file());
|
||||
}
|
||||
|
||||
/// clear_dir removes content a plain `rm -rf` cannot: a root-owned
|
||||
/// 0700 directory (the shape of an overlay workdir left by a crashed
|
||||
/// build). Skipped when neither root nor working non-interactive sudo
|
||||
|
||||
Reference in New Issue
Block a user