mod cross; mod ephemeral; mod local; mod sbuild; use crate::context; use std::error::Error; use std::path::{Path, PathBuf}; /// Build mode for the binary build #[derive(PartialEq)] pub enum BuildMode { /// Use `sbuild` for the build, configured in unshare mode Sbuild, /// Local build, directly on the context Local, } /// Build package in 'cwd' to a .deb pub async fn build_binary_package( arch: Option<&str>, series: Option<&str>, cwd: Option<&Path>, cross: bool, mode: Option, ) -> Result<(), Box> { let cwd = cwd.unwrap_or_else(|| Path::new(".")); // Parse changelog to get package name, version and series let changelog_path = cwd.join("debian/changelog"); let (package, version, package_series) = crate::changelog::parse_changelog_header(&changelog_path)?; let series = if let Some(s) = series { s } else { &package_series }; let current_arch = crate::get_current_arch(); let arch = arch.unwrap_or(¤t_arch); // Make sure we select a specific mode, either using user-requested // or by using default for user-supplied parameters let mode = if let Some(m) = mode { m } else { // By default, we use local build BuildMode::Local }; // Create an ephemeral unshare context for all Local builds let mut guard = if mode == BuildMode::Local { Some(ephemeral::EphemeralContextGuard::new(series).await?) } else { None }; // Prepare build directory let ctx = context::current(); let build_root = ctx.create_temp_dir()?; // Ensure availability of all needed files for the build let parent_dir = cwd.parent().ok_or("Cannot find parent directory")?; ctx.ensure_available(parent_dir, &build_root)?; let parent_dir_name = parent_dir .file_name() .ok_or("Cannot find parent directory name")?; let build_root = format!("{}/{}", build_root, parent_dir_name.to_str().unwrap()); // Run the build using target build mode match mode { BuildMode::Local => local::build(&package, &version, arch, series, &build_root, cross)?, BuildMode::Sbuild => sbuild::build(&package, &version, arch, series, &build_root, cross)?, }; // Retrieve produced .deb files let remote_files = ctx.list_files(Path::new(&build_root))?; for remote_file in remote_files { if remote_file.extension().is_some_and(|ext| ext == "deb") { let file_name = remote_file.file_name().ok_or("Invalid remote filename")?; let local_dest = parent_dir.join(file_name); ctx.retrieve_path(&remote_file, &local_dest)?; } } // Mark build as successful to trigger chroot cleanup if let Some(ref mut g) = guard { g.mark_build_successful(); } Ok(()) } /// Find the current package directory by trying both patterns: /// - package/package /// - package/package-origversion pub(crate) fn find_package_directory( parent_dir: &Path, package: &str, version: &str, ) -> Result> { let ctx = context::current(); // Try package/package pattern first let package_dir = parent_dir.join(package).join(package); if ctx.exists(&package_dir)? { return Ok(package_dir); } // Compute origversion from version: remove everything after first '-', after stripping epoch let version_without_epoch = version.split_once(':').map(|(_, v)| v).unwrap_or(version); let origversion = version_without_epoch .split_once('-') .map(|(v, _)| v) .unwrap_or(version); // Try package/package-origversion pattern let package_dir = parent_dir .join(package) .join(format!("{}-{}", package, origversion)); if ctx.exists(&package_dir)? { return Ok(package_dir); } // Try 'package' only let package_dir = parent_dir.join(package); if ctx.exists(&package_dir)? { return Ok(package_dir); } // Try package-origversion only let package_dir = parent_dir.join(format!("{}-{}", package, origversion)); if ctx.exists(&package_dir)? { return Ok(package_dir); } // List all directories under 'package/' and log them let package_parent = parent_dir; if ctx.exists(package_parent)? { log::debug!( "Listing all directories under '{}':", package_parent.display() ); let entries = ctx.list_files(package_parent)?; let mut found_dirs = Vec::new(); for entry in entries { if entry.is_dir() { if let Some(file_name) = entry.file_name() { found_dirs.push(file_name.to_string_lossy().into_owned()); } log::debug!(" - {}", entry.display()); } } // If we found directories but none matched our patterns, provide helpful error if !found_dirs.is_empty() { return Err(format!( "Could not find package directory for {} in {}. Found directories: {}", package, parent_dir.display(), found_dirs.join(", ") ) .into()); } } Err(format!( "Could not find package directory for {} in {}", package, parent_dir.display() ) .into()) } fn find_dsc_file( build_root: &str, package: &str, version: &str, ) -> Result> { // Strip epoch from version (e.g., "1:2.3.4-5" -> "2.3.4-5") let version_without_epoch = version.split_once(':').map(|(_, v)| v).unwrap_or(version); let dsc_name = format!("{}_{}.dsc", package, version_without_epoch); let dsc_path = PathBuf::from(build_root).join(&dsc_name); // Check if the .dsc file exists in current context let ctx = context::current(); if !ctx.exists(&dsc_path)? { return Err(format!("Could not find .dsc file at {}", dsc_path.display()).into()); } Ok(dsc_path) } #[cfg(test)] mod tests { use serial_test::serial; async fn test_build_end_to_end( package: &str, series: &str, dist: Option<&str>, arch: Option<&str>, cross: bool, ) { log::info!( "Starting end-to-end test for package: {} (series: {}, arch: {:?}, cross: {})", package, series, arch, cross ); let temp_dir = tempfile::tempdir().unwrap(); let cwd = temp_dir.path(); log::debug!("Created temporary directory: {}", cwd.display()); log::info!("Pulling package {} from {}...", package, series); let package_info = crate::package_info::lookup(package, None, Some(series), "", dist, None) .await .expect("Cannot lookup package information"); crate::pull::pull(&package_info, Some(cwd), None, true) .await .expect("Cannot pull package"); log::info!("Successfully pulled package {}", package); // Change directory to the package directory let cwd = crate::deb::find_package_directory(cwd, package, &package_info.stanza.version) .expect("Cannot find package directory"); log::debug!("Package directory: {}", cwd.display()); log::info!("Starting binary package build..."); crate::deb::build_binary_package(arch, Some(series), Some(&cwd), cross, None) .await .expect("Cannot build binary package (deb)"); log::info!("Successfully built binary package"); // Check that the .deb files are present let parent_dir = cwd.parent().expect("Cannot find parent directory"); let deb_files = std::fs::read_dir(parent_dir) .expect("Cannot read build directory") .filter_map(|entry| entry.ok()) .filter(|entry| entry.path().extension().is_some_and(|ext| ext == "deb")) .collect::>(); log::info!("Found {} .deb files after build", deb_files.len()); for file in &deb_files { log::debug!(" - {}", file.path().display()); } assert!(!deb_files.is_empty(), "No .deb files found after build"); log::info!( "End-to-end test completed successfully for package: {}", package ); } // Tests below will be marked 'serial' // As builds are using ephemeral contexts, tests running on the same // process could use the ephemeral context of another thread and // interfere with each other. // FIXME: This is not ideal. In the future, we might want to // either explicitely pass context (instead of shared state) or // fork for building? #[tokio::test] #[test_log::test] #[serial] async fn test_deb_hello_ubuntu_end_to_end() { test_build_end_to_end("hello", "noble", None, None, false).await; } #[tokio::test] #[test_log::test] #[cfg(target_arch = "x86_64")] #[serial] async fn test_deb_hello_ubuntu_cross_end_to_end() { test_build_end_to_end("hello", "noble", None, Some("riscv64"), true).await; } /// This is a specific test case for the latest gcc package on Debian /// The GCC package is complex and hard to build, with specific stages /// and system-bound scripts. Building it requires specific things that /// we want to ensure are not broken. /// NOTE: Ideally, we want to run this in CI, but it takes more than 20h /// to fully build the gcc-15 package on an amd64 builder, which is too /// much time. #[ignore] #[cfg(target_arch = "x86_64")] #[tokio::test] #[test_log::test] #[serial] async fn test_deb_gcc_debian_end_to_end() { test_build_end_to_end("gcc-15", "sid", None, None, false).await; } }