diff --git a/src/context/unshare.rs b/src/context/unshare.rs index 1d6938b..6ef7b92 100644 --- a/src/context/unshare.rs +++ b/src/context/unshare.rs @@ -234,39 +234,33 @@ impl ContextDriver for UnshareDriver { // The upper/work dirs are the persistent ones of a build session: // mounting them again exposes the previous attempt's build - // artifacts over the (live) host tree. - if src.is_dir() && is_overlayfs_available() { - match self.mount_overlay(src, &dest_path, Some(upper), Some(work)) { - Ok(()) => { - debug!( - "Mounted overlay for {} at {} (reused upper {})", - src.display(), - dest_path.display(), - upper.display() - ); - 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 - ); - } - } + // artifacts over the (live) host tree. Mount failure is an ERROR, + // never a silent copy fallback: a copy would merge the host tree + // into the stale staged tree of the previous attempt (old build + // stamps, dropped host deletions) — the caller decides to wipe and + // re-stage instead. + if !src.is_dir() { + return Err(io::Error::new( + io::ErrorKind::Unsupported, + "overlay staging only applies to directories", + )); } - - if src.is_dir() { - copy_dir_recursive(src, &dest_path)?; - } else { - copy_file_with_times(src, &dest_path)?; + if !is_overlayfs_available() { + return Err(io::Error::new( + io::ErrorKind::Unsupported, + "overlayfs is not available on this system", + )); } + 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 { path: Path::new(dest_root).join(filename), - overlay: false, + overlay: true, }) } diff --git a/src/deb/mod.rs b/src/deb/mod.rs index 05b2d18..94144f8 100644 --- a/src/deb/mod.rs +++ b/src/deb/mod.rs @@ -361,9 +361,12 @@ async fn build_binary_package_impl( } let staged_path = if let Some(s) = &session { // Re-mount the session's persistent upperdir over the host - // tree; a driver without overlay support (or a failed mount) - // falls back to a fresh copy — still environment-resumable, - // just not artifact-resumable. + // tree. If overlay staging is unavailable (no overlayfs, mount + // failure, non-overlay driver), wipe the staged tree and + // 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( &parent_dir, &build_root, @@ -371,14 +374,20 @@ async fn build_binary_package_impl( &s.work_dir(), ) { 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)?; + build_resume = false; crate::context::OverlayStaging { path, overlay: false, } } - Err(e) => return Err(e.into()), }; if !staging.overlay { // Copy fallback: the staged tree is fresh, there is nothing diff --git a/src/deb/session.rs b/src/deb/session.rs index a75902a..6a17e1b 100644 --- a/src/deb/session.rs +++ b/src/deb/session.rs @@ -312,7 +312,7 @@ impl Session { /// The advisory lock file path: `/.lock`, outside /// the session root it guards (see [`Session::acquire`]). - fn lock_path(&self) -> PathBuf { + pub(crate) fn lock_path(&self) -> PathBuf { let name = self .root .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 - /// it first). The lock file is deleted along with the root; the flock - /// stays held by this handle until it drops. + /// it first), then unlink the lock file. The caller holds the flock; + /// 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) { if let Err(e) = remove_session_dir(&self.root) { log::warn!( @@ -354,6 +378,7 @@ impl Session { 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 { + 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 /// still the one the current bootstrap would use (name, then size/mtime, /// falling back to the recorded SHA-256 when the cheap check drifts).