use crate::context; use crate::context::{Context, ContextConfig}; use directories::ProjectDirs; use std::error::Error; use std::fs; use std::path::{Path, PathBuf}; use tar::Archive; use xz2::read::XzDecoder; /// An ephemeral unshare context guard that creates and manages a temporary chroot environment /// for building packages with unshare permissions. pub struct EphemeralContextGuard { previous_context: String, chroot_path: PathBuf, build_succeeded: bool, } impl EphemeralContextGuard { /// Create a new ephemeral unshare context for the specified series pub async fn new(series: &str) -> Result> { let current_context_name = context::manager().current_name(); // Create a temporary directory for the chroot let chroot_path_str = context::current().create_temp_dir()?; let chroot_path = PathBuf::from(chroot_path_str); log::debug!( "Creating new chroot for {} at {}...", series, chroot_path.display() ); // Download and extract the chroot tarball Self::download_and_extract_chroot(series, &chroot_path).await?; // Switch to an ephemeral context to build the package in the chroot context::manager().set_current_ephemeral(Context::new(ContextConfig::Unshare { path: chroot_path.to_string_lossy().to_string(), parent: Some(current_context_name.clone()), })); Ok(Self { previous_context: current_context_name, chroot_path, build_succeeded: false, }) } async fn download_and_extract_chroot( series: &str, chroot_path: &PathBuf, ) -> Result<(), Box> { // Get project directories for caching let proj_dirs = ProjectDirs::from("com", "pkh", "pkh") .ok_or("Could not determine project directories")?; let cache_dir = proj_dirs.cache_dir(); fs::create_dir_all(cache_dir)?; // Create tarball filename based on series let tarball_filename = format!("{}-buildd.tar.xz", series); let tarball_path = cache_dir.join(&tarball_filename); // Check for existing lockfile, and wait for a timeout if it exists // After timeout, warn the user let lockfile_path = tarball_path.with_extension("lock"); let ctx = context::current(); // Check if lockfile exists and wait for it to be removed let mut wait_time = 0; let timeout = 300; // 5 minutes timeout let poll_interval = 5; // Check every 5 seconds while ctx.exists(&lockfile_path)? { if wait_time >= timeout { log::warn!( "Lockfile {} exists and has been present for more than {} seconds. \ Another process may be downloading the chroot tarball. Continuing anyway...", lockfile_path.display(), timeout ); break; } log::info!( "Lockfile {} exists, waiting for download to complete... ({}s/{})", lockfile_path.display(), wait_time, timeout ); std::thread::sleep(std::time::Duration::from_secs(poll_interval)); wait_time += poll_interval; } // Download tarball if it doesn't exist if !tarball_path.exists() { log::debug!("Downloading chroot tarball for {}...", series); Self::download_chroot_tarball(series, &tarball_path).await?; } else { log::debug!("Using cached chroot tarball for {}", series); } // Extract tarball to chroot directory log::debug!("Extracting chroot tarball to {}...", chroot_path.display()); Self::extract_tarball(&tarball_path, chroot_path)?; // Create device nodes in the chroot log::debug!("Creating device nodes in chroot..."); Self::create_device_nodes(chroot_path)?; Ok(()) } async fn download_chroot_tarball( series: &str, tarball_path: &Path, ) -> Result<(), Box> { let ctx = context::current(); // Create a lock file to make sure that noone tries to use the file while it's not fully downloaded let lockfile_path = tarball_path.with_extension("lock"); ctx.command("touch") .arg(lockfile_path.to_string_lossy().to_string()) .status()?; // Make sure we have the right apt keyrings to mmdebstrap the chroot // Check for root privileges before downloading keyring if crate::utils::root::is_root()? { crate::apt::keyring::download_trust_keyring(Some(ctx.clone()), series).await?; } else { log::info!( "Lacking root privileges. Please ensure that the keyrings for the target distribution are present on your system." ); } // Use mmdebstrap to download the tarball to the cache directory let status = ctx .command("mmdebstrap") .arg("--variant=buildd") .arg("--mode=unshare") .arg("--include=mount,curl,ca-certificates") .arg("--format=tar") .arg(series) .arg(tarball_path.to_string_lossy().to_string()) .status()?; if !status.success() { // Remove file on error let _ = ctx .command("rm") .arg("-f") .arg(tarball_path.to_string_lossy().to_string()) .status(); let _ = ctx .command("rm") .arg("-f") .arg(lockfile_path.to_string_lossy().to_string()) .status(); return Err(format!("Failed to download chroot tarball for series {}", series).into()); } // Remove lockfile: tarball is fully downloaded let _ = ctx .command("rm") .arg("-f") .arg(lockfile_path.to_string_lossy().to_string()) .status(); Ok(()) } fn extract_tarball( tarball_path: &PathBuf, chroot_path: &PathBuf, ) -> Result<(), Box> { // Create the chroot directory fs::create_dir_all(chroot_path)?; // Open the tarball file let tarball_file = std::fs::File::open(tarball_path)?; let xz_decoder = XzDecoder::new(tarball_file); let mut archive = Archive::new(xz_decoder); // Extract all files to the chroot directory archive.unpack(chroot_path)?; Ok(()) } fn create_device_nodes(chroot_path: &Path) -> Result<(), Box> { let ctx = context::current(); let dev_null_path = chroot_path.join("dev/null"); let dev_zero_path = chroot_path.join("dev/zero"); // Ensure /dev directory exists fs::create_dir_all(chroot_path.join("dev"))?; // Remove existing device nodes if they exist let _ = ctx .command("rm") .arg("-f") .arg(dev_null_path.to_string_lossy().to_string()) .status(); let _ = ctx .command("rm") .arg("-f") .arg(dev_zero_path.to_string_lossy().to_string()) .status(); // Create new device nodes using fakeroot and mknod let status_null = ctx .command("sudo") .arg("mknod") .arg("-m") .arg("666") .arg(dev_null_path.to_string_lossy().to_string()) .arg("c") .arg("1") .arg("3") .status()?; let status_zero = ctx .command("sudo") .arg("mknod") .arg("-m") .arg("666") .arg(dev_zero_path.to_string_lossy().to_string()) .arg("c") .arg("1") .arg("5") .status()?; if !status_null.success() || !status_zero.success() { return Err("Failed to create device nodes".into()); } Ok(()) } /// Mark the build as successful, which will trigger chroot cleanup on drop pub fn mark_build_successful(&mut self) { self.build_succeeded = true; } } impl Drop for EphemeralContextGuard { fn drop(&mut self) { log::debug!("Cleaning up ephemeral context ({:?})...", &self.chroot_path); // Reset to normal context if let Err(e) = context::manager().set_current(&self.previous_context) { log::error!("Failed to restore context {}: {}", self.previous_context, e); } // Remove chroot directory only if build succeeded if self.build_succeeded { log::debug!( "Build succeeded, removing chroot directory: {}", self.chroot_path.display() ); let result = context::current() .command("sudo") .arg("rm") .arg("-rf") .arg(&self.chroot_path) .status(); match result { Ok(status) => { if !status.success() { log::error!( "Failed to remove chroot directory {}", self.chroot_path.display() ); } else { log::debug!( "Successfully removed chroot directory: {}", self.chroot_path.display() ); } } Err(e) => { log::error!( "Failed to execute cleanup command for {}: {}", self.chroot_path.display(), e ); } } } else { log::debug!( "Build did not succeed or was not marked as successful, keeping chroot directory: {}", self.chroot_path.display() ); } } }