Files
pkh/src/deb/local.rs
Valentin Haudiquet daaf33cd6b
Some checks failed
CI / build (push) Failing after 18m26s
CI / snap (push) Has been skipped
deb: fix race condition for test
Fix race condition around current context,
related to find_package_directory.
2026-03-18 23:34:33 +01:00

315 lines
10 KiB
Rust

/// 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<dyn Error>> {
// Environment
let mut env = HashMap::<String, String>::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::<usize>()
.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<dyn Error>> {
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(())
}