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:
+180
-161
@@ -13,7 +13,7 @@ use tar::Archive;
|
||||
use xz2::read::XzDecoder;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Interrupt-time chroot cleanup
|
||||
// Interrupt-time cleanup
|
||||
//
|
||||
// On Ctrl-C, the watchdog in `crate::interrupt` runs the hook registered in
|
||||
// [`EphemeralContextGuard::new_with_context`] right before exiting — the
|
||||
@@ -22,34 +22,61 @@ use xz2::read::XzDecoder;
|
||||
// /proc and any overlayfs mounts.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// 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.
|
||||
/// Interrupt-time teardown of an anonymous temp chroot (no build record):
|
||||
/// unmount everything under it, then remove it — a plain `rm -rf` with a
|
||||
/// retry loop, best-effort. See [`sigint_cleanup_record`] for records.
|
||||
fn sigint_cleanup_chroot(chroot_path: &Path) {
|
||||
let is_root = unsafe { libc::geteuid() } == 0;
|
||||
unmount_under_logged(chroot_path);
|
||||
remove_with_retries(chroot_path, is_root);
|
||||
}
|
||||
|
||||
/// Interrupt-time teardown of a build record: unmount everything under
|
||||
/// the record root, remove the per-run state (the chroot and the overlay
|
||||
/// scratch workdir) and — unless `keep_upper` is set — the staged tree
|
||||
/// and its host-tree snapshot. The manifest and the build log always
|
||||
/// stay: every build leaves a metadata record, the interrupt included.
|
||||
///
|
||||
/// Unlike [`EphemeralContextGuard::drop`], this deliberately does NOT go
|
||||
/// through the context manager, the ephemeral context's driver (whose
|
||||
/// `cleanup()` unmounts the tracked overlays) or the base context's command
|
||||
/// builder: interrupt-time hooks must be self-contained, and those
|
||||
/// machineries may be mid-mutation on the interrupted thread. Instead it
|
||||
/// only reads /proc/mounts and spawns umount/rm directly.
|
||||
///
|
||||
/// On a full cleanup it removes the tree regardless of the build result:
|
||||
/// the build was aborted, and leaving a still-mounted chroot behind is
|
||||
/// exactly the leak this hook exists to prevent.
|
||||
/// `cleanup()` unmounts the tracked overlays) or the base context's
|
||||
/// command builder: interrupt-time hooks must be self-contained, and
|
||||
/// those machineries may be mid-mutation on the interrupted thread.
|
||||
/// Instead it only reads /proc/mounts and spawns umount/rm directly.
|
||||
///
|
||||
/// Best-effort by design: if a child process still holds a mount busy or
|
||||
/// privilege escalation is unavailable, individual steps fail; failures are
|
||||
/// logged (pointing at `pkh prune` for the leftovers) and never panic.
|
||||
fn sigint_cleanup_chroot(chroot_path: &Path, keep_tree: bool) {
|
||||
/// privilege escalation is unavailable, individual steps fail; failures
|
||||
/// are logged (pointing at `pkh prune` for the leftovers) and never
|
||||
/// panic.
|
||||
fn sigint_cleanup_record(record_root: &Path, keep_upper: bool) {
|
||||
let is_root = unsafe { libc::geteuid() } == 0;
|
||||
unmount_under_logged(record_root);
|
||||
// Per-run state first: the chroot holds the /proc bind mount and the
|
||||
// device nodes, the workdir holds root-owned overlay scratch.
|
||||
for dir in ["chroot", "tree-upper-work"] {
|
||||
remove_with_retries(&record_root.join(dir), is_root);
|
||||
}
|
||||
if keep_upper {
|
||||
log::debug!(
|
||||
"Interrupted build record kept its staged tree at {} (fork it \
|
||||
with `pkh deb --incremental`)",
|
||||
record_root.display()
|
||||
);
|
||||
} else {
|
||||
for path in ["tree-upper", "host-files.list"] {
|
||||
remove_with_retries(&record_root.join(path), is_root);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unmount children before parents: /proc/mounts lists mounts roughly in
|
||||
// creation order, so walk it in reverse
|
||||
let mounts = host_mounts_under(chroot_path);
|
||||
for mount_point in mounts.into_iter().rev() {
|
||||
/// Unmount every mount at or under `base`, deepest first, logging the
|
||||
/// failures (interrupt-time: leftover mounts are `pkh prune`'s to
|
||||
/// collect).
|
||||
fn unmount_under_logged(base: &Path) {
|
||||
let is_root = unsafe { libc::geteuid() } == 0;
|
||||
// Unmount children before parents: /proc/mounts lists mounts roughly
|
||||
// in creation order, so walk it in reverse.
|
||||
for mount_point in host_mounts_under(base).into_iter().rev() {
|
||||
if unmount_path(&mount_point, is_root) {
|
||||
log::debug!(
|
||||
"Unmounted {} during interrupt cleanup",
|
||||
@@ -63,19 +90,13 @@ fn sigint_cleanup_chroot(chroot_path: &Path, keep_tree: bool) {
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if keep_tree {
|
||||
log::debug!(
|
||||
"Interrupted build record kept at {} (fork it with `pkh deb --incremental`)",
|
||||
chroot_path.display()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the chroot tree itself (tolerates a missing directory). A
|
||||
// child the Ctrl+C interrupted may still be finishing its writeout —
|
||||
// dpkg defers SIGINT until it reaches a safe state — so retry while rm
|
||||
// reports the tree non-empty instead of leaving it half-removed.
|
||||
/// Privileged `rm -rf` with a retry loop: a child the Ctrl+C interrupted
|
||||
/// may still be finishing its writeout — dpkg defers SIGINT until it
|
||||
/// reaches a safe state — so retry while rm reports the tree non-empty
|
||||
/// instead of leaving it half-removed. A missing path succeeds at once.
|
||||
fn remove_with_retries(path: &Path, is_root: bool) {
|
||||
const RETRIES: usize = 10;
|
||||
const RETRY_DELAY: std::time::Duration = std::time::Duration::from_millis(300);
|
||||
let mut last = None;
|
||||
@@ -86,7 +107,7 @@ fn sigint_cleanup_chroot(chroot_path: &Path, keep_tree: bool) {
|
||||
last = Some(
|
||||
privileged_command("rm", is_root)
|
||||
.arg("-rf")
|
||||
.arg(chroot_path)
|
||||
.arg(path)
|
||||
.status(),
|
||||
);
|
||||
if matches!(&last, Some(Ok(status)) if status.success()) {
|
||||
@@ -95,22 +116,19 @@ fn sigint_cleanup_chroot(chroot_path: &Path, keep_tree: bool) {
|
||||
}
|
||||
match last {
|
||||
Some(Ok(status)) if status.success() => {
|
||||
log::debug!(
|
||||
"Removed chroot {} during interrupt cleanup",
|
||||
chroot_path.display()
|
||||
);
|
||||
log::debug!("Removed {} during interrupt cleanup", path.display());
|
||||
}
|
||||
Some(Ok(status)) => {
|
||||
log::error!(
|
||||
"Failed to remove chroot {} during interrupt cleanup \
|
||||
"Failed to remove {} during interrupt cleanup \
|
||||
(rm exited with {status}); run `pkh prune`",
|
||||
chroot_path.display()
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
log::error!(
|
||||
"Failed to run rm for chroot {} during interrupt cleanup: {e}; run `pkh prune`",
|
||||
chroot_path.display()
|
||||
"Failed to run rm for {} during interrupt cleanup: {e}; run `pkh prune`",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
None => unreachable!("at least one rm attempt ran"),
|
||||
@@ -305,10 +323,11 @@ 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`] 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.
|
||||
/// lives inside a [`Session`] record's `chroot/` directory — extracted
|
||||
/// fresh from the tarball cache on every run (nothing chroot-shaped is
|
||||
/// persisted) and removed at the end of the run. The record itself is
|
||||
/// always kept as metadata and log; its staged tree is retained exactly
|
||||
/// for `--incremental` runs (`keep_upper`).
|
||||
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
|
||||
@@ -327,8 +346,9 @@ pub struct EphemeralContextGuard {
|
||||
cleanup_hook: Option<CleanupHookGuard>,
|
||||
/// The build record this guard works in, when any.
|
||||
session: Option<Session>,
|
||||
/// Whether the record is kept after the build (`pkh deb --incremental`).
|
||||
persist_record: bool,
|
||||
/// Whether the record's staged tree is retained after the build
|
||||
/// (`pkh deb --incremental`).
|
||||
keep_upper: bool,
|
||||
}
|
||||
|
||||
impl EphemeralContextGuard {
|
||||
@@ -345,7 +365,7 @@ impl EphemeralContextGuard {
|
||||
base_ctx: Arc<Context>,
|
||||
view: &dyn BuildView,
|
||||
) -> Result<Self, Box<dyn Error>> {
|
||||
Self::new_internal(None, series, arch, base_ctx, view, false, false).await
|
||||
Self::new_internal(None, series, arch, base_ctx, view, false).await
|
||||
}
|
||||
|
||||
/// Create the ephemeral build context inside `session`'s chroot.
|
||||
@@ -357,31 +377,18 @@ impl EphemeralContextGuard {
|
||||
/// * `arch` - Optional target architecture, as for
|
||||
/// [`EphemeralContextGuard::new_with_context`]
|
||||
/// * `base_ctx` - The base context to use for chroot work
|
||||
/// * `reuse_chroot` - Reuse the forked record's bootstrapped chroot
|
||||
/// instead of re-extracting the tarball; the caller must only set
|
||||
/// 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
|
||||
/// * `keep_upper` - Retain the record's staged tree after the build
|
||||
/// (`pkh deb --incremental`); a plain run's record keeps metadata
|
||||
/// and log only
|
||||
pub async fn new_for_session(
|
||||
session: Session,
|
||||
series: &str,
|
||||
arch: Option<&str>,
|
||||
base_ctx: Arc<Context>,
|
||||
view: &dyn BuildView,
|
||||
reuse_chroot: bool,
|
||||
persist_record: bool,
|
||||
keep_upper: bool,
|
||||
) -> Result<Self, Box<dyn Error>> {
|
||||
Self::new_internal(
|
||||
Some(session),
|
||||
series,
|
||||
arch,
|
||||
base_ctx,
|
||||
view,
|
||||
reuse_chroot,
|
||||
persist_record,
|
||||
)
|
||||
.await
|
||||
Self::new_internal(Some(session), series, arch, base_ctx, view, keep_upper).await
|
||||
}
|
||||
|
||||
async fn new_internal(
|
||||
@@ -390,8 +397,7 @@ impl EphemeralContextGuard {
|
||||
arch: Option<&str>,
|
||||
base_ctx: Arc<Context>,
|
||||
view: &dyn BuildView,
|
||||
reuse_chroot: bool,
|
||||
persist_record: bool,
|
||||
keep_upper: bool,
|
||||
) -> Result<Self, Box<dyn Error>> {
|
||||
// Save the globally-installed context so Drop can restore exactly
|
||||
// this handle: concurrent builds install their own ephemeral
|
||||
@@ -399,8 +405,8 @@ impl EphemeralContextGuard {
|
||||
// before this guard swapped anything in.
|
||||
let previous_context = context::current();
|
||||
|
||||
// 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.
|
||||
// In session mode the chroot lives inside the record root, as
|
||||
// per-run scratch; otherwise it is a fresh temp dir.
|
||||
let chroot_path = match &session {
|
||||
Some(s) => {
|
||||
let path = s.chroot_path();
|
||||
@@ -424,22 +430,23 @@ 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. 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.
|
||||
// leftovers stay handled by `pkh prune` as before.
|
||||
//
|
||||
// Session mode tears the record's per-run state down (chroot and
|
||||
// overlay workdir) and keeps its staged tree exactly for
|
||||
// `--incremental`; a plain temp chroot is removed entirely.
|
||||
let cleanup_hook = if matches!(base_ctx.config, ContextConfig::Local) {
|
||||
Some(crate::interrupt::register_cleanup_hook(Box::new({
|
||||
// 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)
|
||||
})))
|
||||
let hook: Box<dyn Fn() + Send> = match session.as_ref().map(|s| s.root().to_path_buf())
|
||||
{
|
||||
Some(record_root) => {
|
||||
Box::new(move || sigint_cleanup_record(&record_root, keep_upper))
|
||||
}
|
||||
None => {
|
||||
let chroot_path = chroot_path.clone();
|
||||
Box::new(move || sigint_cleanup_chroot(&chroot_path))
|
||||
}
|
||||
};
|
||||
Some(crate::interrupt::register_cleanup_hook(hook))
|
||||
} else {
|
||||
log::debug!(
|
||||
"Base context is not local; skipping interrupt-time cleanup registration for {}",
|
||||
@@ -448,18 +455,11 @@ impl EphemeralContextGuard {
|
||||
None
|
||||
};
|
||||
|
||||
// Download and extract the chroot tarball (or reuse the record's
|
||||
// prepared chroot)
|
||||
if let Err(e) = Self::download_and_extract_chroot(
|
||||
series,
|
||||
arch,
|
||||
&chroot_path,
|
||||
base_ctx.clone(),
|
||||
view,
|
||||
session.as_ref(),
|
||||
reuse_chroot,
|
||||
)
|
||||
.await
|
||||
// Download and extract the chroot tarball — always fresh; the
|
||||
// record's chroot directory is per-run scratch.
|
||||
if let Err(e) =
|
||||
Self::download_and_extract_chroot(series, arch, &chroot_path, base_ctx.clone(), view)
|
||||
.await
|
||||
{
|
||||
// On a Ctrl+C the interrupt watchdog owns the tree: keep the
|
||||
// hook registered (forgetting the guard) so it removes the
|
||||
@@ -476,6 +476,17 @@ impl EphemeralContextGuard {
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
// Record the tarball digest this environment came from: the fork
|
||||
// gate of a later `--incremental` run compares it against the
|
||||
// cache to decide whether the record's staged tree is still
|
||||
// reusable.
|
||||
if let Some(s) = &session
|
||||
&& let Ok(tarball) = chroot_tarball_path(series, arch)
|
||||
&& let Ok(sha) = session::sha256_file(&tarball)
|
||||
{
|
||||
s.set_tarball_sha256(&sha);
|
||||
}
|
||||
|
||||
// Switch to an ephemeral context to build the package in the chroot.
|
||||
// The parent is the base context itself (the one that bootstrapped
|
||||
// the chroot), wired through `with_parent` instead of a config-name
|
||||
@@ -502,7 +513,7 @@ impl EphemeralContextGuard {
|
||||
base_ctx,
|
||||
cleanup_hook,
|
||||
session,
|
||||
persist_record,
|
||||
keep_upper,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -522,25 +533,10 @@ impl EphemeralContextGuard {
|
||||
chroot_path: &PathBuf,
|
||||
ctx: Arc<context::Context>,
|
||||
view: &dyn BuildView,
|
||||
session: Option<&Session>,
|
||||
reuse_chroot: bool,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
// 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 record {} (bootstrap skipped)",
|
||||
s.manifest().id
|
||||
);
|
||||
Self::finalize_chroot(chroot_path, ctx, view)?;
|
||||
return Ok(());
|
||||
}
|
||||
// Not reusing (fresh record or failed probe): never extract
|
||||
// over stale content.
|
||||
clear_dir(chroot_path)?;
|
||||
}
|
||||
// The chroot is per-run scratch: nothing is reused, so never
|
||||
// extract over stale content.
|
||||
clear_dir(chroot_path)?;
|
||||
|
||||
// Clone ctx for use in create_device_nodes after download_chroot_tarball consumes it
|
||||
let ctx_for_devices = ctx.clone();
|
||||
@@ -619,33 +615,13 @@ 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 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)?;
|
||||
let tarball_sha256 = session::sha256_file(&tarball_path)
|
||||
.map_err(|e| format!("cannot hash the chroot tarball: {e}"))?;
|
||||
s.set_chroot(session::ChrootInfo {
|
||||
tarball: tarball_filename,
|
||||
tarball_size: metadata.len(),
|
||||
tarball_mtime_secs: metadata
|
||||
.modified()
|
||||
.ok()
|
||||
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0),
|
||||
tarball_sha256,
|
||||
ready: true,
|
||||
});
|
||||
log::debug!("Record {} chroot is ready", s.manifest().id);
|
||||
}
|
||||
|
||||
// The bootstrap's tarball digest is recorded by the caller (the
|
||||
// record's fork gate compares it against the cache later).
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Device nodes and the /proc bind mount for a (possibly reused)
|
||||
/// chroot: idempotent finalization, safe to run on every attempt.
|
||||
/// Device nodes and the /proc bind mount for a fresh chroot:
|
||||
/// idempotent finalization, safe to run on every run.
|
||||
fn finalize_chroot(
|
||||
chroot_path: &Path,
|
||||
ctx: Arc<context::Context>,
|
||||
@@ -660,8 +636,8 @@ impl EphemeralContextGuard {
|
||||
|
||||
// Bind mount /proc from host into chroot (before entering unshare namespace)
|
||||
// This allows /proc to work in containers where mounting inside unshare fails.
|
||||
// A reused chroot may still carry the mount from the previous
|
||||
// attempt: only mount when it is not already there.
|
||||
// A chroot reused within the same run may still carry the mount:
|
||||
// only mount when it is not already there.
|
||||
if is_mountpoint(&chroot_path.join("proc")) {
|
||||
log::debug!("/proc is already bind-mounted in the chroot");
|
||||
} else {
|
||||
@@ -908,6 +884,11 @@ impl Drop for EphemeralContextGuard {
|
||||
// drained, forgetting is a harmless no-op).
|
||||
if crate::interrupt::interrupted() {
|
||||
context::manager().set_current_ephemeral(self.previous_context.clone());
|
||||
// The record keeps its metadata: mark the aborted build as
|
||||
// failed before the hook tears the per-run state down.
|
||||
if let Some(session) = &self.session {
|
||||
session.set_outcome(session::OUTCOME_FAILED);
|
||||
}
|
||||
if let Some(hook) = self.cleanup_hook.take() {
|
||||
std::mem::forget(hook);
|
||||
}
|
||||
@@ -942,11 +923,11 @@ impl Drop for EphemeralContextGuard {
|
||||
// touches it.
|
||||
context::manager().set_current_ephemeral(self.previous_context.clone());
|
||||
|
||||
// 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.
|
||||
// Session mode: the record's per-run chroot comes down in every
|
||||
// case (nothing chroot-shaped is persisted — success, failure and
|
||||
// interrupt all end the run), then the record is retained:
|
||||
// metadata and log always, the staged tree exactly for
|
||||
// `--incremental` runs.
|
||||
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);
|
||||
@@ -960,7 +941,8 @@ impl Drop for EphemeralContextGuard {
|
||||
.status()
|
||||
};
|
||||
|
||||
session.dispose_after_build(self.persist_record, self.build_succeeded);
|
||||
session.remove_run_state();
|
||||
session.retain_upper(self.keep_upper, self.build_succeeded);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1058,7 +1040,7 @@ mod chroot_cleanup_tests {
|
||||
fn sigint_cleanup_of_missing_chroot_is_a_noop() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let missing = dir.path().join("no-such-chroot");
|
||||
sigint_cleanup_chroot(&missing, false);
|
||||
sigint_cleanup_chroot(&missing);
|
||||
assert!(!missing.exists());
|
||||
}
|
||||
|
||||
@@ -1081,24 +1063,61 @@ mod chroot_cleanup_tests {
|
||||
std::fs::create_dir_all(chroot.join("rootfs")).unwrap();
|
||||
std::fs::write(chroot.join("rootfs").join("file.txt"), "data").unwrap();
|
||||
|
||||
sigint_cleanup_chroot(&chroot, false);
|
||||
sigint_cleanup_chroot(&chroot);
|
||||
|
||||
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.
|
||||
/// The record interrupt teardown removes the per-run state and keeps
|
||||
/// the manifest and log; an `--incremental` record keeps its staged
|
||||
/// tree, a plain one loses it.
|
||||
#[test]
|
||||
fn sigint_cleanup_keeps_the_tree_when_asked() {
|
||||
fn sigint_record_teardown_keeps_metadata_and_drops_the_chroot() {
|
||||
let is_root = unsafe { libc::geteuid() } == 0;
|
||||
if !is_root
|
||||
&& !privileged_command("true", false)
|
||||
.status()
|
||||
.is_ok_and(|s| s.success())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
let incremental = dir.path().join("incremental");
|
||||
let plain = dir.path().join("plain");
|
||||
for root in [&incremental, &plain] {
|
||||
std::fs::create_dir_all(root.join("chroot/rootfs")).unwrap();
|
||||
std::fs::create_dir_all(root.join("tree-upper/src")).unwrap();
|
||||
std::fs::create_dir_all(root.join("log")).unwrap();
|
||||
std::fs::write(root.join("manifest.json"), "{}").unwrap();
|
||||
std::fs::write(root.join("log/build.log"), "output\n").unwrap();
|
||||
std::fs::write(root.join("host-files.list"), "f\t1\t1\ta\n").unwrap();
|
||||
}
|
||||
|
||||
sigint_cleanup_chroot(&record_root, true);
|
||||
sigint_cleanup_record(&incremental, true);
|
||||
sigint_cleanup_record(&plain, false);
|
||||
|
||||
assert!(record_root.join("chroot/rootfs").exists());
|
||||
assert!(record_root.join("session.json").is_file());
|
||||
for root in [&incremental, &plain] {
|
||||
assert!(!root.join("chroot").exists(), "the chroot is per-run");
|
||||
assert!(root.join("manifest.json").is_file(), "metadata stays");
|
||||
assert!(root.join("log/build.log").is_file(), "the log stays");
|
||||
}
|
||||
assert!(
|
||||
incremental.join("tree-upper/src").exists(),
|
||||
"an interrupted incremental run keeps its staged tree"
|
||||
);
|
||||
assert!(
|
||||
incremental.join("host-files.list").is_file(),
|
||||
"the snapshot stays with the staged tree"
|
||||
);
|
||||
assert!(
|
||||
!plain.join("tree-upper").exists(),
|
||||
"an interrupted plain run drops its staged tree"
|
||||
);
|
||||
assert!(
|
||||
!plain.join("host-files.list").exists(),
|
||||
"the snapshot goes with the staged tree"
|
||||
);
|
||||
}
|
||||
|
||||
/// clear_dir removes content a plain `rm -rf` cannot: a root-owned
|
||||
|
||||
Reference in New Issue
Block a user