/// Local binary package building /// Directly calling 'debian/rules' in current context use crate::context; use crate::deb::find_dsc_file; use log::warn; use std::collections::HashMap; use std::error::Error; use std::path::Path; use crate::apt; use crate::deb::cross; #[allow(clippy::too_many_arguments)] pub async fn build( package: &str, version: &str, arch: &str, series: &str, build_root: &str, cross: bool, ppa: Option<&[&str]>, inject_packages: Option<&[&str]>, ) -> 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()); let ctx = context::current(); // 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)?; cross::ensure_repositories(arch, series)?; } let mut sources = apt::sources::load(None)?; 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; } } if modified { apt::sources::save_legacy(None, 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(None, 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()?; if !status.success() { return Err( "Could not execute apt-get update. If this is a local build, try executing with sudo." .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")?; // Install injected packages if specified if let Some(packages) = inject_packages { log::info!("Installing injected packages: {:?}", packages); let mut cmd = ctx.command("apt-get"); cmd.envs(env.clone()) .arg("-y") .arg("--allow-downgrades") .arg("install") .args(packages); let status = cmd.status()?; if !status.success() { return Err(format!("Could not install injected packages: {:?}", packages).into()); } } // 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)?; 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)?; 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(()) } fn dose3_explain_dependencies( package: &str, version: &str, arch: &str, build_root: &str, cross: bool, ) -> Result<(), Box> { let ctx = context::current(); // 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)?; 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(()) }