One deb block per package cannot express two series needing different rules. Make pull and deb lists of entries, each with its own series scope: every matching entry applies, in file order, so a stonking entry can carry its own dependency rules next to the resolute one. Thread the series through find_package_directory for the package_directory lookup, which keeps its deb-then-pull fallback.
1361 lines
48 KiB
Rust
1361 lines
48 KiB
Rust
/// Local binary package building
|
|
/// Directly calling 'debian/rules' in current context
|
|
use crate::context::{Context, ContextCommand, LineSink};
|
|
use crate::deb::{Phase, enter_phase, find_dsc_file};
|
|
use crate::logfmt::QuiltClassifier;
|
|
use crate::report::BuildView;
|
|
use log::warn;
|
|
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
|
use std::error::Error;
|
|
use std::path::{Path, PathBuf};
|
|
use std::sync::Arc;
|
|
|
|
use crate::apt;
|
|
use crate::deb::cross;
|
|
use crate::debian::control::ControlInfo;
|
|
use crate::debian::deps::{Deps, Facts, ParseOpts, PkgRelation};
|
|
|
|
/// 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: &[String],
|
|
inject_packages: &[String],
|
|
ctx: Arc<Context>,
|
|
view: &dyn BuildView,
|
|
jobs: Option<usize>,
|
|
) -> Result<Vec<PathBuf>, Box<dyn Error>> {
|
|
let sink: Option<Arc<dyn LineSink>> = view.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: honor an explicit -j/--jobs count, otherwise detect
|
|
// the number of cores available inside the build context (nproc).
|
|
let num_cores = match jobs {
|
|
Some(j) => j,
|
|
None => 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
|
|
for ppa_str in ppa {
|
|
let (ppa_user, ppa_name) = crate::package_info::split_ppa(ppa_str)?;
|
|
let base_url = crate::package_info::ppa_to_base_url(ppa_user, ppa_name);
|
|
|
|
// 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,
|
|
kind: crate::apt::sources::SourceKind::Deb,
|
|
components: vec!["main".to_string()],
|
|
architectures: architectures.clone(),
|
|
signed_by: None,
|
|
trusted: None,
|
|
suite: suites,
|
|
uri: base_url,
|
|
// No origin: saved to the pkh-owned added-sources file
|
|
origin: None,
|
|
};
|
|
sources.push(new_source);
|
|
modified = true;
|
|
added_ppas.push((ppa_user, ppa_name));
|
|
log::info!(
|
|
"Added PPA: {} for series {} with architectures {:?}",
|
|
ppa_str,
|
|
series,
|
|
architectures
|
|
);
|
|
}
|
|
}
|
|
|
|
// UBUNTU: Ensure the 'universe' component is enabled on official
|
|
// Ubuntu sources (many build dependencies live there). The old
|
|
// `uri.contains("ubuntu")` gate also caught third-party repositories
|
|
// whose URL merely mentions Ubuntu; the mirror-data check leaves them
|
|
// alone. 'universe' is only added when Ubuntu's component list still
|
|
// carries it.
|
|
let ubuntu_components = crate::distro_info::get_dist_components("ubuntu")?;
|
|
if ubuntu_components.iter().any(|c| c == "universe") {
|
|
for source in &mut sources {
|
|
if crate::distro_info::is_official_source("ubuntu", &source.uri)
|
|
&& !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 {
|
|
// Each entry is written back to its origin file in its own format;
|
|
// new PPA entries go to the pkh-owned added-sources file
|
|
apt::sources::save(Some(ctx.clone()), sources)?;
|
|
|
|
// 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...");
|
|
enter_phase(view, 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}"));
|
|
}
|
|
enter_phase(view, 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
|
|
// Find the actual package directory
|
|
let package_dir =
|
|
crate::deb::find_package_directory(Path::new(build_root), package, version, series, &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(), view, &sink)?;
|
|
|
|
// Install injected packages if specified
|
|
if !inject_packages.is_empty() {
|
|
install_injected_packages(inject_packages, &env, ctx.clone(), view, &sink)?;
|
|
}
|
|
|
|
// Resolve and install the Build-* dependencies with dpkg's cross
|
|
// semantics; this replaces the historical `apt-get build-dep` passes,
|
|
// whose `--host-architecture` resolution cannot express the
|
|
// Multi-Arch-aware variant choice dpkg's checker requires.
|
|
install_build_dependencies(
|
|
package,
|
|
version,
|
|
arch,
|
|
series,
|
|
package_dir_str,
|
|
build_root,
|
|
cross,
|
|
&env,
|
|
ctx.clone(),
|
|
view,
|
|
&sink,
|
|
)?;
|
|
|
|
// Run the build step
|
|
log::debug!("Building (debian/rules build) package...");
|
|
enter_phase(view, 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
|
|
enter_phase(view, 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(),
|
|
);
|
|
}
|
|
|
|
// Collect the exact set of artifacts produced by this build so the caller
|
|
// retrieves only those: the binary packages registered in debian/files
|
|
// (the contract between dh_builddeb/dpkg-gencontrol and the artifact
|
|
// generators) plus the .buildinfo/.changes generated below. This avoids
|
|
// globbing the build root, which would also surface stale files copied
|
|
// alongside the package tree.
|
|
let mut artifacts = collect_binary_artifacts(&ctx, package_dir_str, build_root)?;
|
|
|
|
// 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.
|
|
match generate_upload_metadata(package_dir_str, build_root, arch, cross, &env, &ctx) {
|
|
Ok((buildinfo, changes)) => {
|
|
log::info!(
|
|
"generated upload metadata: {} and {}",
|
|
buildinfo.display(),
|
|
changes.display()
|
|
);
|
|
artifacts.push(buildinfo);
|
|
artifacts.push(changes);
|
|
}
|
|
Err(e) => {
|
|
warn!("failed to generate .buildinfo/.changes: {}", e);
|
|
}
|
|
}
|
|
|
|
Ok(artifacts)
|
|
}
|
|
|
|
/// One candidate binary package for a dependency name, as reported by
|
|
/// `apt-cache show`.
|
|
#[derive(Debug, Clone)]
|
|
struct Candidate {
|
|
/// Debian architecture of the candidate (`amd64`, `arm64`, `all`, ...).
|
|
arch: String,
|
|
/// Multi-Arch attribute; an absent field means `no`.
|
|
multiarch: String,
|
|
}
|
|
|
|
/// Pure core of the install strategy: the apt install specification that
|
|
/// satisfies `rel` among the `candidates` available for its package name.
|
|
///
|
|
/// The mapping reproduces the dpkg cross semantics that
|
|
/// [`crate::debian::deps`] evaluates against (`Dpkg::Deps::KnownFacts
|
|
/// _find_package`), preferring the runnable build-architecture variant
|
|
/// whenever dpkg's rules accept several:
|
|
///
|
|
/// - unqualified: a `Multi-Arch: foreign` build-architecture candidate
|
|
/// (tools: the variant that runs on the build machine), then the
|
|
/// host-architecture candidate (the variant dpkg checks unqualified
|
|
/// dependencies against), then an `Architecture: all` candidate. A
|
|
/// foreign-arch candidate of a non-foreign package satisfies nothing in
|
|
/// cross mode — installing it would diverge from the checker;
|
|
/// - `:native`: the build-architecture candidate (`Architecture: all`
|
|
/// accepted); a `Multi-Arch: foreign` candidate aborts the lookup,
|
|
/// like dpkg's checker;
|
|
/// - `:any`: a `Multi-Arch: allowed` candidate, host architecture first;
|
|
/// - explicit `:arch`: exactly that candidate;
|
|
/// - a name with no candidate at all is passed through unqualified, so
|
|
/// apt resolves virtual packages (`debhelper-compat`, dbus session
|
|
/// alternatives, ...) to a satisfying provider.
|
|
///
|
|
/// `None` when no candidate can satisfy the relation per dpkg semantics:
|
|
/// the caller falls through to the next alternative of the clause.
|
|
fn install_spec_for(
|
|
rel: &PkgRelation,
|
|
candidates: &[Candidate],
|
|
build_arch: &str,
|
|
host_arch: &str,
|
|
) -> Option<String> {
|
|
let qualified = |arch: &str| format!("{}:{}", rel.package, arch);
|
|
match rel.arch_qualifier.as_deref() {
|
|
Some("native") => {
|
|
if candidates
|
|
.iter()
|
|
.any(|c| c.arch == build_arch && c.multiarch == "foreign")
|
|
{
|
|
return None;
|
|
}
|
|
candidates
|
|
.iter()
|
|
.find(|c| c.arch == build_arch)
|
|
.map(|_| qualified(build_arch))
|
|
.or_else(|| {
|
|
candidates
|
|
.iter()
|
|
.find(|c| c.arch == "all")
|
|
.map(|_| rel.package.clone())
|
|
})
|
|
}
|
|
Some("any") => candidates
|
|
.iter()
|
|
.filter(|c| c.multiarch == "allowed")
|
|
.find(|c| c.arch == host_arch)
|
|
.or_else(|| candidates.iter().find(|c| c.multiarch == "allowed"))
|
|
.map(|c| qualified(&c.arch)),
|
|
Some(qual) => candidates
|
|
.iter()
|
|
.find(|c| c.arch == qual)
|
|
.map(|_| qualified(qual)),
|
|
None => {
|
|
if candidates.is_empty() {
|
|
return Some(rel.package.clone());
|
|
}
|
|
candidates
|
|
.iter()
|
|
.find(|c| c.arch == build_arch && c.multiarch == "foreign")
|
|
.map(|_| qualified(build_arch))
|
|
.or_else(|| {
|
|
candidates
|
|
.iter()
|
|
.find(|c| c.arch == host_arch)
|
|
.map(|_| qualified(host_arch))
|
|
})
|
|
.or_else(|| {
|
|
candidates
|
|
.iter()
|
|
.find(|c| c.arch == "all")
|
|
.map(|_| rel.package.clone())
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Pure core of the resolver loop: the next batch of install
|
|
/// specifications, one alternative per still-unsatisfied clause,
|
|
/// advancing each clause's cursor past the alternatives that no candidate
|
|
/// can satisfy per dpkg semantics.
|
|
fn next_install_pass(
|
|
clauses: &[Vec<PkgRelation>],
|
|
unsatisfied: &[usize],
|
|
cursors: &mut [usize],
|
|
candidates: &BTreeMap<String, Vec<Candidate>>,
|
|
build_arch: &str,
|
|
host_arch: &str,
|
|
) -> Vec<String> {
|
|
let mut specs = Vec::new();
|
|
for &i in unsatisfied {
|
|
let empty = Vec::new();
|
|
while cursors[i] < clauses[i].len() {
|
|
let rel = &clauses[i][cursors[i]];
|
|
let cands = candidates.get(&rel.package).unwrap_or(&empty);
|
|
let spec = install_spec_for(rel, cands, build_arch, host_arch);
|
|
cursors[i] += 1;
|
|
if let Some(spec) = spec {
|
|
specs.push(spec);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
specs
|
|
}
|
|
|
|
/// Query the candidates available in the build context for `names`: one
|
|
/// entry per (package, architecture). The plain name surfaces the
|
|
/// build-architecture and `all` candidates; the host-qualified name is
|
|
/// what surfaces foreign-arch candidates. Names missing from the indexes
|
|
/// and virtual names simply produce no stanza.
|
|
fn query_candidates(
|
|
ctx: &Arc<Context>,
|
|
names: &[String],
|
|
build_arch: &str,
|
|
host_arch: &str,
|
|
) -> BTreeMap<String, Vec<Candidate>> {
|
|
let mut query: Vec<String> = Vec::new();
|
|
for name in names {
|
|
query.push(name.clone());
|
|
if host_arch != build_arch {
|
|
query.push(format!("{name}:{host_arch}"));
|
|
}
|
|
}
|
|
let Ok(output) = ctx.command("apt-cache").arg("show").args(&query).output() else {
|
|
return BTreeMap::new();
|
|
};
|
|
|
|
let mut map: BTreeMap<String, Vec<Candidate>> = BTreeMap::new();
|
|
for para in crate::debian::control::parse_paragraphs(&String::from_utf8_lossy(&output.stdout)) {
|
|
let Some(name) = para.get("Package") else {
|
|
continue;
|
|
};
|
|
let Some(arch) = para.get("Architecture") else {
|
|
continue;
|
|
};
|
|
let entry = map.entry(name.to_string()).or_default();
|
|
if entry.iter().any(|c| c.arch == arch) {
|
|
continue;
|
|
}
|
|
entry.push(Candidate {
|
|
arch: arch.to_string(),
|
|
multiarch: para.get("Multi-Arch").unwrap_or("no").to_string(),
|
|
});
|
|
}
|
|
map
|
|
}
|
|
|
|
/// Build architecture inside the build context.
|
|
fn context_build_arch(ctx: &Arc<Context>) -> String {
|
|
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)
|
|
}
|
|
|
|
/// Active build profiles for the dependency evaluation: what the build
|
|
/// steps run with (`DEB_BUILD_PROFILES`, set to `cross` for cross
|
|
/// builds), else the vendor defaults.
|
|
fn build_profiles_for(env: &HashMap<String, String>, ctx: &Arc<Context>) -> Vec<String> {
|
|
match env.get("DEB_BUILD_PROFILES") {
|
|
Some(value) => value
|
|
.split(',')
|
|
.map(|p| p.trim().to_string())
|
|
.filter(|p| !p.is_empty())
|
|
.collect(),
|
|
None => {
|
|
let vendor = ctx
|
|
.read_file(Path::new("/etc/dpkg/origins/default"))
|
|
.ok()
|
|
.and_then(|content| crate::build::env::vendor_from_origins_content(&content))
|
|
.unwrap_or_else(crate::build::env::current_vendor);
|
|
crate::build::env::resolve_build_profiles(&[], &vendor)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Installed-package facts inside the build context. An unreadable status
|
|
/// database (fresh chroot) yields empty facts: everything declared then
|
|
/// installs.
|
|
fn load_context_facts(ctx: &Arc<Context>, build_arch: &str, host_arch: &str) -> Facts {
|
|
match ctx.read_file(Path::new("/var/lib/dpkg/status")) {
|
|
Ok(content) => Facts::from_status(&content, host_arch, build_arch),
|
|
Err(e) => {
|
|
log::debug!("cannot read the context dpkg status: {e}");
|
|
Facts::new(host_arch, build_arch)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Resolve and install the Build-* dependencies with dpkg's cross
|
|
/// semantics: the clauses of `debian/control` are evaluated natively (the
|
|
/// `crate::debian::deps` `dpkg-checkbuilddeps` equivalent) against the
|
|
/// context's installed-package state, and the unsatisfied ones install
|
|
/// through explicitly architecture-qualified names.
|
|
///
|
|
/// Build conflicts abort before anything installs. The loop installs one
|
|
/// alternative per still-unsatisfied clause per apt transaction and
|
|
/// re-evaluates after each; a clause whose alternatives no candidate can
|
|
/// satisfy per dpkg semantics aborts the build. On failure the dose3
|
|
/// diagnosis runs, like the historical `build-dep` passes.
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn install_build_dependencies(
|
|
package: &str,
|
|
version: &str,
|
|
arch: &str,
|
|
series: &str,
|
|
package_dir: &str,
|
|
build_root: &str,
|
|
cross: bool,
|
|
env: &HashMap<String, String>,
|
|
ctx: Arc<Context>,
|
|
view: &dyn BuildView,
|
|
sink: &Option<Arc<dyn LineSink>>,
|
|
) -> Result<(), Box<dyn Error>> {
|
|
enter_phase(view, Phase::InstallingBuildDeps);
|
|
|
|
let host_arch = arch.to_string();
|
|
let build_arch = context_build_arch(&ctx);
|
|
let profiles = build_profiles_for(env, &ctx);
|
|
|
|
let control_content = ctx
|
|
.read_file(&Path::new(package_dir).join("debian/control"))
|
|
.map_err(|e| format!("cannot read debian/control: {e}"))?;
|
|
let control = ControlInfo::parse_content(&control_content)
|
|
.map_err(|e| format!("invalid debian/control in {package_dir}: {e}"))?;
|
|
let source = &control.source;
|
|
let deps_value = ["Build-Depends", "Build-Depends-Arch", "Build-Depends-Indep"]
|
|
.iter()
|
|
.filter_map(|f| source.get(f))
|
|
.collect::<Vec<_>>()
|
|
.join(", ");
|
|
let conflicts_value = [
|
|
"Build-Conflicts",
|
|
"Build-Conflicts-Arch",
|
|
"Build-Conflicts-Indep",
|
|
]
|
|
.iter()
|
|
.filter_map(|f| source.get(f))
|
|
.collect::<Vec<_>>()
|
|
.join(", ");
|
|
if deps_value.trim().is_empty() && conflicts_value.trim().is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
let parse_opts = ParseOpts {
|
|
host_arch: host_arch.clone(),
|
|
build_arch: build_arch.clone(),
|
|
build_profiles: profiles,
|
|
reduce_restrictions: true,
|
|
union: false,
|
|
build_dep: true,
|
|
};
|
|
let mut clauses: Vec<Vec<PkgRelation>> = Deps::parse(&deps_value, &parse_opts)?
|
|
.clauses()
|
|
.map(<[PkgRelation]>::to_vec)
|
|
.collect();
|
|
// Package-specific workarounds (see data/quirks.yml), before anything
|
|
// derives candidate queries or install specs from the clauses.
|
|
crate::quirks::apply_dependency_quirks(package, series, &mut clauses, &parse_opts)?;
|
|
let conflict_clauses: Vec<Vec<PkgRelation>> = if conflicts_value.trim().is_empty() {
|
|
Vec::new()
|
|
} else {
|
|
let union_opts = ParseOpts {
|
|
union: true,
|
|
..parse_opts.clone()
|
|
};
|
|
Deps::parse(&conflicts_value, &union_opts)?
|
|
.clauses()
|
|
.map(<[PkgRelation]>::to_vec)
|
|
.collect()
|
|
};
|
|
|
|
let names: Vec<String> = clauses
|
|
.iter()
|
|
.chain(conflict_clauses.iter())
|
|
.flatten()
|
|
.map(|rel| rel.package.clone())
|
|
.collect::<BTreeSet<_>>()
|
|
.into_iter()
|
|
.collect();
|
|
let candidates = query_candidates(&ctx, &names, &build_arch, &host_arch);
|
|
|
|
let mut cursors = vec![0usize; clauses.len()];
|
|
loop {
|
|
let facts = load_context_facts(&ctx, &build_arch, &host_arch);
|
|
|
|
// Build conflicts use the same lookup as dependencies in dpkg's
|
|
// checker: a satisfied conflict clause aborts before anything
|
|
// installs.
|
|
let mut violated = Vec::new();
|
|
for alternatives in &conflict_clauses {
|
|
for rel in alternatives {
|
|
if facts.evaluate_relation(rel) == Some(true) {
|
|
violated.push(rel.output());
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if !violated.is_empty() {
|
|
view.suspend();
|
|
if let Err(e) =
|
|
dose3_explain_dependencies(package, version, arch, build_root, cross, ctx.clone())
|
|
{
|
|
log::debug!("dose-builddebcheck diagnosis failed: {e}");
|
|
}
|
|
return Err(format!(
|
|
"build dependencies conflict with installed packages: {}",
|
|
violated.join(", ")
|
|
)
|
|
.into());
|
|
}
|
|
|
|
let unsatisfied: Vec<usize> = (0..clauses.len())
|
|
.filter(|&i| {
|
|
!clauses[i]
|
|
.iter()
|
|
.any(|rel| facts.evaluate_relation(rel) == Some(true))
|
|
})
|
|
.collect();
|
|
if unsatisfied.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
let specs = next_install_pass(
|
|
&clauses,
|
|
&unsatisfied,
|
|
&mut cursors,
|
|
&candidates,
|
|
&build_arch,
|
|
&host_arch,
|
|
);
|
|
if specs.is_empty() {
|
|
view.suspend();
|
|
if let Err(e) =
|
|
dose3_explain_dependencies(package, version, arch, build_root, cross, ctx.clone())
|
|
{
|
|
log::debug!("dose-builddebcheck diagnosis failed: {e}");
|
|
}
|
|
let remaining: Vec<String> = unsatisfied
|
|
.iter()
|
|
.map(|&i| {
|
|
clauses[i]
|
|
.iter()
|
|
.map(PkgRelation::output)
|
|
.collect::<Vec<_>>()
|
|
.join(" | ")
|
|
})
|
|
.collect();
|
|
return Err(format!(
|
|
"Could not satisfy build dependencies per dpkg cross semantics: {}",
|
|
remaining.join(", ")
|
|
)
|
|
.into());
|
|
}
|
|
|
|
log::debug!("Installing build dependencies: {:?}", specs);
|
|
let mut cmd = ctx.command("apt-get");
|
|
cmd.envs(env.clone()).arg("-y").arg("install");
|
|
for spec in &specs {
|
|
cmd.arg(spec);
|
|
}
|
|
let status = cap(&mut cmd, sink).status()?;
|
|
if !status.success() {
|
|
view.suspend();
|
|
if let Err(e) =
|
|
dose3_explain_dependencies(package, version, arch, build_root, cross, ctx.clone())
|
|
{
|
|
log::debug!("dose-builddebcheck diagnosis failed: {e}");
|
|
}
|
|
return Err(format!(
|
|
"Could not install build-dependencies for the build: {}",
|
|
specs.join(" ")
|
|
)
|
|
.into());
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Collect the binary artifacts (.deb/.udeb) registered by the build in
|
|
/// `debian/files`, returning their paths inside the build context
|
|
/// (`<build_root>/<filename>`). `debian/files` is the canonical record of
|
|
/// which binary packages `debian/rules binary` produced, so we retrieve
|
|
/// exactly those instead of globbing the build root (which would also pick
|
|
/// up stale files copied alongside the package tree).
|
|
fn collect_binary_artifacts(
|
|
ctx: &Arc<Context>,
|
|
package_dir: &str,
|
|
build_root: &str,
|
|
) -> Result<Vec<PathBuf>, Box<dyn Error>> {
|
|
let files_content = ctx
|
|
.read_file(&Path::new(package_dir).join("debian/files"))
|
|
.unwrap_or_default();
|
|
let files_list = crate::debian::FilesList::parse(&files_content)
|
|
.map_err(|e| format!("invalid debian/files in {package_dir}: {e}"))?;
|
|
let upload_dir = Path::new(build_root);
|
|
let mut artifacts = Vec::new();
|
|
for entry in files_list.iter() {
|
|
if matches!(entry.package_type.as_deref(), Some("deb") | Some("udeb")) {
|
|
artifacts.push(upload_dir.join(&entry.filename));
|
|
}
|
|
}
|
|
Ok(artifacts)
|
|
}
|
|
|
|
/// 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<(PathBuf, PathBuf), Box<dyn Error>> {
|
|
// 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);
|
|
// shared `Vendor:`/`Origin:` parsing with the source-build path.
|
|
let vendor = ctx
|
|
.read_file(Path::new("/etc/dpkg/origins/default"))
|
|
.ok()
|
|
.and_then(|content| crate::build::env::vendor_from_origins_content(&content))
|
|
.unwrap_or_else(crate::build::env::current_vendor);
|
|
|
|
// The recorded profiles must describe what the build actually ran with:
|
|
// the DEB_BUILD_PROFILES exported to the build steps ('cross' for cross
|
|
// builds), else the vendor defaults.
|
|
let profiles = match env.get("DEB_BUILD_PROFILES") {
|
|
Some(value) => value
|
|
.split(',')
|
|
.map(|p| p.trim().to_string())
|
|
.filter(|p| !p.is_empty())
|
|
.collect(),
|
|
None => crate::build::env::resolve_build_profiles(&[], &vendor),
|
|
};
|
|
|
|
// Record exactly the environment exported to the build steps
|
|
// (DEB_BUILD_OPTIONS with the real parallel count and 'nocheck', LANG=C,
|
|
// SOURCE_DATE_EPOCH, cross DEB_* variables, ...), not values recomputed
|
|
// from host state; buildinfo_environment filters out non-dpkg variables.
|
|
let exported_env: BTreeMap<String, String> =
|
|
env.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
|
|
|
|
let opts = crate::build::binary::BinaryMetadataOptions {
|
|
profiles,
|
|
vendor,
|
|
exported_env,
|
|
build_arch,
|
|
host_arch,
|
|
};
|
|
let (buildinfo, changes) = crate::build::binary::generate_binary_metadata(
|
|
ctx,
|
|
Path::new(package_dir),
|
|
Path::new(build_root),
|
|
&opts,
|
|
)?;
|
|
Ok((buildinfo, changes))
|
|
}
|
|
|
|
/// 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>,
|
|
view: &dyn BuildView,
|
|
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(());
|
|
}
|
|
|
|
// A `single-debian-patch` tree is already patched by construction:
|
|
// `dpkg-source -b` folds the working-tree delta into
|
|
// 'debian/patches/debian-changes' and registers it in the series
|
|
// WITHOUT applying it — the content stays ambient in the working tree
|
|
// (e.g. the vendored rust '.cargo/config.toml'). Raw `quilt push -a`
|
|
// would then refuse the patch ('file already exists'); only dpkg's own
|
|
// patch(1)-based application tolerates that. Since `dpkg-source -b`
|
|
// regenerates the patch from the working tree at source-build time,
|
|
// applying patches is wrong in principle here: skip the step.
|
|
if uses_single_debian_patch(&ctx, Path::new(package_dir)) {
|
|
log::info!(
|
|
"Tree uses single-debian-patch: the working tree already \
|
|
carries the patch content, skipping quilt patch application"
|
|
);
|
|
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
|
|
view.phase(
|
|
Phase::ApplyingPatches.label(),
|
|
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(())
|
|
}
|
|
|
|
/// Whether the source options of the package tree at `package_dir` (inside
|
|
/// the build context) declare the `single-debian-patch` mode: either
|
|
/// `debian/source/local-options` or `debian/source/options` contains a
|
|
/// line whose trimmed content — after stripping a leading `--` long-option
|
|
/// dash — is exactly `single-debian-patch` (both spellings exist in the
|
|
/// wild). A line merely containing the token as a substring (e.g.
|
|
/// `--single-debian-patch-foo`) does not count.
|
|
fn uses_single_debian_patch(ctx: &Context, package_dir: &Path) -> bool {
|
|
let local_options = ctx
|
|
.read_file(&package_dir.join("debian/source/local-options"))
|
|
.ok();
|
|
let options = ctx
|
|
.read_file(&package_dir.join("debian/source/options"))
|
|
.ok();
|
|
options_declare_single_debian_patch(local_options.as_deref(), options.as_deref())
|
|
}
|
|
|
|
/// Pure decision core of [`uses_single_debian_patch`]: do the (optional)
|
|
/// contents of `debian/source/local-options` / `debian/source/options`
|
|
/// declare `single-debian-patch`?
|
|
fn options_declare_single_debian_patch(local_options: Option<&str>, options: Option<&str>) -> bool {
|
|
let declares = |content: Option<&str>| {
|
|
content.is_some_and(|content| {
|
|
content.lines().any(|line| {
|
|
let line = line.trim();
|
|
let line = line.strip_prefix("--").unwrap_or(line);
|
|
line == "single-debian-patch"
|
|
})
|
|
})
|
|
};
|
|
declares(local_options) || declares(options)
|
|
}
|
|
|
|
/// 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: &[String],
|
|
env: &HashMap<String, String>,
|
|
ctx: Arc<Context>,
|
|
view: &dyn BuildView,
|
|
sink: &Option<Arc<dyn LineSink>>,
|
|
) -> Result<(), Box<dyn Error>> {
|
|
log::info!("Installing injected packages: {:?}", packages);
|
|
|
|
enter_phase(view, 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.as_str());
|
|
}
|
|
}
|
|
|
|
// 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(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::context::ContextConfig;
|
|
|
|
fn cand(arch: &str, ma: &str) -> Candidate {
|
|
Candidate {
|
|
arch: arch.to_string(),
|
|
multiarch: ma.to_string(),
|
|
}
|
|
}
|
|
|
|
fn rel(s: &str) -> PkgRelation {
|
|
crate::debian::deps::parse_simple(s, true).unwrap()
|
|
}
|
|
|
|
/// Unqualified dependencies install the dpkg-satisfying variant,
|
|
/// preferring the runnable build-architecture one: Multi-Arch:
|
|
/// foreign candidates from the build architecture, then the
|
|
/// host-architecture candidate dpkg checks them against, then
|
|
/// `Architecture: all`. A foreign-arch candidate of a non-foreign
|
|
/// package satisfies nothing in cross mode.
|
|
#[test]
|
|
fn install_spec_unqualified_prefers_runnable_then_host() {
|
|
let host = "arm64";
|
|
let build = "amd64";
|
|
|
|
let spec = install_spec_for(
|
|
&rel("t"),
|
|
&[cand("amd64", "foreign"), cand("arm64", "same")],
|
|
build,
|
|
host,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(spec, "t:amd64");
|
|
|
|
let spec = install_spec_for(
|
|
&rel("t"),
|
|
&[cand("arm64", "same"), cand("amd64", "no")],
|
|
build,
|
|
host,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(spec, "t:arm64");
|
|
|
|
assert_eq!(
|
|
install_spec_for(&rel("t"), &[cand("amd64", "no")], build, host),
|
|
None
|
|
);
|
|
|
|
let spec = install_spec_for(&rel("t"), &[cand("all", "foreign")], build, host).unwrap();
|
|
assert_eq!(spec, "t");
|
|
|
|
// Native builds (host == build): a plain Multi-Arch: no candidate
|
|
// is the host candidate.
|
|
let spec = install_spec_for(&rel("t"), &[cand("amd64", "no")], "amd64", "amd64").unwrap();
|
|
assert_eq!(spec, "t:amd64");
|
|
}
|
|
|
|
/// Qualified dependencies install exactly the variant their qualifier
|
|
/// selects per dpkg semantics; virtual names (no candidates) pass
|
|
/// through unqualified for apt to resolve a provider.
|
|
#[test]
|
|
fn install_spec_qualifiers() {
|
|
let host = "arm64";
|
|
let build = "amd64";
|
|
|
|
// :native: the build-architecture candidate; an Architecture: all
|
|
// candidate passes under the plain name; a Multi-Arch: foreign
|
|
// candidate aborts the lookup, like dpkg's checker.
|
|
assert_eq!(
|
|
install_spec_for(&rel("t:native"), &[cand("amd64", "no")], build, host).unwrap(),
|
|
"t:amd64"
|
|
);
|
|
assert_eq!(
|
|
install_spec_for(&rel("t:native"), &[cand("all", "no")], build, host).unwrap(),
|
|
"t"
|
|
);
|
|
assert_eq!(
|
|
install_spec_for(&rel("t:native"), &[cand("amd64", "foreign")], build, host),
|
|
None
|
|
);
|
|
|
|
// :any: a Multi-Arch: allowed candidate, host first.
|
|
assert_eq!(
|
|
install_spec_for(
|
|
&rel("t:any"),
|
|
&[cand("amd64", "allowed"), cand("arm64", "allowed")],
|
|
build,
|
|
host
|
|
)
|
|
.unwrap(),
|
|
"t:arm64"
|
|
);
|
|
assert_eq!(
|
|
install_spec_for(&rel("t:any"), &[cand("riscv64", "allowed")], build, host).unwrap(),
|
|
"t:riscv64"
|
|
);
|
|
assert_eq!(
|
|
install_spec_for(&rel("t:any"), &[cand("amd64", "same")], build, host),
|
|
None
|
|
);
|
|
|
|
// Explicit qualifier: exactly that candidate.
|
|
assert_eq!(
|
|
install_spec_for(&rel("t:arm64"), &[cand("arm64", "same")], build, host).unwrap(),
|
|
"t:arm64"
|
|
);
|
|
assert_eq!(
|
|
install_spec_for(&rel("t:arm64"), &[cand("amd64", "same")], build, host),
|
|
None
|
|
);
|
|
|
|
// Virtual package: debhelper-compat and friends.
|
|
assert_eq!(
|
|
install_spec_for(&rel("t (= 13)"), &[], build, host).unwrap(),
|
|
"t"
|
|
);
|
|
}
|
|
|
|
/// The resolver loop tries the alternatives of a clause in order,
|
|
/// falling past the ones no candidate can satisfy, and stops
|
|
/// contributing a clause once its alternatives are exhausted.
|
|
#[test]
|
|
fn next_install_pass_falls_through_alternatives() {
|
|
// `a:arm64` has only an amd64 candidate: no candidate satisfies
|
|
// the explicit qualifier, so the clause falls through to `t`.
|
|
let clauses = vec![
|
|
vec![rel("a:arm64"), rel("t")],
|
|
vec![rel("u")],
|
|
vec![rel("v")],
|
|
];
|
|
let mut candidates = BTreeMap::new();
|
|
candidates.insert("a".to_string(), vec![cand("amd64", "same")]);
|
|
candidates.insert("t".to_string(), vec![cand("arm64", "same")]);
|
|
candidates.insert("v".to_string(), vec![cand("amd64", "foreign")]);
|
|
let mut cursors = vec![0; clauses.len()];
|
|
|
|
let host = "arm64";
|
|
let build = "amd64";
|
|
let specs = next_install_pass(&clauses, &[0, 1, 2], &mut cursors, &candidates, build, host);
|
|
assert_eq!(
|
|
specs,
|
|
vec![
|
|
"t:arm64".to_string(),
|
|
"u".to_string(),
|
|
"v:amd64".to_string()
|
|
]
|
|
);
|
|
assert_eq!(cursors, vec![2, 1, 1]);
|
|
|
|
// An exhausted clause contributes nothing; the loop aborts with
|
|
// the remaining clauses when nothing installs any more.
|
|
let specs = next_install_pass(&clauses, &[1], &mut cursors, &candidates, build, host);
|
|
assert!(specs.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn detector_matches_local_options_options_and_dashed_spellings() {
|
|
// The pkh scaffold spelling: bare token in local-options.
|
|
assert!(options_declare_single_debian_patch(
|
|
Some("single-debian-patch\n"),
|
|
None
|
|
));
|
|
// The other common spelling in either file: `--`-prefixed.
|
|
assert!(options_declare_single_debian_patch(
|
|
Some("--single-debian-patch\n"),
|
|
None
|
|
));
|
|
assert!(options_declare_single_debian_patch(
|
|
None,
|
|
Some("--single-debian-patch\n")
|
|
));
|
|
// Trailing whitespace and blank lines around the token.
|
|
assert!(options_declare_single_debian_patch(
|
|
Some("\n single-debian-patch \n"),
|
|
None
|
|
));
|
|
// Declared in options while local-options carries other options.
|
|
assert!(options_declare_single_debian_patch(
|
|
Some("--extend-diff-ignore='^vendor/'\n"),
|
|
Some("single-debian-patch\n")
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn detector_rejects_substrings_empty_and_absent_files() {
|
|
// The token must not match as a substring of another option.
|
|
assert!(!options_declare_single_debian_patch(
|
|
Some("--single-debian-patch-ignore=^foo\n"),
|
|
None
|
|
));
|
|
assert!(!options_declare_single_debian_patch(
|
|
Some("--no-single-debian-patch\n"),
|
|
Some("single-debian-patching\n")
|
|
));
|
|
// Absent or empty files: not single-debian-patch.
|
|
assert!(!options_declare_single_debian_patch(None, None));
|
|
assert!(!options_declare_single_debian_patch(Some(""), Some("")));
|
|
// Unrelated content.
|
|
assert!(!options_declare_single_debian_patch(
|
|
Some("--extend-diff-ignore='^vendor/'\n"),
|
|
Some("tar-ignore = .git\n")
|
|
));
|
|
}
|
|
|
|
/// A tree with `single-debian-patch` in its local-options is detected
|
|
/// through the real build-context file access; a tree without the
|
|
/// declaration is not.
|
|
#[test]
|
|
fn detector_on_a_real_tree_through_the_context() {
|
|
let ctx = Context::new(ContextConfig::Local).unwrap();
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let tree = dir.path().join("pkg");
|
|
std::fs::create_dir_all(tree.join("debian/source")).unwrap();
|
|
std::fs::write(
|
|
tree.join("debian/source/local-options"),
|
|
"single-debian-patch\n",
|
|
)
|
|
.unwrap();
|
|
assert!(uses_single_debian_patch(&ctx, &tree));
|
|
|
|
// No source options at all.
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let tree = dir.path().join("pkg");
|
|
std::fs::create_dir_all(tree.join("debian/source")).unwrap();
|
|
assert!(!uses_single_debian_patch(&ctx, &tree));
|
|
}
|
|
|
|
/// Direct call of the patch-application phase on a tree simulating the
|
|
/// `dpkg-source -b` state of a single-debian-patch scaffold:
|
|
/// 'debian-changes' is registered in the series while its content is
|
|
/// already ambient in the working tree (raw `quilt push -a` would
|
|
/// refuse it with 'file already exists'). The phase must skip the
|
|
/// application entirely and succeed.
|
|
#[test]
|
|
fn apply_phase_skips_single_debian_patch_trees() {
|
|
let ctx = Arc::new(Context::new(ContextConfig::Local).unwrap());
|
|
|
|
// Dashed spelling through debian/source/options this time.
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let tree = dir.path().join("pkg");
|
|
std::fs::create_dir_all(tree.join("debian/patches")).unwrap();
|
|
std::fs::create_dir_all(tree.join("debian/source")).unwrap();
|
|
// The touched file already carries the patched content: applying
|
|
// the patch would fail ('README' already exists / differs only in
|
|
// the patch's imagination).
|
|
std::fs::write(tree.join("README"), "patched\n").unwrap();
|
|
std::fs::write(
|
|
tree.join("debian/patches/debian-changes"),
|
|
"--- a/README\n+++ b/README\n@@ -1 +1 @@\n-orig\n+patched\n",
|
|
)
|
|
.unwrap();
|
|
std::fs::write(tree.join("debian/patches/series"), "debian-changes\n").unwrap();
|
|
std::fs::write(
|
|
tree.join("debian/source/options"),
|
|
"--single-debian-patch\n",
|
|
)
|
|
.unwrap();
|
|
|
|
apply_quilt_patches(
|
|
tree.to_str().unwrap(),
|
|
&HashMap::new(),
|
|
ctx,
|
|
&crate::report::Quiet,
|
|
&None,
|
|
)
|
|
.unwrap();
|
|
}
|
|
}
|