build: re-implement source builds natively, drop dpkg-buildpackage shell-out

Replace the 'dpkg-buildpackage -S' wrapper with a native pipeline in
src/build/:

- deb822 control parser/writer with dpkg-compatible multiline rendering
  (control.rs)
- md5/sha1/sha256 checksum registry, insertion-ordered like dpkg's
  artifact accumulation (checksums.rs)
- Debian version splitting/validation and full changelog entry parsing,
  including binNMU binary-only entries (metadata.rs)
- build-type bitflags and rules-target/artifact-suffix mapping
  (buildtype.rs)
- environment setup: SOURCE_DATE_EPOCH, DEB_BUILD_OPTIONS,
  dpkg-architecture env dump, vendor default profiles and the sanitized
  Environment field recorded in .buildinfo (env.rs)
- debian/files registry with atomic saves (files.rs)
- native .buildinfo writer, including the Installed-Build-Depends
  closure computed over the dpkg status database (buildinfo.rs)
- native .changes writer emitting dpkg's canonical field order with
  legacy Files + Checksums-Sha1/Sha256 (changes.rs)
- gpgme clearsigning with the transitive checksum cascade
  (dsc -> buildinfo -> changes), key discovery from the changelog
  maintainer and UNRELEASED no-sign handling (sign.rs)

dpkg-source (-b/--before-build/--after-build) intentionally remains a
subprocess; debian/rules execution is unchanged.

Validated differentially against real dpkg-buildpackage -S -I -i -nc -d
on native and 3.0 (quilt) fixture packages: .dsc byte-identical, .changes
payload matches modulo machine-dependent Installed-Build-Depends and
Environment content, all signatures verify with gpg, artifact ordering
and UNRELEASED no-sign behavior match dpkg.
This commit is contained in:
2026-08-23 01:29:35 +02:00
parent e5adf600c3
commit 9d2519ed7b
12 changed files with 2813 additions and 82 deletions
+160
View File
@@ -0,0 +1,160 @@
//! Debian build types (`dpkg-buildpackage -b/-B/-A/-S/-g/-G/--build=...`)
//! and their mapping to `debian/rules` targets.
/// Build type bit flags, mirroring `Dpkg::BuildTypes`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BuildType(u8);
/// Source build component (`-S`, `--build=source`).
pub const SOURCE: BuildType = BuildType(0x1);
/// Arch-dependent binary build component (`-B`, `--build=any`).
pub const ARCH_DEP: BuildType = BuildType(0x2);
/// Arch-independent binary build component (`-A`, `--build=all`).
pub const ARCH_INDEP: BuildType = BuildType(0x4);
/// Any binary component.
pub const BINARY: BuildType = BuildType(ARCH_DEP.0 | ARCH_INDEP.0);
/// Normal full build: source + binaries (`-F`, default).
pub const FULL: BuildType = BuildType(SOURCE.0 | BINARY.0);
/// Source + arch-dependent (`-G`).
pub const SOURCE_ARCH_DEP: BuildType = BuildType(SOURCE.0 | ARCH_DEP.0);
/// Source + arch-indep (`-g`).
pub const SOURCE_ARCH_INDEP: BuildType = BuildType(SOURCE.0 | ARCH_INDEP.0);
impl BuildType {
/// Construct from raw bits.
pub const fn from_bits(bits: u8) -> Self {
BuildType(bits)
}
/// Raw bits.
pub const fn bits(self) -> u8 {
self.0
}
/// True if any of `other`'s components are set.
pub fn has_any(self, other: BuildType) -> bool {
self.0 & other.0 != 0
}
/// True if all of `other`'s components are set.
pub fn has_all(self, other: BuildType) -> bool {
self.0 & other.0 == other.0
}
/// True if none of `other`'s components are set.
pub fn has_none(self, other: BuildType) -> bool {
self.0 & other.0 == 0
}
/// Parse a comma-separated `--build=<type>[,...]` option value.
///
/// Valid components: `full`, `source`, `binary`, `any`, `all`.
pub fn from_options(value: &str) -> Result<BuildType, String> {
let mut result = BuildType(0);
for part in value.split(',') {
match part.trim() {
"full" => result = FULL,
"source" => result = BuildType(result.0 | SOURCE.0),
"binary" => result = BuildType(result.0 | BINARY.0),
"any" => result = BuildType(result.0 | ARCH_DEP.0),
"all" => result = BuildType(result.0 | ARCH_INDEP.0),
other => return Err(format!("unknown build type component '{}'", other)),
}
}
if result.0 == 0 {
return Err("empty build type".to_string());
}
Ok(result)
}
/// Canonical comma-separated representation (as passed to
/// `dpkg-genchanges --build=` / `dpkg-genbuildinfo --build=`).
pub fn to_options(self) -> String {
let mut parts = Vec::new();
if self.has_any(SOURCE) {
parts.push("source");
}
if self.has_all(BINARY) {
parts.push("binary");
} else {
if self.has_any(ARCH_DEP) {
parts.push("any");
}
if self.has_any(ARCH_INDEP) {
parts.push("all");
}
}
parts.join(",")
}
/// The `debian/rules` build target for this type:
/// `build`, `build-arch` or `build-indep`.
pub fn build_target(self) -> &'static str {
if self.has_all(BINARY) || self.has_none(BINARY) {
"build"
} else if self.has_any(ARCH_DEP) {
"build-arch"
} else {
"build-indep"
}
}
/// The `debian/rules` binary target for this type:
/// `binary`, `binary-arch` or `binary-indep`.
pub fn binary_target(self) -> &'static str {
if self.has_all(BINARY) || self.has_none(BINARY) {
"binary"
} else if self.has_any(ARCH_DEP) {
"binary-arch"
} else {
"binary-indep"
}
}
/// The architecture suffix used in artifact file names:
/// host arch, `all` or `source`.
pub fn arch_suffix<'a>(self, host_arch: &'a str) -> &'a str {
if self.has_any(ARCH_DEP) {
host_arch
} else if self.has_any(ARCH_INDEP) {
"all"
} else {
"source"
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_options() {
assert_eq!(BuildType::from_options("full").unwrap(), FULL);
assert_eq!(BuildType::from_options("source").unwrap(), SOURCE);
assert_eq!(BuildType::from_options("source,any").unwrap(), SOURCE_ARCH_DEP);
assert_eq!(BuildType::from_options("any,all").unwrap(), BINARY);
assert!(BuildType::from_options("bogus").is_err());
}
#[test]
fn roundtrip_options() {
for t in [FULL, SOURCE, BINARY, SOURCE_ARCH_DEP, SOURCE_ARCH_INDEP] {
assert_eq!(BuildType::from_options(&t.to_options()).unwrap(), t);
}
}
#[test]
fn targets() {
assert_eq!(FULL.build_target(), "build");
assert_eq!(FULL.binary_target(), "binary");
assert_eq!(ARCH_DEP.build_target(), "build-arch");
assert_eq!(ARCH_DEP.binary_target(), "binary-arch");
assert_eq!(ARCH_INDEP.build_target(), "build-indep");
assert_eq!(ARCH_INDEP.binary_target(), "binary-indep");
assert_eq!(SOURCE.arch_suffix("amd64"), "source");
assert_eq!(ARCH_DEP.arch_suffix("amd64"), "amd64");
assert_eq!(ARCH_INDEP.arch_suffix("amd64"), "all");
}
}