deb: survive crashed sessions and config-blind resumes
CI / build (push) Successful in 3m9s
CI / test (push) Skipped
CI / publish (push) Skipped
CI / snap (push) Successful in 6m46s

Two failures from resumed kernel sessions:

A resumed build re-runs debian/rules build, but the kernels keep
their wrapper-step stamps in debian/stamps and the flavour config
rule there exports .config from the annotations with no config
prerequisite: the resumed build silently kept the previous
attempt's configuration and a config edit never reached the .deb.
Dropping the stamp cache unconditionally would impose the kernel
packaging's shape on every package, so it is data-driven instead:
packages declare the caches under a resume_clear quirk, and
resumed builds remove those tree-relative paths before the build.
The inner kbuild keeps its own incremental state, so only the
cheap wrapper passes re-run and config-affected objects recompile.

A SIGKILLed build (OOM) leaves its overlay mounts and /proc bind
mount behind, and overlayfs creates root-owned work state inside
the workdir: the next plain build (recording is always on, it
replaces the session of its identity) failed to clear the
leftovers with a permission error. Session clearing now unmounts
everything under each entry at depth (re-reading /proc/mounts,
tolerating mount stacks from consecutive crashes) and escalates
through sudo; when the tree still cannot be cleared, the build
reports it and continues without a session instead of layering
over a half-cleared one.
This commit is contained in:
2026-09-28 00:05:14 +02:00
parent 6bb8ab5016
commit d927dc93ab
8 changed files with 230 additions and 42 deletions
+16 -36
View File
@@ -350,20 +350,7 @@ impl Session {
/// attempt's tree would merge stale build artifacts and resurrect
/// host-deleted files into the build.
pub fn wipe_staged_tree(&self) -> io::Result<()> {
let staged = self.chroot_path().join("tmp/pkh-build");
fs::create_dir_all(&staged)?;
for entry in fs::read_dir(&staged)?.flatten() {
let path = entry.path();
let result = if path.is_dir() {
fs::remove_dir_all(&path)
} else {
fs::remove_file(&path)
}
.or_else(|_| crate::deb::ephemeral::privileged_remove(&path));
result
.map_err(|e| io::Error::other(format!("cannot clear {}: {e}", staged.display())))?;
}
Ok(())
crate::deb::ephemeral::clear_dir(&self.chroot_path().join("tmp/pkh-build"))
}
/// Remove the whole session tree (unmounting anything mounted under
@@ -557,6 +544,9 @@ fn open_from_dir(root: PathBuf) -> Option<Session> {
pub enum SessionOpenError {
/// The session is locked by a concurrent build.
Busy(SessionBusy),
/// The previous session could not be cleared (crashed-build leftovers
/// that even the privileged removal could not lift).
Unusable(String),
/// Anything else (I/O, manifest errors).
Other(String),
}
@@ -565,6 +555,7 @@ impl std::fmt::Display for SessionOpenError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SessionOpenError::Busy(busy) => write!(f, "{busy}"),
SessionOpenError::Unusable(message) => write!(f, "{message}"),
SessionOpenError::Other(message) => write!(f, "{message}"),
}
}
@@ -618,28 +609,17 @@ pub fn fresh_session(
session.acquire().map_err(SessionOpenError::Busy)?;
// Wipe the previous attempt (the lock file lives outside the root): a
// previous chroot holds root-owned device nodes, so escalate on
// failure, like the prune removal does.
for entry in fs::read_dir(&root)
.map_err(|e| SessionOpenError::Other(format!("cannot clear {}: {e}", root.display())))?
.flatten()
{
let path = entry.path();
let result = if path.is_dir() {
fs::remove_dir_all(&path)
} else {
fs::remove_file(&path)
}
.or_else(|_| crate::deb::ephemeral::privileged_remove(&path));
if let Err(e) = result {
log::warn!(
"Failed to clear the previous session file '{}': {}",
path.display(),
e
);
}
}
// Wipe the previous attempt (the lock file lives outside the root). A
// crashed build leaves its mounts behind and the kernel creates
// root-owned state inside overlay workdirs, so the clearing unmounts
// at depth and escalates via sudo when needed; see clear_dir.
crate::deb::ephemeral::clear_dir(&root).map_err(|e| {
SessionOpenError::Unusable(format!(
"cannot clear the previous build session {}: {e}. \
Free it with `pkh prune` (or remove it manually) and rebuild",
root.display()
))
})?;
session.reset_fresh(host_tree, identity, tree_version);
Ok(session)