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:
2026-09-20 18:04:13 +02:00
parent 02cb5306f3
commit af9845c480
2 changed files with 638 additions and 54 deletions
+568 -54
View File
@@ -5,13 +5,15 @@ use crate::deb::{Phase, enter_phase, find_dsc_file};
use crate::logfmt::QuiltClassifier; use crate::logfmt::QuiltClassifier;
use crate::report::BuildView; use crate::report::BuildView;
use log::warn; use log::warn;
use std::collections::{BTreeMap, HashMap}; use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::error::Error; use std::error::Error;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use crate::apt; use crate::apt;
use crate::deb::cross; 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 /// Attach the capture sink to a command when the live UI is active
fn cap<'a>( fn cap<'a>(
@@ -255,59 +257,22 @@ pub async fn build(
install_injected_packages(inject_packages, &env, ctx.clone(), view, &sink)?; install_injected_packages(inject_packages, &env, ctx.clone(), view, &sink)?;
} }
// Install arch-specific build dependencies // Resolve and install the Build-* dependencies with dpkg's cross
log::debug!("Installing arch-specific build dependencies..."); // semantics; this replaces the historical `apt-get build-dep` passes,
enter_phase(view, Phase::InstallingBuildDeps); // whose `--host-architecture` resolution cannot express the
let mut cmd = ctx.command("apt-get"); // Multi-Arch-aware variant choice dpkg's checker requires.
cmd.current_dir(package_dir_str) install_build_dependencies(
.envs(env.clone()) package,
.arg("-y") version,
.arg("build-dep"); arch,
if cross { package_dir_str,
cmd.arg(format!("--host-architecture={arch}")); build_root,
} cross,
cmd.arg("--arch-only"); &env,
let status = cap(&mut cmd, &sink).arg("./").status()?; ctx.clone(),
view,
// If build-dep fails, we try to explain the failure using dose-debcheck &sink,
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());
}
}
// Run the build step // Run the build step
log::debug!("Building (debian/rules build) package..."); log::debug!("Building (debian/rules build) package...");
@@ -371,6 +336,403 @@ pub async fn build(
Ok(artifacts) 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 /// Collect the binary artifacts (.deb/.udeb) registered by the build in
/// `debian/files`, returning their paths inside the build context /// `debian/files`, returning their paths inside the build context
/// (`<build_root>/<filename>`). `debian/files` is the canonical record of /// (`<build_root>/<filename>`). `debian/files` is the canonical record of
@@ -724,6 +1086,158 @@ mod tests {
use super::*; use super::*;
use crate::context::ContextConfig; 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] #[test]
fn detector_matches_local_options_options_and_dashed_spellings() { fn detector_matches_local_options_options_and_dashed_spellings() {
// The pkh scaffold spelling: bare token in local-options. // The pkh scaffold spelling: bare token in local-options.
+70
View File
@@ -739,4 +739,74 @@ mod tests {
"arch-independant .deb not produced, got: {deb_files:?}" "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}"
);
}
} }