diff --git a/src/context/api.rs b/src/context/api.rs index b6b4a91..d6ae00f 100644 --- a/src/context/api.rs +++ b/src/context/api.rs @@ -29,9 +29,38 @@ use super::schroot::SchrootDriver; use super::ssh::SshDriver; use super::unshare::UnshareDriver; +/// Outcome of an overlay-aware staging +/// ([`ContextDriver::ensure_available_with_overlay`]). +#[derive(Debug, Clone)] +pub struct OverlayStaging { + /// The staged path as it appears inside the context. + pub path: PathBuf, + /// Whether the staging reused the persistent overlay upperdir (build + /// artifacts of a previous attempt are visible); `false` means a fresh + /// copy was staged instead. + pub overlay: bool, +} + /// A ContextDriver is the interface for the logic happening inside a context pub trait ContextDriver { fn ensure_available(&self, src: &Path, dest_root: &str) -> io::Result; + /// Stage `src` at `dest_root` over the given persistent overlay + /// upperdir/workdir (host-side paths), for resumable build sessions. + /// + /// The default implementation rejects the request; drivers that cannot + /// overlay-mount fall back to a plain copy in their caller. + fn ensure_available_with_overlay( + &self, + _src: &Path, + _dest_root: &str, + _upper: &Path, + _work: &Path, + ) -> io::Result { + Err(io::Error::new( + io::ErrorKind::Unsupported, + "overlay staging is not supported by this context driver", + )) + } fn retrieve_path(&self, src: &Path, dest: &Path) -> io::Result<()>; fn list_files(&self, path: &Path) -> io::Result>; fn run( @@ -281,6 +310,25 @@ impl Context { .ensure_available(src, dest_root) } + /// Stage `src` at `dest_root` reusing the given overlay upperdir and + /// workdir (host-side paths), so build artifacts written inside the + /// context during a previous attempt survive into this one. Falls back + /// to a fresh copy when the driver does not support overlay staging or + /// the mount fails (the returned [`OverlayStaging::overlay`] says + /// which happened). + pub fn ensure_available_with_overlay( + &self, + src: &Path, + dest_root: &str, + upper: &Path, + work: &Path, + ) -> io::Result { + self.driver() + .as_ref() + .unwrap() + .ensure_available_with_overlay(src, dest_root, upper, work) + } + /// Create a temp directory inside context pub fn create_temp_dir(&self) -> io::Result { self.driver().as_ref().unwrap().create_temp_dir() diff --git a/src/context/mod.rs b/src/context/mod.rs index c686979..06ad391 100644 --- a/src/context/mod.rs +++ b/src/context/mod.rs @@ -7,7 +7,7 @@ pub(crate) mod shell; mod ssh; mod unshare; -pub use api::{Context, ContextCommand, ContextConfig, LineSink, Stream}; +pub use api::{Context, ContextCommand, ContextConfig, LineSink, OverlayStaging, Stream}; // The driver trait is implementation detail of the context API; it is only // needed crate-internally (test-run capture wrapper), so keep it out of the // public surface (and its documentation requirement). diff --git a/src/context/unshare.rs b/src/context/unshare.rs index 43ca512..1d6938b 100644 --- a/src/context/unshare.rs +++ b/src/context/unshare.rs @@ -176,7 +176,7 @@ impl ContextDriver for UnshareDriver { // Try overlayfs for directories first — avoids a potentially expensive full copy if src.is_dir() && is_overlayfs_available() { - match self.try_overlay_mount(src, &dest_path) { + match self.mount_overlay(src, &dest_path, None, None) { Ok(()) => { debug!( "Mounted overlay for {} at {}", @@ -218,6 +218,58 @@ impl ContextDriver for UnshareDriver { Ok(Path::new(dest_root).join(filename)) } + fn ensure_available_with_overlay( + &self, + src: &Path, + dest_root: &str, + upper: &Path, + work: &Path, + ) -> io::Result { + let dest_dir = Path::new(&self.path).join(dest_root.trim_start_matches('/')); + std::fs::create_dir_all(&dest_dir)?; + let filename = src + .file_name() + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Invalid source path"))?; + let dest_path = dest_dir.join(filename); + + // 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 + ); + } + } + } + + if src.is_dir() { + copy_dir_recursive(src, &dest_path)?; + } else { + copy_file_with_times(src, &dest_path)?; + } + Ok(super::api::OverlayStaging { + path: Path::new(dest_root).join(filename), + overlay: false, + }) + } + fn cleanup(&self) -> io::Result<()> { let mounts = self.overlay_mounts.lock().unwrap(); for mount_path in mounts.iter() { @@ -370,19 +422,33 @@ impl UnshareDriver { .expect("UnshareDriver requires a parent context") } - /// Try to mount `src` as an overlay at `dest_path` inside the chroot. + /// Mount `src` as an overlay at `dest_path` inside the chroot. /// - /// On success, the overlay is tracked in `overlay_mounts` for later cleanup. - /// On failure, the caller should fall back to the copy-based approach. - fn try_overlay_mount(&self, src: &Path, dest_path: &Path) -> io::Result<()> { - // Create unique upper/work dirs inside the chroot + /// `upper`/`work` override the per-mount random directories: the + /// resumable-session staging passes its persistent upperdir so build + /// artifacts survive across attempts. On success, the overlay is + /// tracked in `overlay_mounts` for later cleanup. On failure, the + /// caller should fall back to the copy-based approach. + fn mount_overlay( + &self, + src: &Path, + dest_path: &Path, + upper: Option<&Path>, + work: Option<&Path>, + ) -> io::Result<()> { + // Create unique upper/work dirs inside the chroot unless the caller + // pinned them (session reuse). let overlay_base = Path::new(&self.path).join("pkh-overlay"); let id = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos(); - let upper_dir = overlay_base.join(format!("upper-{}", id)); - let work_dir = overlay_base.join(format!("work-{}", id)); + let upper_dir = upper + .map(Path::to_path_buf) + .unwrap_or_else(|| overlay_base.join(format!("upper-{}", id))); + let work_dir = work + .map(Path::to_path_buf) + .unwrap_or_else(|| overlay_base.join(format!("work-{}", id))); fs::create_dir_all(&upper_dir)?; fs::create_dir_all(&work_dir)?; @@ -421,9 +487,13 @@ impl UnshareDriver { .output()?; if !output.status.success() { - // Clean up dirs we created - let _ = fs::remove_dir_all(&upper_dir); - let _ = fs::remove_dir_all(&work_dir); + // Clean up dirs we created (never the caller-pinned persistent + // upper/work of a session: they hold the previous attempt's + // build artifacts) + if upper.is_none() { + let _ = fs::remove_dir_all(&upper_dir); + let _ = fs::remove_dir_all(&work_dir); + } let stderr = String::from_utf8_lossy(&output.stderr); return Err(io::Error::other(format!( "Overlay mount of '{}' at '{}' failed: {}",