626 lines
20 KiB
Rust
626 lines
20 KiB
Rust
/// Local binary package building
|
|
/// Directly calling 'debian/rules' in current context
|
|
use crate::context::{Context, ContextCommand, LineSink};
|
|
use crate::deb::find_dsc_file;
|
|
use crate::ui::deb::{DebUi, Phase};
|
|
use crate::ui::logfmt::QuiltClassifier;
|
|
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;
|
|
|
|
/// Attach the capture sink to a command when the live UI is active
|
|
fn cap<'a>(
|
|
cmd: &'a mut ContextCommand<'a>,
|
|
sink: &Option<Arc<dyn LineSink>>,
|
|
) -> &'a mut ContextCommand<'a> {
|
|
if let Some(s) = sink {
|
|
cmd.capture(s.clone());
|
|
}
|
|
cmd
|
|
}
|
|
|
|
#[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<Context>,
|
|
ui: Option<Arc<DebUi>>,
|
|
) -> Result<(), Box<dyn Error>> {
|
|
let sink: Option<Arc<dyn LineSink>> = ui.as_ref().map(|u| u.sink());
|
|
|
|
// 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());
|
|
|
|
// 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, 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![series.to_string()];
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
// 'proposed' pockets are marked 'NotAutomatic' in their Release file,
|
|
// giving them an apt priority of 1: without an explicit pin, apt
|
|
// would ignore them even when enabled
|
|
if pocket_name.starts_with("proposed") {
|
|
pin_pocket(&pocket_suite, &ctx)?;
|
|
}
|
|
}
|
|
|
|
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...");
|
|
if let Some(u) = &ui {
|
|
u.phase(Phase::UpdatingPackageLists);
|
|
}
|
|
let status = cap(
|
|
ctx.command("apt-get").envs(env.clone()).arg("update"),
|
|
&sink,
|
|
)
|
|
.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}"));
|
|
}
|
|
if let Some(u) = &ui {
|
|
u.phase(Phase::InstallingEssentials);
|
|
}
|
|
let status = cap(&mut cmd, &sink).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")?;
|
|
|
|
// Reproducibility: export SOURCE_DATE_EPOCH from the changelog entry,
|
|
// like dpkg-buildpackage does.
|
|
match ctx.read_file(&package_dir.join("debian/changelog")) {
|
|
Ok(content) => {
|
|
if let Ok(entry) = crate::debian::parse_changelog_entry_from_str(&content) {
|
|
env.insert("SOURCE_DATE_EPOCH".to_string(), entry.timestamp.to_string());
|
|
}
|
|
}
|
|
Err(e) => log::debug!("cannot read changelog for SOURCE_DATE_EPOCH: {}", e),
|
|
}
|
|
|
|
// Apply quilt patches if the package provides a patch series
|
|
apply_quilt_patches(package_dir_str, &env, ctx.clone(), &ui, &sink)?;
|
|
|
|
// Install injected packages if specified
|
|
if let Some(packages) = inject_packages {
|
|
install_injected_packages(packages, &env, ctx.clone(), &ui, &sink)?;
|
|
}
|
|
|
|
// Install arch-specific build dependencies
|
|
log::debug!("Installing arch-specific build dependencies...");
|
|
if let Some(u) = &ui {
|
|
u.phase(Phase::InstallingBuildDeps);
|
|
}
|
|
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 = cap(&mut cmd, &sink).arg("./").status()?;
|
|
|
|
// If build-dep fails, we try to explain the failure using dose-debcheck
|
|
if !status.success() {
|
|
if let Some(u) = &ui {
|
|
u.suspend();
|
|
}
|
|
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 = cap(
|
|
ctx.command("apt-get")
|
|
.current_dir(package_dir_str)
|
|
.envs(env.clone())
|
|
.arg("-y")
|
|
.arg("build-dep")
|
|
.arg("./"),
|
|
&sink,
|
|
)
|
|
.status()?;
|
|
|
|
// If build-dep fails, we try to explain the failure using dose-debcheck
|
|
if !status.success() {
|
|
if let Some(u) = &ui {
|
|
u.suspend();
|
|
}
|
|
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...");
|
|
if let Some(u) = &ui {
|
|
u.phase(Phase::Building);
|
|
}
|
|
let status = cap(
|
|
ctx.command("debian/rules")
|
|
.current_dir(package_dir_str)
|
|
.envs(env.clone())
|
|
.arg("build"),
|
|
&sink,
|
|
)
|
|
.status()?;
|
|
if !status.success() {
|
|
return Err("Error while building the package".into());
|
|
}
|
|
|
|
// Run the 'binary' step to produce deb
|
|
if let Some(u) = &ui {
|
|
u.phase(Phase::ProducingBinaries);
|
|
}
|
|
let status = cap(
|
|
ctx.command("fakeroot")
|
|
.current_dir(package_dir_str)
|
|
.envs(env.clone())
|
|
.arg("debian/rules")
|
|
.arg("binary"),
|
|
&sink,
|
|
)
|
|
.status()?;
|
|
if !status.success() {
|
|
return Err(
|
|
"Error while building the binary artifacts (.deb) from the built package".into(),
|
|
);
|
|
}
|
|
|
|
// Generate the upload metadata (.buildinfo + .changes) natively, the
|
|
// equivalent of dpkg-genbuildinfo -b + dpkg-genchanges -b, consuming
|
|
// debian/files produced by the build. Failures are logged but do not
|
|
// discard the produced binaries.
|
|
if let Err(e) = generate_upload_metadata(package_dir_str, build_root, arch, cross, &env, &ctx) {
|
|
warn!("failed to generate .buildinfo/.changes: {}", e);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Generate `.buildinfo` and `.changes` for the finished binary build,
|
|
/// inside the build context.
|
|
fn generate_upload_metadata(
|
|
package_dir: &str,
|
|
build_root: &str,
|
|
arch: &str,
|
|
cross: bool,
|
|
env: &HashMap<String, String>,
|
|
ctx: &Arc<Context>,
|
|
) -> Result<(), Box<dyn Error>> {
|
|
use std::path::Path;
|
|
|
|
let changelog_path = Path::new(package_dir).join("debian/changelog");
|
|
let changelog_content = ctx.read_file(&changelog_path)?;
|
|
let entry = crate::debian::parse_changelog_entry_from_str(&changelog_content)?;
|
|
|
|
// Build architecture: the machine inside the build context.
|
|
let build_arch = ctx
|
|
.command("dpkg")
|
|
.arg("--print-architecture")
|
|
.output()
|
|
.ok()
|
|
.filter(|o| o.status.success())
|
|
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
|
|
.filter(|s| !s.is_empty())
|
|
.unwrap_or_else(crate::get_current_arch);
|
|
let host_arch = if cross {
|
|
arch.to_string()
|
|
} else {
|
|
build_arch.clone()
|
|
};
|
|
|
|
// Vendor resolution inside the context (falls back to the host view).
|
|
let vendor = ctx
|
|
.read_file(Path::new("/etc/dpkg/origins/default"))
|
|
.ok()
|
|
.and_then(|content| {
|
|
for line in content.lines() {
|
|
if let Some(v) = line.strip_prefix("Vendor:") {
|
|
let v = v.trim();
|
|
if !v.is_empty() {
|
|
return Some(v.to_string());
|
|
}
|
|
}
|
|
}
|
|
None
|
|
})
|
|
.unwrap_or_else(crate::build::env::current_vendor);
|
|
|
|
let profiles = crate::build::env::resolve_build_profiles(&[], &vendor);
|
|
let source_date_epoch = env
|
|
.get("SOURCE_DATE_EPOCH")
|
|
.and_then(|v| v.parse::<i64>().ok())
|
|
.unwrap_or(entry.timestamp);
|
|
|
|
let opts = crate::build::binary::BinaryMetadataOptions {
|
|
profiles,
|
|
vendor,
|
|
parallel: crate::build::env::num_parallel(),
|
|
source_date_epoch,
|
|
build_arch,
|
|
host_arch,
|
|
};
|
|
let (buildinfo, changes) = crate::build::binary::generate_binary_metadata(
|
|
ctx,
|
|
Path::new(package_dir),
|
|
Path::new(build_root),
|
|
&opts,
|
|
)?;
|
|
log::info!(
|
|
"generated upload metadata: {} and {}",
|
|
buildinfo.display(),
|
|
changes.display()
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
/// Apply quilt patches before building, if the package provides a
|
|
/// 'debian/patches/series' file
|
|
fn apply_quilt_patches(
|
|
package_dir: &str,
|
|
env: &HashMap<String, String>,
|
|
ctx: Arc<Context>,
|
|
ui: &Option<Arc<DebUi>>,
|
|
sink: &Option<Arc<dyn LineSink>>,
|
|
) -> Result<(), Box<dyn Error>> {
|
|
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 total_patches = series_content
|
|
.lines()
|
|
.filter(|line| !line.trim().is_empty() && !line.trim().starts_with('#'))
|
|
.count();
|
|
let has_patches = total_patches > 0;
|
|
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 = cap(
|
|
ctx.command("apt-get")
|
|
.envs(env.clone())
|
|
.arg("-y")
|
|
.arg("install")
|
|
.arg("quilt"),
|
|
sink,
|
|
)
|
|
.status()?;
|
|
if !status.success() {
|
|
return Err("Could not install 'quilt', required to apply patches".into());
|
|
}
|
|
|
|
// Apply all patches listed in the series
|
|
if let Some(u) = ui {
|
|
u.phase_with(
|
|
Phase::ApplyingPatches,
|
|
Box::new(QuiltClassifier::new(total_patches)),
|
|
);
|
|
}
|
|
let mut patch_env = env.clone();
|
|
patch_env.insert("QUILT_PATCHES".to_string(), "debian/patches".to_string());
|
|
let status = cap(
|
|
ctx.command("quilt")
|
|
.current_dir(package_dir)
|
|
.envs(patch_env)
|
|
.arg("push")
|
|
.arg("-a"),
|
|
sink,
|
|
)
|
|
.status()?;
|
|
if !status.success() {
|
|
return Err("Failed to apply quilt patches ('quilt push -a')".into());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Pin a 'NotAutomatic' pocket (e.g. '-proposed') so apt takes it into
|
|
/// account during dependency resolution.
|
|
///
|
|
/// Apt preferences are global: a single 'release' pin matches the pinned
|
|
/// suite on every repository carrying it (archive, security and ports),
|
|
/// for all architectures, so this also covers cross-builds pulling
|
|
/// dependencies from 'ports.ubuntu.com'.
|
|
fn pin_pocket(pocket_suite: &str, ctx: &Arc<Context>) -> Result<(), Box<dyn Error>> {
|
|
let pin_path = format!("/etc/apt/preferences.d/pkh-{}", pocket_suite);
|
|
let pin_content = format!(
|
|
"Package: *\nPin: release a={}\nPin-Priority: 600\n",
|
|
pocket_suite
|
|
);
|
|
log::info!("Pinning pocket '{}' with priority 600", pocket_suite);
|
|
ctx.write_file(Path::new(&pin_path), &pin_content)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn install_injected_packages(
|
|
packages: &[&str],
|
|
env: &HashMap<String, String>,
|
|
ctx: Arc<Context>,
|
|
ui: &Option<Arc<DebUi>>,
|
|
sink: &Option<Arc<dyn LineSink>>,
|
|
) -> Result<(), Box<dyn Error>> {
|
|
log::info!("Installing injected packages: {:?}", packages);
|
|
|
|
if let Some(u) = ui {
|
|
u.phase(Phase::InjectingPackages);
|
|
}
|
|
|
|
// Separate .deb files from package names
|
|
let mut deb_files: Vec<String> = 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 = cap(&mut cmd, sink).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<Context>,
|
|
) -> Result<(), Box<dyn Error>> {
|
|
// 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(())
|
|
}
|