debian/deps: native dependency grammar and build-dep checking
Replace dpkg-checkbuilddeps with a native implementation: - full dependency grammar: comma clauses, | alternatives, << <= = >= >> relations, :arch qualifiers (any/native/specific), [arch lists] and <profile restriction> formulas per alternative; - restriction reduction against active build profiles and the host arch at parse time (reduce_restrictions semantics); - evaluation against a parsed dpkg status database with Multi-Arch semantics (foreign/allowed) and versioned Provides rules (unversioned provides never satisfy versioned deps; versioned ones must satisfy the relation); - clause simplification with implication-based deduplication, rendering dpkg-compatible 'unmet build dependencies/conflicts' diagnostics. check_build_depends() consumes debian/control + CheckOpts (-A/-B/-I equivalents). run_source_build performs the check when forced (-D parity); source-only builds skip it entirely like dpkg-buildpackage, and unsatisfied deps propagate as UnmetBuildDependencies -> exit 3. Unit tests port the Dpkg_Deps.t reduction matrices; differential gate runs 24 scenarios (alternatives, versions, arch/profile restrictions, Multi-Arch, Provides, conflicts, -A/-B flags) against real dpkg-checkbuilddeps comparing exit status and diagnostics.
This commit is contained in:
@@ -113,7 +113,7 @@ Exit codes matter: e.g. unsatisfied build-deps ⇒ exit 3.
|
||||
| `dpkg-parsechangelog` | source/version/maintainer/distribution/timestamp | **Low** — documented format; crates exist (`debian-changelog`, `deb822-parser` ecosystem) | Replaced (see §11, [`metadata.rs`](../src/build/metadata.rs)) |
|
||||
| `dpkg-version` compare | epoch/upstream/revision ordering | **Low** — small well-specified algorithm; crate `debversion` | **Replaced** (§11, [`debian/version.rs`](../src/debian/version.rs): `Ord`/`compare`/`later_than`) |
|
||||
| `dpkg-architecture` | arch ↔ triplet tables, multiarch tuple, env dump | **Low-medium** — embed cputable/ostable/tupletable/abitable data (stable for years) | **Replaced** (§11, [`debian/arch.rs`](../src/debian/arch.rs)) |
|
||||
| `dpkg-checkbuilddeps` | deps vs installed status | **Medium** — `Dpkg::Deps` grammar (alternatives, arch qualifiers, `<profiles>` restrictions, versioned Provides subtleties, Multi-Arch facts) + status-file scan | Phase 2; keep `apt-get build-dep`/subprocess until then |
|
||||
| `dpkg-checkbuilddeps` | deps vs installed status | **Medium** — `Dpkg::Deps` grammar (alternatives, arch qualifiers, `<profiles>` restrictions, versioned Provides subtleties, Multi-Arch facts) + status-file scan | **Replaced** (§11, [`debian/deps.rs`](../src/debian/deps.rs); wired into the pipeline behind `-D`, source-only builds skip it like dpkg-buildpackage) |
|
||||
| `dpkg-genbuildinfo` | `.buildinfo` | **Medium** — deb822 emit + status snapshot + checksums | Native (see §11, [`buildinfo.rs`](../src/build/buildinfo.rs)) |
|
||||
| `dpkg-genchanges` | `.changes` | **Medium** — deb822 emit + `debian/files` consumption + `.deb` control extraction (ar+tar, trivial with crates) | Native for source uploads (§11, [`changes.rs`](../src/build/changes.rs)); binary aggregation next |
|
||||
| `dpkg-distaddfile`/`debian/files` protocol | build outputs registry | **Trivial** — one append-only line format | Native ([`files.rs`](../src/build/files.rs)) |
|
||||
|
||||
@@ -28,6 +28,10 @@ pub struct SourceBuildOptions {
|
||||
pub sign_keyid: Option<String>,
|
||||
/// Sign even for an UNRELEASED changelog (`--force-sign`).
|
||||
pub force_sign: bool,
|
||||
/// Force build-dependency checking even though this is a source-only
|
||||
/// build (`-D`). `dpkg-buildpackage` skips `dpkg-checkbuilddeps`
|
||||
/// entirely for source-only builds unless forced.
|
||||
pub force_dep_check: bool,
|
||||
}
|
||||
|
||||
/// Artifacts produced by a successful source build.
|
||||
@@ -198,6 +202,26 @@ pub fn run_source_build(
|
||||
&["-I", "-i", "--before-build", "."],
|
||||
&pipeline_env,
|
||||
)?;
|
||||
|
||||
// Build-dependency check (native dpkg-checkbuilddeps equivalent).
|
||||
// dpkg-buildpackage skips it entirely for source-only builds unless
|
||||
// forced with -D; unsatisfied dependencies abort with exit status 3.
|
||||
if opts.force_dep_check {
|
||||
let check_opts = crate::debian::deps::CheckOpts {
|
||||
host_arch: arch_vars
|
||||
.get("DEB_HOST_ARCH")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| crate::debian::arch::native().unwrap_or_default()),
|
||||
build_profiles: profiles.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
let report = crate::debian::deps::check_build_depends(&ctrl, &check_opts)?;
|
||||
if !report.is_ok() {
|
||||
eprintln!("{}", report.message());
|
||||
return Err(Box::new(crate::debian::deps::UnmetBuildDependencies(report)));
|
||||
}
|
||||
}
|
||||
|
||||
run_command(cwd, "dpkg-source", &["-I", "-i", "-b", "."], &pipeline_env)?;
|
||||
|
||||
if !dsc_path.exists() {
|
||||
@@ -909,6 +933,158 @@ mod differential_tests {
|
||||
diff_arch_env_one(None);
|
||||
}
|
||||
|
||||
/// Differential check of [`crate::debian::deps::check_build_depends`]
|
||||
/// against real `dpkg-checkbuilddeps` on one fixture: exit status and
|
||||
/// reported unmet/conflict lists must match.
|
||||
fn diff_checkbuilddeps_case(control: &str, status: &str, args: &[&str]) {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
std::fs::write(dir.path().join("control"), control).expect("write control");
|
||||
let admindir = dir.path().join("admin");
|
||||
fs::create_dir_all(&admindir).expect("mkdir admindir");
|
||||
fs::write(admindir.join("status"), status).expect("write status");
|
||||
|
||||
// Real tool. Profiles are always pinned via -P so the comparison is
|
||||
// independent of the local vendor defaults; -I skips the vendor
|
||||
// builtin dependencies (build-essential:native), matching the
|
||||
// native checker which knows no builtins. All options must precede
|
||||
// the control-file operand (POSIX-style option parsing).
|
||||
let output = Command::new("dpkg-checkbuilddeps")
|
||||
.current_dir(dir.path())
|
||||
.arg("--admindir")
|
||||
.arg(&admindir)
|
||||
.args(args)
|
||||
.arg("-I")
|
||||
.arg("control")
|
||||
.output()
|
||||
.expect("run dpkg-checkbuilddeps (is dpkg-dev installed?)");
|
||||
let real_exit = output.status.code().unwrap_or(-1);
|
||||
let real_msg = String::from_utf8_lossy(&output.stderr)
|
||||
.lines()
|
||||
.filter_map(|l| l.split_once("error: ").map(|(_, m)| m.trim()))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
// Native checker with equivalent options.
|
||||
let mut profiles: Vec<String> = Vec::new();
|
||||
let mut ignore_arch = false;
|
||||
let mut ignore_indep = false;
|
||||
let mut i = 0;
|
||||
while i < args.len() {
|
||||
match args[i] {
|
||||
"-A" => ignore_arch = true,
|
||||
"-B" => ignore_indep = true,
|
||||
"-P" => {
|
||||
i += 1;
|
||||
profiles = args
|
||||
.get(i)
|
||||
.map(|p| p.split(',').map(str::to_string).collect())
|
||||
.unwrap_or_default();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
let opts = crate::debian::deps::CheckOpts {
|
||||
host_arch: crate::debian::arch::native().unwrap_or_else(|_| "amd64".into()),
|
||||
build_profiles: profiles,
|
||||
ignore_arch,
|
||||
ignore_indep,
|
||||
ignore_builtin: true,
|
||||
admindir: admindir.clone(),
|
||||
};
|
||||
let control_info = crate::debian::ControlInfo::parse_content(control).expect("parse control");
|
||||
let report = crate::debian::deps::check_build_depends(&control_info, &opts)
|
||||
.expect("native parse failure");
|
||||
|
||||
let ours_exit = if report.is_ok() { 0 } else { 1 };
|
||||
assert_eq!(ours_exit, real_exit, "exit status mismatch for {control:?} {args:?}");
|
||||
assert_eq!(
|
||||
report.message(),
|
||||
real_msg,
|
||||
"diagnostics mismatch for {control:?} {args:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Matrix of dependency-checking scenarios validated against the real
|
||||
/// tool: alternatives, version relations, arch/profile restrictions,
|
||||
/// conflicts and `-A`/`-B`/`-P` flag handling.
|
||||
#[test]
|
||||
fn diff_checkbuilddeps_matrix() {
|
||||
const STATUS: &str = "\
|
||||
Package: libc6
|
||||
Status: install ok installed
|
||||
Version: 2.39-0ubuntu8
|
||||
Architecture: amd64
|
||||
|
||||
Package: libfoo-dev
|
||||
Status: install ok installed
|
||||
Version: 1.2-3
|
||||
Architecture: amd64
|
||||
|
||||
Package: ma-foreign-pkg
|
||||
Status: install ok installed
|
||||
Version: 1.0
|
||||
Architecture: i386
|
||||
Multi-Arch: foreign
|
||||
|
||||
Package: provider
|
||||
Status: install ok installed
|
||||
Version: 5.0
|
||||
Architecture: amd64
|
||||
Provides: virtual-thing (= 2.0), plain-virtual
|
||||
";
|
||||
const HEAD: &str = "Source: t\nMaintainer: a <a@b.c>\n";
|
||||
const TAIL: &str = "\nPackage: t\nArchitecture: any\nDescription: x\n y\n";
|
||||
|
||||
let case = |bd: &str, bc: &str, args: &[&str]| {
|
||||
let mut control = String::from(HEAD);
|
||||
if !bd.is_empty() {
|
||||
control.push_str(&format!("Build-Depends: {bd}\n"));
|
||||
}
|
||||
if !bc.is_empty() {
|
||||
control.push_str(&format!("Build-Conflicts: {bc}\n"));
|
||||
}
|
||||
control.push_str(TAIL);
|
||||
diff_checkbuilddeps_case(&control, STATUS, args);
|
||||
};
|
||||
|
||||
// Satisfied / unsatisfied basics.
|
||||
case("libc6 (>= 1)", "", &["-P", "cross"]);
|
||||
case("missing-abc", "", &["-P", "cross"]);
|
||||
case("libc6 (>> 999)", "", &["-P", "cross"]);
|
||||
// Alternatives.
|
||||
case("missing-a | libc6", "", &["-P", "cross"]);
|
||||
case("missing-a | missing-b", "", &["-P", "cross"]);
|
||||
// Architecture restrictions (host is the native arch).
|
||||
case("missing-abc [!amd64]", "", &["-P", "cross"]);
|
||||
case("missing-abc [amd64]", "", &["-P", "cross"]);
|
||||
// Profile restrictions.
|
||||
case("missing-abc <stage1>", "", &["-P", "stage1"]);
|
||||
case("missing-abc <stage1>", "", &["-P", "cross"]);
|
||||
case("missing-abc <!stage1>", "", &["-P", "stage1"]);
|
||||
// Multi-Arch foreign satisfies unqualified deps.
|
||||
case("ma-foreign-pkg", "", &["-P", "cross"]);
|
||||
// Provides: versioned provide satisfying / not satisfying.
|
||||
case("virtual-thing (>= 1.0)", "", &["-P", "cross"]);
|
||||
case("virtual-thing (>= 3.0)", "", &["-P", "cross"]);
|
||||
case("plain-virtual", "", &["-P", "cross"]);
|
||||
case("plain-virtual (>= 1.0)", "", &["-P", "cross"]);
|
||||
// Conflicts.
|
||||
case("", "libc6 (<< 1)", &["-P", "cross"]);
|
||||
case("", "libc6", &["-P", "cross"]);
|
||||
case("", "missing-abc", &["-P", "cross"]);
|
||||
// -A/-B field handling.
|
||||
let control_ab = format!(
|
||||
"{HEAD}Build-Depends: libc6\nBuild-Depends-Arch: missing-arch-dep\nBuild-Depends-Indep: missing-indep-dep\n{TAIL}"
|
||||
);
|
||||
diff_checkbuilddeps_case(&control_ab, STATUS, &["-P", "cross"]);
|
||||
diff_checkbuilddeps_case(&control_ab, STATUS, &["-A", "-P", "cross"]);
|
||||
diff_checkbuilddeps_case(&control_ab, STATUS, &["-B", "-P", "cross"]);
|
||||
|
||||
// Combined unmet + conflict reporting in one run.
|
||||
case("missing-one, libc6 (>> 999)", "libfoo-dev", &["-P", "cross"]);
|
||||
}
|
||||
|
||||
/// Differential check of [`crate::debian::version`] against real
|
||||
/// `dpkg --compare-versions` over every ported dpkg test vector and
|
||||
/// every relation operator.
|
||||
|
||||
+1313
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,7 @@ pub mod arch;
|
||||
pub mod changelog;
|
||||
pub mod checksums;
|
||||
pub mod control;
|
||||
pub mod deps;
|
||||
pub mod files;
|
||||
pub mod version;
|
||||
|
||||
|
||||
@@ -252,6 +252,13 @@ fn main() {
|
||||
let cwd = current_dir_or_exit();
|
||||
if let Err(e) = pkh::build::build_source_package(Some(&cwd)) {
|
||||
error!("{}", e);
|
||||
// Unmet build dependencies/conflicts exit with status 3,
|
||||
// like dpkg-buildpackage does.
|
||||
if e.downcast_ref::<pkh::debian::deps::UnmetBuildDependencies>()
|
||||
.is_some()
|
||||
{
|
||||
std::process::exit(3);
|
||||
}
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user