deb: wipe the staged tree when overlay staging falls back to a copy
On a resumed session whose overlay mount fails (no overlayfs, mount error, non-overlay driver), the fallback used to copy the host tree into the staged tree of the previous attempt: a merge that keeps the old build stamps and resurrects files deleted on the host — a silently half-stale build, exactly what resume must never produce. The driver now reports the failure instead of copying, and the caller wipes the staged build root and re-stages a fresh copy (environment resumable, build artifacts discarded).
This commit is contained in:
+23
-29
@@ -234,39 +234,33 @@ impl ContextDriver for UnshareDriver {
|
|||||||
|
|
||||||
// The upper/work dirs are the persistent ones of a build session:
|
// The upper/work dirs are the persistent ones of a build session:
|
||||||
// mounting them again exposes the previous attempt's build
|
// mounting them again exposes the previous attempt's build
|
||||||
// artifacts over the (live) host tree.
|
// artifacts over the (live) host tree. Mount failure is an ERROR,
|
||||||
if src.is_dir() && is_overlayfs_available() {
|
// never a silent copy fallback: a copy would merge the host tree
|
||||||
match self.mount_overlay(src, &dest_path, Some(upper), Some(work)) {
|
// into the stale staged tree of the previous attempt (old build
|
||||||
Ok(()) => {
|
// stamps, dropped host deletions) — the caller decides to wipe and
|
||||||
debug!(
|
// re-stage instead.
|
||||||
"Mounted overlay for {} at {} (reused upper {})",
|
if !src.is_dir() {
|
||||||
src.display(),
|
return Err(io::Error::new(
|
||||||
dest_path.display(),
|
io::ErrorKind::Unsupported,
|
||||||
upper.display()
|
"overlay staging only applies to directories",
|
||||||
);
|
));
|
||||||
return Ok(super::api::OverlayStaging {
|
|
||||||
path: Path::new(dest_root).join(filename),
|
|
||||||
overlay: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
debug!(
|
|
||||||
"Overlay remount failed for '{}', falling back to copy: {}",
|
|
||||||
src.display(),
|
|
||||||
e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if !is_overlayfs_available() {
|
||||||
if src.is_dir() {
|
return Err(io::Error::new(
|
||||||
copy_dir_recursive(src, &dest_path)?;
|
io::ErrorKind::Unsupported,
|
||||||
} else {
|
"overlayfs is not available on this system",
|
||||||
copy_file_with_times(src, &dest_path)?;
|
));
|
||||||
}
|
}
|
||||||
|
self.mount_overlay(src, &dest_path, Some(upper), Some(work))?;
|
||||||
|
debug!(
|
||||||
|
"Mounted overlay for {} at {} (reused upper {})",
|
||||||
|
src.display(),
|
||||||
|
dest_path.display(),
|
||||||
|
upper.display()
|
||||||
|
);
|
||||||
Ok(super::api::OverlayStaging {
|
Ok(super::api::OverlayStaging {
|
||||||
path: Path::new(dest_root).join(filename),
|
path: Path::new(dest_root).join(filename),
|
||||||
overlay: false,
|
overlay: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+14
-5
@@ -361,9 +361,12 @@ async fn build_binary_package_impl(
|
|||||||
}
|
}
|
||||||
let staged_path = if let Some(s) = &session {
|
let staged_path = if let Some(s) = &session {
|
||||||
// Re-mount the session's persistent upperdir over the host
|
// Re-mount the session's persistent upperdir over the host
|
||||||
// tree; a driver without overlay support (or a failed mount)
|
// tree. If overlay staging is unavailable (no overlayfs, mount
|
||||||
// falls back to a fresh copy — still environment-resumable,
|
// failure, non-overlay driver), wipe the staged tree and
|
||||||
// just not artifact-resumable.
|
// re-stage a fresh copy: copying over the previous attempt's
|
||||||
|
// tree would merge stale build artifacts and resurrect
|
||||||
|
// host-deleted files into the build. Still environment-
|
||||||
|
// resumable, just not artifact-resumable.
|
||||||
let staging = match build_ctx.ensure_available_with_overlay(
|
let staging = match build_ctx.ensure_available_with_overlay(
|
||||||
&parent_dir,
|
&parent_dir,
|
||||||
&build_root,
|
&build_root,
|
||||||
@@ -371,14 +374,20 @@ async fn build_binary_package_impl(
|
|||||||
&s.work_dir(),
|
&s.work_dir(),
|
||||||
) {
|
) {
|
||||||
Ok(staging) => staging,
|
Ok(staging) => staging,
|
||||||
Err(e) if e.kind() == std::io::ErrorKind::Unsupported => {
|
Err(e) => {
|
||||||
|
log::info!(
|
||||||
|
"Overlay staging unavailable ({}): the staged tree is \
|
||||||
|
re-staged from scratch, build artifacts are discarded",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
s.wipe_staged_tree()?;
|
||||||
let path = build_ctx.ensure_available(&parent_dir, &build_root)?;
|
let path = build_ctx.ensure_available(&parent_dir, &build_root)?;
|
||||||
|
build_resume = false;
|
||||||
crate::context::OverlayStaging {
|
crate::context::OverlayStaging {
|
||||||
path,
|
path,
|
||||||
overlay: false,
|
overlay: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => return Err(e.into()),
|
|
||||||
};
|
};
|
||||||
if !staging.overlay {
|
if !staging.overlay {
|
||||||
// Copy fallback: the staged tree is fresh, there is nothing
|
// Copy fallback: the staged tree is fresh, there is nothing
|
||||||
|
|||||||
+52
-3
@@ -312,7 +312,7 @@ impl Session {
|
|||||||
|
|
||||||
/// The advisory lock file path: `<sessions root>/<slug>.lock`, outside
|
/// The advisory lock file path: `<sessions root>/<slug>.lock`, outside
|
||||||
/// the session root it guards (see [`Session::acquire`]).
|
/// the session root it guards (see [`Session::acquire`]).
|
||||||
fn lock_path(&self) -> PathBuf {
|
pub(crate) fn lock_path(&self) -> PathBuf {
|
||||||
let name = self
|
let name = self
|
||||||
.root
|
.root
|
||||||
.file_name()
|
.file_name()
|
||||||
@@ -343,9 +343,33 @@ impl Session {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wipe the staged tree inside the chroot (the stable build root).
|
||||||
|
///
|
||||||
|
/// Used when overlay staging is unavailable on a resumed session: the
|
||||||
|
/// fallback re-stages a fresh copy, and copying over the previous
|
||||||
|
/// 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(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Remove the whole session tree (unmounting anything mounted under
|
/// Remove the whole session tree (unmounting anything mounted under
|
||||||
/// it first). The lock file is deleted along with the root; the flock
|
/// it first), then unlink the lock file. The caller holds the flock;
|
||||||
/// stays held by this handle until it drops.
|
/// an opener that raced ahead of the unlink failed its lock attempt
|
||||||
|
/// while we held it, and the next one creates a fresh session.
|
||||||
pub fn remove(&self) {
|
pub fn remove(&self) {
|
||||||
if let Err(e) = remove_session_dir(&self.root) {
|
if let Err(e) = remove_session_dir(&self.root) {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
@@ -354,6 +378,7 @@ impl Session {
|
|||||||
e
|
e
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
let _ = fs::remove_file(self.lock_path());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -649,6 +674,30 @@ impl Session {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the lock file at `path` is currently held by a live build
|
||||||
|
/// (flock taken). `Ok(false)` also covers a missing file.
|
||||||
|
pub(crate) fn lock_is_held(path: &Path) -> io::Result<bool> {
|
||||||
|
use std::os::unix::io::AsRawFd;
|
||||||
|
let file = match fs::OpenOptions::new().read(true).open(path) {
|
||||||
|
Ok(file) => file,
|
||||||
|
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(false),
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
|
let taken = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
|
||||||
|
if taken == 0 {
|
||||||
|
// Nobody holds it: release the probe's own lock and report free.
|
||||||
|
unsafe {
|
||||||
|
libc::flock(file.as_raw_fd(), libc::LOCK_UN);
|
||||||
|
}
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
let err = io::Error::last_os_error();
|
||||||
|
if err.kind() == io::ErrorKind::WouldBlock {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
Err(err)
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether the cached chroot tarball the session was bootstrapped from is
|
/// Whether the cached chroot tarball the session was bootstrapped from is
|
||||||
/// still the one the current bootstrap would use (name, then size/mtime,
|
/// still the one the current bootstrap would use (name, then size/mtime,
|
||||||
/// falling back to the recorded SHA-256 when the cheap check drifts).
|
/// falling back to the recorded SHA-256 when the cheap check drifts).
|
||||||
|
|||||||
Reference in New Issue
Block a user