deb: install build-deps per dpkg cross semantics
Replace the apt-get build-dep passes with a native resolver: the Build-* clauses are evaluated against the context's package state by crate::debian::deps (the dpkg-checkbuilddeps equivalent), and the unsatisfied ones install through explicitly architecture-qualified names. apt's --host-architecture build-dep resolution is coarser than the checker it feeds: unqualified Multi-Arch: same libraries only ever get the host-architecture variant, and there is no way to also satisfy the build-architecture needs of a cross build without a native re-resolution pass — which installs co-install partners that break unpacking for -dev packages whose variants conflict on arch-differing files (curl-config; the libcurl4-gnutls-dev regression). The resolver now applies the same dpkg rules as the checker, preferring the runnable build-architecture variant of Multi-Arch: foreign tools, and passes virtual names through to apt. Build-Conflicts are now checked before anything installs, which the build-dep passes never did, and a failed dose3 diagnosis no longer masks the resolver error (a latent flaw of the passes: binary-only builds have no .dsc for dose-builddebcheck to read). An end-to-end test pins the expected failure: a cross build whose build-dependencies cannot satisfy dpkg's cross semantics aborts, naming the dependency.
This commit is contained in:
+568
-54
@@ -5,13 +5,15 @@ use crate::deb::{Phase, enter_phase, find_dsc_file};
|
||||
use crate::logfmt::QuiltClassifier;
|
||||
use crate::report::BuildView;
|
||||
use log::warn;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
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>(
|
||||
@@ -255,59 +257,22 @@ pub async fn build(
|
||||
install_injected_packages(inject_packages, &env, ctx.clone(), view, &sink)?;
|
||||
}
|
||||
|
||||
// Install arch-specific build dependencies
|
||||
log::debug!("Installing arch-specific build dependencies...");
|
||||
enter_phase(view, 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() {
|
||||
view.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, only if the source declares
|
||||
// any: without --arch-only this pass resolves the whole Build-Depends field
|
||||
// too, which is redundant after the first pass and breaks cross builds.
|
||||
let has_indep_deps = match ctx.read_file(&package_dir.join("debian/control")) {
|
||||
Ok(control) => control
|
||||
.lines()
|
||||
.any(|l| l.to_ascii_lowercase().starts_with("build-depends-indep:")),
|
||||
Err(e) => {
|
||||
log::debug!("cannot read debian/control for indep build-deps: {}", e);
|
||||
true
|
||||
}
|
||||
};
|
||||
if has_indep_deps {
|
||||
log::debug!("Installing arch-independant 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("./");
|
||||
let status = cap(&mut cmd, &sink).status()?;
|
||||
|
||||
// If build-dep fails, we try to explain the failure using dose-debcheck
|
||||
if !status.success() {
|
||||
view.suspend();
|
||||
dose3_explain_dependencies(package, version, arch, build_root, cross, ctx.clone())?;
|
||||
return Err("Could not install build-dependencies for the build".into());
|
||||
}
|
||||
}
|
||||
// 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,
|
||||
package_dir_str,
|
||||
build_root,
|
||||
cross,
|
||||
&env,
|
||||
ctx.clone(),
|
||||
view,
|
||||
&sink,
|
||||
)?;
|
||||
|
||||
// Run the build step
|
||||
log::debug!("Building (debian/rules build) package...");
|
||||
@@ -371,6 +336,403 @@ pub async fn build(
|
||||
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,
|
||||
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 clauses: Vec<Vec<PkgRelation>> = Deps::parse(&deps_value, &parse_opts)?
|
||||
.clauses()
|
||||
.map(<[PkgRelation]>::to_vec)
|
||||
.collect();
|
||||
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
|
||||
@@ -724,6 +1086,158 @@ 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.
|
||||
|
||||
@@ -739,4 +739,74 @@ mod tests {
|
||||
"arch-independant .deb not produced, got: {deb_files:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// A cross build whose build-dependencies cannot be satisfied under
|
||||
/// dpkg's cross semantics must abort at the install step, naming the
|
||||
/// unsatisfied dependency: the expected-failure counterpart of the
|
||||
/// cross builds above.
|
||||
#[tokio::test]
|
||||
#[test_log::test]
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
async fn test_deb_cross_unsatisfiable_build_dep_fails_end_to_end() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let pkg_dir = temp_dir.path().join("pkh-crosstest");
|
||||
std::fs::create_dir_all(pkg_dir.join("debian/source")).unwrap();
|
||||
|
||||
std::fs::write(
|
||||
pkg_dir.join("debian/changelog"),
|
||||
"pkh-crosstest (1.0) noble; urgency=medium\n\n \
|
||||
* Synthetic package declaring an unsatisfiable build-dependency.\n\n \
|
||||
-- pkh tests <pkh@example.com> Tue, 15 Sep 2026 08:00:00 +0000\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
std::fs::write(
|
||||
pkg_dir.join("debian/control"),
|
||||
"Source: pkh-crosstest\n\
|
||||
Section: devel\n\
|
||||
Priority: optional\n\
|
||||
Maintainer: pkh tests <pkh@example.com>\n\
|
||||
Standards-Version: 4.7.4\n\
|
||||
Build-Depends: pkh-no-such-package-xyz\n\
|
||||
Architecture: any\n\
|
||||
\n\
|
||||
Package: pkh-crosstest\n\
|
||||
Architecture: any\n\
|
||||
Depends: ${misc:Depends}, ${shlibs:Depends}\n\
|
||||
Description: Cross-build negative regression package\n \
|
||||
Declares a build-dependency absent from the archive.\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
std::fs::write(
|
||||
pkg_dir.join("debian/rules"),
|
||||
"#!/usr/bin/make -f\n%:\n\tdh $@\n",
|
||||
)
|
||||
.unwrap();
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let rules = pkg_dir.join("debian/rules");
|
||||
let mut perms = std::fs::metadata(&rules).unwrap().permissions();
|
||||
perms.set_mode(0o755);
|
||||
std::fs::set_permissions(&rules, perms).unwrap();
|
||||
|
||||
std::fs::write(pkg_dir.join("debian/source/format"), "3.0 (native)\n").unwrap();
|
||||
|
||||
let ctx = Arc::new(Context::new(crate::context::ContextConfig::Local).unwrap());
|
||||
|
||||
let err = crate::deb::build_binary_package(DebBuildOptions {
|
||||
arch: Some("arm64".to_string()),
|
||||
series: Some("noble".to_string()),
|
||||
cwd: Some(pkg_dir),
|
||||
cross: true,
|
||||
ctx: Some(ctx),
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.expect_err("an unsatisfiable build-dependency must fail the cross build");
|
||||
|
||||
assert!(
|
||||
err.to_string().contains("pkh-no-such-package-xyz"),
|
||||
"error should name the unsatisfied dependency: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user