Fix race condition around current context, related to find_package_directory.
315 lines
10 KiB
Rust
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(())
|
|
}
|