/// Local binary package building /// Directly calling 'debian/rules' in current context use crate::context::Context; use crate::deb::find_dsc_file; use log::warn; use std::collections::HashMap; use std::error::Error; use std::path::Path; use std::sync::Arc; use crate::apt; use crate::deb::cross; #[allow(clippy::too_many_arguments)] pub async fn build( package: &str, version: &str, arch: &str, series: &str, pocket: Option<&str>, build_root: &str, cross: bool, ppa: Option<&[&str]>, inject_packages: Option<&[&str]>, ctx: Arc, ) -> Result<(), Box> { // Environment let mut env = HashMap::::new(); env.insert("LANG".to_string(), "C".to_string()); env.insert("DEBIAN_FRONTEND".to_string(), "noninteractive".to_string()); // Parallel building: find local number of cores, and use that let num_cores = ctx .command("nproc") .output() .map(|output| { if output.status.success() { String::from_utf8_lossy(&output.stdout) .trim() .parse::() .unwrap_or(1) } else { 1 // Default to 1 if nproc fails } }) .unwrap_or(1); // Default to 1 if we can't execute the command // Build options: parallel, disable tests by default env.insert( "DEB_BUILD_OPTIONS".to_string(), format!("parallel={} nocheck", num_cores), ); if cross { log::debug!("Setting up environment for local cross build..."); cross::setup_environment(&mut env, arch, ctx.clone())?; cross::ensure_repositories(arch, series, pocket, ctx.clone())?; } let mut sources = apt::sources::load(Some(ctx.clone()))?; let mut modified = false; let mut added_ppas: Vec<(&str, &str)> = Vec::new(); // Add PPA repositories if specified if let Some(ppas) = ppa { for ppa_str in ppas { // PPA format: user/ppa_name let parts: Vec<&str> = ppa_str.split('/').collect(); if parts.len() == 2 { let base_url = crate::package_info::ppa_to_base_url(parts[0], parts[1]); // Add new PPA source if not found if !sources.iter().any(|s| s.uri.contains(&base_url)) { // Get host and target architectures let host_arch = crate::get_current_arch(); let target_arch = arch; // Create architectures list with both host and target if different let mut architectures = vec![host_arch.clone()]; if host_arch != *target_arch { architectures.push(target_arch.to_string()); } // Create suite list with all Ubuntu series let suites = vec![format!("{}", series)]; let new_source = crate::apt::sources::SourceEntry { enabled: true, components: vec!["main".to_string()], architectures: architectures.clone(), suite: suites, uri: base_url, }; sources.push(new_source); modified = true; added_ppas.push((parts[0], parts[1])); log::info!( "Added PPA: {} for series {} with architectures {:?}", ppa_str, series, architectures ); } } else { return Err( format!("Invalid PPA format: '{}'. Expected: user/ppa_name", ppa_str).into(), ); } } } // UBUNTU: Ensure 'universe' repository is enabled for source in &mut sources { if source.uri.contains("ubuntu") && !source.components.contains(&"universe".to_string()) { source.components.push("universe".to_string()); modified = true; } } // Enable the requested pocket on archive sources, so build-dependencies // are resolved from that pocket if let Some(pocket_name) = pocket { let pocket_suite = format!("{series}-{pocket_name}"); log::info!("Enabling pocket '{}' for build dependencies", pocket_suite); for source in &mut sources { if crate::deb::is_archive_source(&source.uri) && !source.suite.contains(&pocket_suite) { source.suite.push(pocket_suite.clone()); modified = true; } } } if modified { apt::sources::save_legacy(Some(ctx.clone()), sources, "/etc/apt/sources.list")?; // Download and import PPA keys for all added PPAs for (user, ppa_name) in added_ppas { if let Err(e) = crate::apt::keyring::download_trust_ppa_key(Some(ctx.clone()), user, ppa_name).await { warn!( "Failed to download PPA key for {}/{}: {}", user, ppa_name, e ); } } } // Update package lists log::debug!("Updating package lists for local build..."); let status = ctx .command("apt-get") .envs(env.clone()) .arg("update") .status() .map_err(|e| { format!( "Failed to run 'apt-get update' inside the build context: {}. \ If this is a local build, make sure apt-get is available and \ try executing with sudo.", e ) })?; if !status.success() { return Err("apt-get update failed inside the build context. \ If this is a local build, try executing with sudo, \ or re-run with RUST_LOG=debug for more details." .into()); } // Install essential packages log::debug!("Installing essential packages for local build..."); let mut cmd = ctx.command("apt-get"); cmd.envs(env.clone()) .arg("-y") .arg("install") .arg("build-essential") .arg("dose-builddebcheck") .arg("fakeroot"); if cross { cmd.arg(format!("crossbuild-essential-{arch}")); cmd.arg(format!("libc6-{arch}-cross")); cmd.arg(format!("libc6-dev-{arch}-cross")); cmd.arg("dpkg-cross"); cmd.arg(format!("libc6:{arch}")); cmd.arg(format!("libc6-dev:{arch}")); } let status = cmd.status()?; if !status.success() { return Err("Could not install essential packages for the build".into()); } // Find the actual package directory let package_dir = crate::deb::find_package_directory(Path::new(build_root), package, version, &ctx)?; let package_dir_str = package_dir .to_str() .ok_or("Invalid package directory path")?; // Apply quilt patches if the package provides a patch series apply_quilt_patches(package_dir_str, &env, ctx.clone())?; // Install injected packages if specified if let Some(packages) = inject_packages { install_injected_packages(packages, &env, ctx.clone())?; } // Install arch-specific build dependencies log::debug!("Installing arch-specific build dependencies..."); let mut cmd = ctx.command("apt-get"); cmd.current_dir(package_dir_str) .envs(env.clone()) .arg("-y") .arg("build-dep"); if cross { cmd.arg(format!("--host-architecture={arch}")); } cmd.arg("--arch-only"); let status = cmd.arg("./").status()?; // If build-dep fails, we try to explain the failure using dose-debcheck if !status.success() { dose3_explain_dependencies(package, version, arch, build_root, cross, ctx.clone())?; return Err("Could not install build-dependencies for the build".into()); } // Install arch-independant build dependencies log::debug!("Installing arch-independant build dependencies..."); let status = ctx .command("apt-get") .current_dir(package_dir_str) .envs(env.clone()) .arg("-y") .arg("build-dep") .arg("./") .status()?; // If build-dep fails, we try to explain the failure using dose-debcheck if !status.success() { dose3_explain_dependencies(package, version, arch, build_root, cross, ctx.clone())?; return Err("Could not install build-dependencies for the build".into()); } // Run the build step log::debug!("Building (debian/rules build) package..."); let status = ctx .command("debian/rules") .current_dir(package_dir_str) .envs(env.clone()) .arg("build") .status()?; if !status.success() { return Err("Error while building the package".into()); } // Run the 'binary' step to produce deb let status = ctx .command("fakeroot") .current_dir(package_dir_str) .envs(env.clone()) .arg("debian/rules") .arg("binary") .status()?; if !status.success() { return Err( "Error while building the binary artifacts (.deb) from the built package".into(), ); } Ok(()) } /// Apply quilt patches before building, if the package provides a /// 'debian/patches/series' file fn apply_quilt_patches( package_dir: &str, env: &HashMap, ctx: Arc, ) -> Result<(), Box> { let series_path = Path::new(package_dir).join("debian/patches/series"); if !ctx.exists(&series_path)? { log::debug!( "No '{}' found, skipping quilt patch application", series_path.display() ); return Ok(()); } // Skip patch application if the series file contains no patches let series_content = ctx.read_file(&series_path)?; let has_patches = series_content .lines() .any(|line| !line.trim().is_empty() && !line.trim().starts_with('#')); if !has_patches { log::debug!( "'{}' contains no patches, skipping quilt patch application", series_path.display() ); return Ok(()); } // Make sure quilt is available in the build context log::debug!("Installing quilt for patch application..."); let status = ctx .command("apt-get") .envs(env.clone()) .arg("-y") .arg("install") .arg("quilt") .status()?; if !status.success() { return Err("Could not install 'quilt', required to apply patches".into()); } // Apply all patches listed in the series log::info!("Applying quilt patches from debian/patches/series..."); let mut patch_env = env.clone(); patch_env.insert("QUILT_PATCHES".to_string(), "debian/patches".to_string()); let status = ctx .command("quilt") .current_dir(package_dir) .envs(patch_env) .arg("push") .arg("-a") .status()?; if !status.success() { return Err("Failed to apply quilt patches ('quilt push -a')".into()); } Ok(()) } fn install_injected_packages( packages: &[&str], env: &HashMap, ctx: Arc, ) -> Result<(), Box> { log::info!("Installing injected packages: {:?}", packages); // Separate .deb files from package names let mut deb_files: Vec = Vec::new(); let mut package_names: Vec<&str> = Vec::new(); for pkg in packages { // Check if it's a .deb file path (ends with .deb and exists as a file) let pkg_path = Path::new(pkg); if pkg.ends_with(".deb") && pkg_path.exists() { // Copy the .deb file into the build context let dest_root = ctx.create_temp_dir()?; let chroot_path = ctx.ensure_available(pkg_path, &dest_root)?; log::debug!( "Copied .deb file '{}' to chroot path '{}'", pkg, chroot_path.display() ); deb_files.push(chroot_path.to_string_lossy().to_string()); } else { package_names.push(pkg); } } // Install .deb files if !deb_files.is_empty() || !package_names.is_empty() { log::info!("Installing .deb files: {:?}", deb_files); let mut cmd = ctx.command("apt-get"); cmd.envs(env.clone()) .arg("-y") .arg("--allow-downgrades") .arg("install"); // Add the .deb file paths with ./ prefix for apt to recognize them as local files for deb_path in &deb_files { cmd.arg(format!("./{}", deb_path.trim_start_matches('/'))); } if !package_names.is_empty() { cmd.args(&package_names); } let status = cmd.status()?; if !status.success() { return Err(format!("Could not install injected packages: {:?}", deb_files).into()); } } Ok(()) } fn dose3_explain_dependencies( package: &str, version: &str, arch: &str, build_root: &str, cross: bool, ctx: Arc, ) -> Result<(), Box> { // Construct the list of Packages files let mut bg_args = Vec::new(); let mut cmd = ctx.command("apt-get"); cmd.arg("indextargets") .arg("--format") .arg("$(FILENAME)") .arg("Created-By: Packages"); let output = cmd.output()?; if output.status.success() { let filenames = String::from_utf8_lossy(&output.stdout); for file in filenames.lines() { let file = file.trim(); if !file.is_empty() { bg_args.push(file.to_string()); } } } // Transform the dsc file into a 'Source' stanza (replacing 'Source' with 'Package') // TODO: Remove potential GPG headers/signature let dsc_path = find_dsc_file(build_root, package, version, &ctx)?; let mut dsc_content = ctx.read_file(&dsc_path)?; dsc_content = dsc_content.replace("Source", "Package"); ctx.write_file( Path::new(&format!("{build_root}/dsc-processed")), &dsc_content, )?; // Call dose-builddebcheck let local_arch = crate::get_current_arch(); let mut cmd = ctx.command("dose-builddebcheck"); cmd.arg("--verbose") .arg("--failures") .arg("--explain") .arg("--summary") .arg(format!("--deb-native-arch={}", local_arch)); if cross { cmd.arg(format!("--deb-host-arch={}", arch)) .arg("--deb-profiles=cross") .arg(format!("--deb-foreign-archs={}", arch)); } cmd.args(bg_args).arg(format!("{build_root}/dsc-processed")); cmd.status()?; Ok(()) }