debian: test the cross build-dep lookup against dpkg

The Facts lookup already implemented Dpkg::Deps::KnownFacts
_find_package, but nothing pinned its cross-compilation behaviour:
with host != build, an unqualified dependency resolves against the
HOST architecture instance (or any instance of a Multi-Arch: foreign
package, or an Architecture: all one) and never against the
build-architecture instance of a Multi-Arch: no/same package.

Cover the matrix with unit tests and, like the source-build checker,
with a differential sweep against real dpkg-checkbuilddeps on a
synthetic admindir with -a <host>: the checker and the real tool
agree on every fixture, including the quirks (:native aborting on
Multi-Arch: foreign instances, first-match version binding).
This commit is contained in:
2026-09-20 18:04:05 +02:00
parent 5c026a7050
commit 02cb5306f3
+227
View File
@@ -1349,6 +1349,233 @@ Provides: virt2 (>= 1.0), plain
assert_eq!(facts.evaluate_relation(&o("plain")), Some(false));
}
/// Cross-compilation semantics of the package lookup (the `Facts`
/// host/build split), mirroring `Dpkg::Deps::KnownFacts::_find_package`:
/// an unqualified dependency is satisfied by the HOST architecture
/// instance, by any instance of a `Multi-Arch: foreign` package, or by
/// an `Architecture: all` instance — never by a foreign-arch instance
/// of a `Multi-Arch: no`/`same` package. The build-architecture
/// instances only come into play through `:native`.
///
/// Verdicts marked «live» were probed against `dpkg-checkbuilddeps -a
/// <host>` on a real amd64 system carrying the build-arch instances.
#[test]
fn cross_lookup_matrix() {
const O: fn(&str) -> PkgRelation = |s| parse_simple(s, true).unwrap();
// host = arm64 (target), build = amd64 (machine): the instances
// below simulate what a cross-building amd64 machine has installed.
let facts = |ma_build: &str, ma_host: &str| {
let mut f = Facts::new("arm64", "amd64");
if !ma_build.is_empty() {
f.add_installed("t", "1.0", "amd64", ma_build);
}
if !ma_host.is_empty() {
f.add_installed("t", "1.0", "arm64", ma_host);
}
f
};
// Unqualified: only the host-arch instance satisfies...
assert_eq!(
facts("no", "").evaluate_relation(&O("t")),
Some(false),
"M-A:no build-arch instance must not satisfy an unqualified dep"
);
assert_eq!(
facts("same", "").evaluate_relation(&O("t")),
Some(false),
"M-A:same build-arch instance must not satisfy an unqualified dep"
);
assert_eq!(facts("", "no").evaluate_relation(&O("t")), Some(true));
assert_eq!(facts("", "same").evaluate_relation(&O("t")), Some(true));
// ...unless the package is Multi-Arch: foreign («live»: bison,
// flex: the natively-installed variant satisfies the cross check).
assert_eq!(facts("foreign", "").evaluate_relation(&O("t")), Some(true));
assert_eq!(facts("", "foreign").evaluate_relation(&O("t")), Some(true));
// `Architecture: all` instances satisfy unqualified dependencies
// whatever the Multi-Arch attribute.
let mut all = Facts::new("arm64", "amd64");
all.add_installed("t", "1.0", "all", "foreign");
assert_eq!(all.evaluate_relation(&O("t")), Some(true));
let mut all2 = Facts::new("arm64", "amd64");
all2.add_installed("t", "1.0", "all", "no");
assert_eq!(all2.evaluate_relation(&O("t")), Some(true));
// Versioned relations check the first matching instance only:
// insertion order decides which instance a dependency binds to,
// and an unsatisfying version does not fall through to later
// instances.
let mut mixed = Facts::new("arm64", "amd64");
mixed.add_installed("t", "0.5", "arm64", "no");
mixed.add_installed("t", "3.0", "amd64", "no");
assert_eq!(mixed.evaluate_relation(&O("t (>= 1)")), Some(false));
assert_eq!(mixed.evaluate_relation(&O("t (<< 1)")), Some(true));
// `:native`: the build-architecture instance satisfies («live»:
// gcc:native on an amd64 machine, whatever the target); an
// Architecture: all instance does too — but a Multi-Arch: foreign
// instance aborts the whole lookup, even on the build architecture
// («live»: flex:native with natively-installed M-A:foreign flex is
// unmet).
assert_eq!(
facts("no", "").evaluate_relation(&O("t:native")),
Some(true)
);
assert_eq!(
facts("same", "").evaluate_relation(&O("t:native")),
Some(true)
);
assert_eq!(
facts("foreign", "").evaluate_relation(&O("t:native")),
Some(false)
);
// An Architecture: all instance satisfies :native — unless it is
// Multi-Arch: foreign, which aborts the lookup like any foreign
// instance.
assert_eq!(all2.evaluate_relation(&O("t:native")), Some(true));
assert_eq!(all.evaluate_relation(&O("t:native")), Some(false));
assert_eq!(
facts("", "no").evaluate_relation(&O("t:native")),
Some(false)
);
// `:any`: only a Multi-Arch: allowed instance satisfies, on any
// architecture («live»: libssl-dev:any with M-A:same libssl-dev is
// unmet).
assert_eq!(
facts("allowed", "").evaluate_relation(&O("t:any")),
Some(true)
);
assert_eq!(
facts("", "allowed").evaluate_relation(&O("t:any")),
Some(true)
);
assert_eq!(
facts("same", "").evaluate_relation(&O("t:any")),
Some(false)
);
// Explicit architecture qualifier: only that exact instance.
assert_eq!(facts("no", "").evaluate_relation(&O("t:amd64")), Some(true));
assert_eq!(
facts("", "no").evaluate_relation(&O("t:amd64")),
Some(false)
);
}
/// The same cross matrix, validated against the real
/// `dpkg-checkbuilddeps`: for each fixture the exit status and the
/// reported unmet list must match, with host != build (the machine is
/// the native architecture; the host architecture is a foreign one).
#[test]
fn diff_checkbuilddeps_cross_matrix() {
let build_arch = arch::native().unwrap_or_else(|_| "amd64".into());
// Any foreign arch the dpkg tables know; the instances only exist
// in the synthetic status file.
let host_arch = if build_arch == "arm64" {
"riscv64".to_string()
} else {
"arm64".to_string()
};
let mk_status = |entries: &[(&str, &str)]| {
let mut s = String::new();
for (pkg_arch, ma) in entries {
let ma = if ma.is_empty() { "no" } else { ma };
s.push_str(&format!(
"Package: t\nStatus: install ok installed\nVersion: 1.0\nArchitecture: {pkg_arch}\nMulti-Arch: {ma}\n\n"
));
}
s
};
for (name, entries, dep) in [
// Unqualified: build-arch instances never satisfy, host-arch
// and Multi-Arch: foreign do.
("ma-no-build", &[("amd64", "no")] as &[(&str, &str)], "t"),
("ma-same-build", &[("amd64", "same")], "t"),
("ma-no-host", &[("arm64", "no")], "t"),
("ma-same-host", &[("arm64", "same")], "t"),
("ma-foreign-build", &[("amd64", "foreign")], "t"),
("all-build", &[("all", "foreign")], "t"),
// :native and :any qualifiers.
("native-build", &[("amd64", "no")], "t:native"),
("native-foreign-build", &[("amd64", "foreign")], "t:native"),
("any-allowed-build", &[("amd64", "allowed")], "t:any"),
("any-same-build", &[("amd64", "same")], "t:any"),
("explicit-build", &[("amd64", "no")], "t:amd64"),
("explicit-host", &[("arm64", "no")], "t:amd64"),
] {
// Substitute the foreign architecture for fixtures that name
// the host arch explicitly.
let dep = dep.replace("arm64", &host_arch);
let entries: Vec<(String, &str)> = entries
.iter()
.map(|(a, m)| (a.replace("amd64", &build_arch), *m))
.collect();
let entries: Vec<(&str, &str)> =
entries.iter().map(|(a, m)| (a.as_str(), *m)).collect();
let status = mk_status(&entries);
let verdict = |bd: &str, host: &str| {
diff_cross_case(bd, &status, &build_arch, host, |ours, real| {
assert_eq!(ours, real, "verdict mismatch for {name}")
})
};
verdict(&dep, &host_arch);
}
}
/// One differential cross case: run the real `dpkg-checkbuilddeps`
/// with `-a <host>` against a synthetic admindir, and the native
/// checker with the equivalent options on the same control, then hand
/// both verdicts to `compare`.
fn diff_cross_case(
bd: &str,
status: &str,
build_arch: &str,
host_arch: &str,
compare: impl Fn(bool, bool),
) {
let control_text = format!(
"Source: t\nMaintainer: a <a@b.c>\nBuild-Depends: {bd}\n\nPackage: t\nArchitecture: any\nDescription: x\n y\n"
);
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("control"), &control_text).unwrap();
let admindir = dir.path().join("admin");
std::fs::create_dir_all(&admindir).unwrap();
std::fs::write(admindir.join("status"), status).unwrap();
let output = std::process::Command::new("dpkg-checkbuilddeps")
.current_dir(dir.path())
.env("LC_ALL", "C")
.arg("--admindir")
.arg(&admindir)
.arg("-a")
.arg(host_arch)
.arg("-I")
.arg("control")
.output()
.expect("run dpkg-checkbuilddeps (is dpkg-dev installed?)");
let real_ok = output.status.success();
let opts = CheckOpts {
host_arch: host_arch.to_string(),
build_arch: build_arch.to_string(),
build_profiles: Vec::new(),
ignore_arch: false,
ignore_indep: false,
ignore_builtin: true,
admindir: admindir.to_path_buf(),
};
let control = ControlInfo::parse_content(&control_text).unwrap();
let ours_ok = check_build_depends(&control, &opts)
.expect("native check failure")
.is_ok();
compare(ours_ok, real_ok);
}
/// The same undecidable verdicts through the direct facts API: an
/// unreadable provided version and an invalid (non-`=`) provide each
/// leave a versioned relation undecided, while a readable provider