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:
@@ -0,0 +1,317 @@
|
||||
//! Build environment setup: `SOURCE_DATE_EPOCH`, `DEB_BUILD_OPTIONS`,
|
||||
//! architecture variables (via `dpkg-architecture`) and the sanitized
|
||||
//! environment recorded in `.buildinfo` files.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
/// Number of parallel jobs to advertise in `DEB_BUILD_OPTIONS`.
|
||||
pub fn num_parallel() -> usize {
|
||||
std::thread::available_parallelism()
|
||||
.map(|n| n.get())
|
||||
.unwrap_or(1)
|
||||
}
|
||||
|
||||
/// Compute the environment variables exported by `dpkg-buildpackage` before
|
||||
/// running any build step.
|
||||
///
|
||||
/// Mirrors dpkg behavior:
|
||||
/// - `SOURCE_DATE_EPOCH` from the changelog entry timestamp
|
||||
/// (<https://reproducible-builds.org/specs/source-date-epoch/>),
|
||||
/// - `DEB_BUILD_OPTIONS=parallel=N` (auto-detected job count),
|
||||
/// - `DEB_BUILD_PROFILES` when non-default profiles are requested.
|
||||
pub fn build_env(
|
||||
source_date_epoch: i64,
|
||||
parallel: usize,
|
||||
build_profiles: &[String],
|
||||
) -> BTreeMap<String, String> {
|
||||
let mut env = BTreeMap::new();
|
||||
env.insert(
|
||||
"SOURCE_DATE_EPOCH".to_string(),
|
||||
source_date_epoch.to_string(),
|
||||
);
|
||||
env.insert(
|
||||
"DEB_BUILD_OPTIONS".to_string(),
|
||||
format!("parallel={}", parallel),
|
||||
);
|
||||
if !build_profiles.is_empty() {
|
||||
env.insert("DEB_BUILD_PROFILES".to_string(), build_profiles.join(","));
|
||||
}
|
||||
env
|
||||
}
|
||||
|
||||
/// Import the full architecture variable set by running
|
||||
/// `dpkg-architecture -f [-a <host-arch>]` and parsing its `KEY=VALUE` dump.
|
||||
///
|
||||
/// This exports all `DEB_BUILD_*`, `DEB_HOST_*` and `DEB_TARGET_*` variables
|
||||
/// (`*_ARCH`, `*_OS`, `*_CPU`, `*_MULTIARCH`, `*_GNU_TYPE`, ...), exactly as
|
||||
/// `dpkg-buildpackage` does.
|
||||
pub fn arch_env(host_arch: Option<&str>) -> Result<BTreeMap<String, String>, String> {
|
||||
let mut cmd = Command::new("dpkg-architecture");
|
||||
cmd.arg("-f");
|
||||
if let Some(arch) = host_arch {
|
||||
cmd.args(["--host-arch", arch]);
|
||||
}
|
||||
|
||||
let output = cmd
|
||||
.output()
|
||||
.map_err(|e| {
|
||||
format!(
|
||||
"failed to run 'dpkg-architecture': {}. Is 'dpkg-dev' installed?",
|
||||
e
|
||||
)
|
||||
})?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(format!(
|
||||
"dpkg-architecture failed with status {}: {}",
|
||||
output.status,
|
||||
String::from_utf8_lossy(&output.stderr).trim()
|
||||
));
|
||||
}
|
||||
|
||||
let mut env = BTreeMap::new();
|
||||
for line in String::from_utf8_lossy(&output.stdout).lines() {
|
||||
if let Some((key, value)) = line.split_once('=') {
|
||||
env.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
}
|
||||
Ok(env)
|
||||
}
|
||||
|
||||
/// Read the current vendor name from `/etc/dpkg/origins/default`
|
||||
/// (its `Vendor:` or `Origin:` field), defaulting to `"debian"`.
|
||||
pub fn current_vendor() -> String {
|
||||
read_vendor_from(Path::new("/etc/dpkg/origins/default"))
|
||||
.unwrap_or_else(|| "debian".to_string())
|
||||
}
|
||||
|
||||
fn read_vendor_from(path: &Path) -> Option<String> {
|
||||
let content = std::fs::read_to_string(path).ok()?;
|
||||
for line in content.lines() {
|
||||
if let Some(value) = line.strip_prefix("Vendor:") {
|
||||
let v = value.trim();
|
||||
if !v.is_empty() {
|
||||
return Some(v.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fall back to Origin if no Vendor field is present.
|
||||
for line in content.lines() {
|
||||
if let Some(value) = line.strip_prefix("Origin:") {
|
||||
let v = value.trim();
|
||||
if !v.is_empty() {
|
||||
return Some(v.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Default build profiles applied by vendor hooks.
|
||||
///
|
||||
/// The Ubuntu vendor module activates `derivative.ubuntu noudeb` by default;
|
||||
/// Debian applies none. This mirrors what `Dpkg::BuildProfiles` resolves when
|
||||
/// `DEB_BUILD_PROFILES` is unset.
|
||||
pub fn default_build_profiles(vendor: &str) -> Vec<String> {
|
||||
if vendor.eq_ignore_ascii_case("ubuntu") {
|
||||
vec!["derivative.ubuntu".to_string(), "noudeb".to_string()]
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve the active build profiles: explicit `-P` profiles take precedence,
|
||||
/// then `DEB_BUILD_PROFILES` from the environment, then vendor defaults.
|
||||
pub fn resolve_build_profiles(explicit: &[String], vendor: &str) -> Vec<String> {
|
||||
if !explicit.is_empty() {
|
||||
return explicit.to_vec();
|
||||
}
|
||||
if let Ok(value) = std::env::var("DEB_BUILD_PROFILES") {
|
||||
let profiles: Vec<String> = value
|
||||
.split(',')
|
||||
.map(|p| p.trim().to_string())
|
||||
.filter(|p| !p.is_empty())
|
||||
.collect();
|
||||
if !profiles.is_empty() {
|
||||
return profiles;
|
||||
}
|
||||
}
|
||||
default_build_profiles(vendor)
|
||||
}
|
||||
|
||||
/// Environment variables that may affect a build without leaking private
|
||||
/// information; only these are recorded in the `.buildinfo` `Environment`
|
||||
/// field. Mirrors `Dpkg::BuildInfo::get_build_env_allowed()`.
|
||||
const ENV_ALLOWED: &[&str] = &[
|
||||
// Tool behavior.
|
||||
"POSIXLY_CORRECT",
|
||||
"GETCONF_DIR",
|
||||
// Resolver.
|
||||
"RESOLV_HOST_CONF",
|
||||
"RESOLV_MULTI",
|
||||
"RESOLV_REORDER",
|
||||
"RES_OPTIONS",
|
||||
// Toolchain.
|
||||
"CC",
|
||||
"CPP",
|
||||
"CXX",
|
||||
"OBJC",
|
||||
"OBJCXX",
|
||||
"PC",
|
||||
"FC",
|
||||
"M2C",
|
||||
"AS",
|
||||
"LD",
|
||||
"AR",
|
||||
"RANLIB",
|
||||
"MAKE",
|
||||
"AWK",
|
||||
"LEX",
|
||||
"YACC",
|
||||
// Toolchain flags.
|
||||
"ASFLAGS",
|
||||
"ASFLAGS_FOR_BUILD",
|
||||
"CFLAGS",
|
||||
"CFLAGS_FOR_BUILD",
|
||||
"CPPFLAGS",
|
||||
"CPPFLAGS_FOR_BUILD",
|
||||
"CXXFLAGS",
|
||||
"CXXFLAGS_FOR_BUILD",
|
||||
"OBJCFLAGS",
|
||||
"OBJCFLAGS_FOR_BUILD",
|
||||
"OBJCXXFLAGS",
|
||||
"OBJCXXFLAGS_FOR_BUILD",
|
||||
"DFLAGS",
|
||||
"DFLAGS_FOR_BUILD",
|
||||
"FFLAGS",
|
||||
"FFLAGS_FOR_BUILD",
|
||||
"LDFLAGS",
|
||||
"LDFLAGS_FOR_BUILD",
|
||||
"ARFLAGS",
|
||||
"LFLAGS",
|
||||
"YFLAGS",
|
||||
"MAKEFLAGS",
|
||||
"GNUMAKEFLAGS",
|
||||
// Dynamic linker.
|
||||
"LD_ASSUME_KERNEL",
|
||||
"LD_AUDIT",
|
||||
"LD_BIND_NOT",
|
||||
"LD_BIND_NOW",
|
||||
"LD_DYNAMIC_WEAK",
|
||||
"LD_LIBRARY_PATH",
|
||||
"LD_ORIGIN_PATH",
|
||||
"LD_PREFER_MAP_32BIT_EXEC",
|
||||
"LD_PRELOAD",
|
||||
// Timezone.
|
||||
"TZ",
|
||||
"TZDIR",
|
||||
// Dates.
|
||||
"DATEMSK",
|
||||
// Locale.
|
||||
"LANG",
|
||||
"LANGUAGE",
|
||||
"LC_ALL",
|
||||
"LC_CTYPE",
|
||||
"LC_NUMERIC",
|
||||
"LC_TIME",
|
||||
"LC_COLLATE",
|
||||
"LC_MONETARY",
|
||||
"LC_MESSAGES",
|
||||
"LC_PAPER",
|
||||
"LC_NAME",
|
||||
"LC_ADDRESS",
|
||||
"LC_TELEPHONE",
|
||||
"LC_MEASUREMENT",
|
||||
"LC_IDENTIFICATION",
|
||||
// Locale paths.
|
||||
"LOCPATH",
|
||||
"I18NPATH",
|
||||
"NLSPATH",
|
||||
"GCONV_PATH",
|
||||
// Build flags.
|
||||
"DEB_BUILD_OPTIONS",
|
||||
"DEB_BUILD_PROFILES",
|
||||
"DEB_VENDOR",
|
||||
// dpkg.
|
||||
"DPKG_ROOT",
|
||||
"DPKG_ADMINDIR",
|
||||
"DPKG_DATADIR",
|
||||
"DPKG_ORIGINS_DIR",
|
||||
// dpkg-deb.
|
||||
"DPKG_DEB_COMPRESSOR_TYPE",
|
||||
"DPKG_DEB_COMPRESSOR_LEVEL",
|
||||
// dpkg-gensymbols.
|
||||
"DPKG_GENSYMBOLS_CHECK_LEVEL",
|
||||
// Reproducible builds.
|
||||
"SOURCE_DATE_EPOCH",
|
||||
];
|
||||
|
||||
/// Build the `.buildinfo` `Environment` field value: allowed variables from
|
||||
/// the current process environment plus the `extra` overrides exported to
|
||||
/// build steps (e.g. `SOURCE_DATE_EPOCH`, `DEB_BUILD_OPTIONS`), sorted by
|
||||
/// name, quoted and escaped, one per line.
|
||||
///
|
||||
/// Matches `cleansed_environment()` in `dpkg-genbuildinfo` (minus
|
||||
/// `dpkg-buildflags` origin tracking).
|
||||
pub fn buildinfo_environment(extra: &BTreeMap<String, String>) -> String {
|
||||
let mut values: BTreeMap<String, String> = BTreeMap::new();
|
||||
for var in ENV_ALLOWED {
|
||||
if let Ok(value) = std::env::var(var) {
|
||||
values.insert(var.to_string(), value);
|
||||
}
|
||||
}
|
||||
// Variables we export ourselves always take precedence.
|
||||
for (key, value) in extra {
|
||||
if ENV_ALLOWED.contains(&key.as_str()) {
|
||||
values.insert(key.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
// Leading `\n`: pre-wrapped multiline field, dpkg-style.
|
||||
let mut out = String::from("\n");
|
||||
out.push_str(
|
||||
&values
|
||||
.into_iter()
|
||||
.map(|(var, value)| format!("{}=\"{}\"", var, value.replace('"', "\\\"")))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"),
|
||||
);
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn build_env_values() {
|
||||
let env = build_env(1787392800, 16, &[]);
|
||||
assert_eq!(env.get("SOURCE_DATE_EPOCH").unwrap(), "1787392800");
|
||||
assert_eq!(env.get("DEB_BUILD_OPTIONS").unwrap(), "parallel=16");
|
||||
assert!(env.get("DEB_BUILD_PROFILES").is_none());
|
||||
|
||||
let env = build_env(1, 4, &["nodoc".to_string(), "cross".to_string()]);
|
||||
assert_eq!(env.get("DEB_BUILD_PROFILES").unwrap(), "nodoc,cross");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vendor_defaults() {
|
||||
assert!(default_build_profiles("debian").is_empty());
|
||||
assert_eq!(
|
||||
default_build_profiles("ubuntu"),
|
||||
vec!["derivative.ubuntu".to_string(), "noudeb".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn environment_escaping() {
|
||||
// The function reads the process env; just verify formatting helpers
|
||||
// through a controlled subprocess-free path is not possible, so check
|
||||
// the constant list contains essentials.
|
||||
assert!(ENV_ALLOWED.contains(&"SOURCE_DATE_EPOCH"));
|
||||
assert!(ENV_ALLOWED.contains(&"DEB_BUILD_OPTIONS"));
|
||||
assert!(!ENV_ALLOWED.contains(&"HOME"));
|
||||
assert!(!ENV_ALLOWED.contains(&"PATH"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user