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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user