From 02cb5306f398a21efd3c321310e38a2579811d98 Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Sun, 20 Sep 2026 18:04:05 +0200 Subject: [PATCH] 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 : the checker and the real tool agree on every fixture, including the quirks (:native aborting on Multi-Arch: foreign instances, first-match version binding). --- src/debian/deps.rs | 227 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 227 insertions(+) diff --git a/src/debian/deps.rs b/src/debian/deps.rs index 71602a9..535f7bb 100644 --- a/src/debian/deps.rs +++ b/src/debian/deps.rs @@ -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 + /// ` 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 ` 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 \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