From 3a454b08113eef228a35fe07f6eb44c319642742 Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Wed, 16 Sep 2026 04:22:43 +0200 Subject: [PATCH] build: record the actual build environment in .buildinfo The binary build exported DEB_BUILD_OPTIONS='parallel= nocheck' (or the -j override) but the generated .buildinfo recomputed the environment from host state: host core count, no nocheck, and vendor profiles that ignored DEB_BUILD_PROFILES (a cross build recorded no 'cross' profile). generate_binary_metadata now records the exact env map that was exported to the build steps, and the recorded profiles come from the exported DEB_BUILD_PROFILES when set. Also unifies vendor parsing on one helper (the context-side copy lacked the Origin: fallback of the source-build path). --- src/build/binary.rs | 68 +++++++++++++++++++++++++++++---------------- src/build/env.rs | 30 ++++++++++++++++++-- src/build/mod.rs | 7 +++-- src/deb/local.rs | 47 +++++++++++++++---------------- 4 files changed, 98 insertions(+), 54 deletions(-) diff --git a/src/build/binary.rs b/src/build/binary.rs index 83b565e..1867d82 100644 --- a/src/build/binary.rs +++ b/src/build/binary.rs @@ -36,10 +36,13 @@ pub struct BinaryMetadataOptions { pub profiles: Vec, /// Vendor name (`Build-Origin`). pub vendor: String, - /// Parallel job count advertised in `DEB_BUILD_OPTIONS`. - pub parallel: usize, - /// Reproducible-builds epoch exported to the build. - pub source_date_epoch: i64, + /// Environment variables pkh exported to the build steps (e.g. `LANG`, + /// `DEB_BUILD_OPTIONS` with the real parallel count and `nocheck`, + /// `SOURCE_DATE_EPOCH`, cross `DEB_*` variables). Recorded — filtered to + /// dpkg's allow-list — in the `.buildinfo` `Environment` field, taking + /// precedence over whatever the host process inherited, so the metadata + /// describes the environment the build actually ran in. + pub exported_env: BTreeMap, /// Build architecture (the machine inside the build context). pub build_arch: String, /// Host architecture (the packages' target); equals the build @@ -210,8 +213,10 @@ pub fn generate_binary_metadata( // ------------------------------------------------------------------ // .buildinfo generation, then registration in debian/files // ------------------------------------------------------------------ - let pipeline_env = pipeline_environment(opts); - let environment = crate::build::env::buildinfo_environment(&pipeline_env); + // Record exactly the environment that was exported to the build steps, + // overriding any host-inherited value (dpkg-style allowed-variable + // filtering, export precedence). + let environment = crate::build::env::buildinfo_environment(&opts.exported_env); // dpkg-genbuildinfo sorts the accumulated architecture values, while // dpkg-genchanges keeps encounter order. @@ -294,24 +299,6 @@ pub fn generate_binary_metadata( Ok((buildinfo_path, changes_path)) } -/// Environment exported to the build steps; recorded (filtered) in the -/// `.buildinfo` `Environment` field. -fn pipeline_environment(opts: &BinaryMetadataOptions) -> BTreeMap { - let mut env = BTreeMap::new(); - env.insert( - "SOURCE_DATE_EPOCH".to_string(), - opts.source_date_epoch.to_string(), - ); - env.insert( - "DEB_BUILD_OPTIONS".to_string(), - format!("parallel={}", opts.parallel), - ); - if !opts.profiles.is_empty() { - env.insert("DEB_BUILD_PROFILES".to_string(), opts.profiles.join(",")); - } - env -} - /// Compute md5/sha1/sha256 digests and sizes for the named files inside the /// context directory `dir`, using coreutils. fn hashes_in_context( @@ -463,6 +450,39 @@ fn include_dsc_artifacts( mod tests { use super::*; + /// The recorded `.buildinfo` `Environment` must carry the environment + /// actually exported to the build steps (`parallel=N nocheck`, `LANG=C`, + /// ...), taking precedence over any host-inherited value, instead of + /// values recomputed from host state at generation time. + #[test] + fn environment_records_exported_env_not_host_defaults() { + let mut exported_env = BTreeMap::new(); + exported_env.insert("LANG".to_string(), "C".to_string()); + exported_env.insert( + "DEB_BUILD_OPTIONS".to_string(), + "parallel=7 nocheck".to_string(), + ); + let opts = BinaryMetadataOptions { + profiles: Vec::new(), + vendor: "debian".to_string(), + exported_env, + build_arch: "amd64".to_string(), + host_arch: "amd64".to_string(), + }; + + let environment = crate::build::env::buildinfo_environment(&opts.exported_env); + assert!( + environment.contains("DEB_BUILD_OPTIONS=\"parallel=7 nocheck\""), + "recorded Environment must carry the exported DEB_BUILD_OPTIONS: {environment}" + ); + assert!( + environment.contains("LANG=\"C\""), + "recorded Environment must carry the exported LANG: {environment}" + ); + // Not in dpkg's allowed-variable list: never recorded. + assert!(!environment.contains("DEBIAN_FRONTEND"), "{environment}"); + } + /// A minimal previous-version `.dsc` with a 3-column Checksums-Sha1 /// field, a 4-column Checksums-Sha256 line and a 3-column `Files`. /// Regression: the old code filled `names` from any line with a third diff --git a/src/build/env.rs b/src/build/env.rs index e1cd431..faa11c1 100644 --- a/src/build/env.rs +++ b/src/build/env.rs @@ -60,11 +60,16 @@ pub fn arch_env(host_arch: Option<&str>) -> Result, Str /// 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()) + std::fs::read_to_string(Path::new("/etc/dpkg/origins/default")) + .ok() + .and_then(|content| vendor_from_origins_content(&content)) + .unwrap_or_else(|| "debian".to_string()) } -fn read_vendor_from(path: &Path) -> Option { - let content = std::fs::read_to_string(path).ok()?; +/// Extract the vendor name from the content of a dpkg origins file: its +/// `Vendor:` field, falling back to `Origin:` when absent. `None` when +/// neither field carries a non-empty value. +pub fn vendor_from_origins_content(content: &str) -> Option { for line in content.lines() { if let Some(value) = line.strip_prefix("Vendor:") { let v = value.trim(); @@ -282,6 +287,25 @@ mod tests { ); } + #[test] + fn vendor_from_origins_content_prefers_vendor_then_origin() { + assert_eq!( + vendor_from_origins_content("Vendor: Ubuntu\nSuite: noble\n"), + Some("Ubuntu".to_string()) + ); + // Origin fallback when no Vendor field is present. + assert_eq!( + vendor_from_origins_content("Origin: Debian\nSuite: stable\n"), + Some("Debian".to_string()) + ); + // Empty Vendor falls through to Origin. + assert_eq!( + vendor_from_origins_content("Vendor: \nOrigin: Debian\n"), + Some("Debian".to_string()) + ); + assert_eq!(vendor_from_origins_content("Suite: stable\n"), None); + } + #[test] fn environment_escaping() { // The function reads the process env; just verify formatting helpers diff --git a/src/build/mod.rs b/src/build/mod.rs index ad8b215..ef80f8c 100644 --- a/src/build/mod.rs +++ b/src/build/mod.rs @@ -1439,7 +1439,7 @@ Provides: virtual-thing (= 2.0), plain-virtual let vendor = env::current_vendor(); let profiles = env::resolve_build_profiles(&[], &vendor); let parallel = env::num_parallel(); - let build_env_vars: Vec<(String, String)> = [ + let build_env_vars: BTreeMap = [ ("LANG".to_string(), "C".to_string()), ( "DEB_BUILD_OPTIONS".to_string(), @@ -1467,8 +1467,9 @@ Provides: virtual-thing (= 2.0), plain-virtual let opts = crate::build::binary::BinaryMetadataOptions { profiles, vendor, - parallel, - source_date_epoch: entry.timestamp, + // The metadata records exactly the environment exported to the + // build steps above. + exported_env: build_env_vars, build_arch: native_arch.clone(), host_arch: native_arch, }; diff --git a/src/deb/local.rs b/src/deb/local.rs index 462783d..1c05552 100644 --- a/src/deb/local.rs +++ b/src/deb/local.rs @@ -5,7 +5,7 @@ use crate::deb::find_dsc_file; use crate::ui::deb::{DebUi, Phase}; use crate::ui::logfmt::QuiltClassifier; use log::warn; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use std::error::Error; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -420,10 +420,6 @@ fn generate_upload_metadata( env: &HashMap, ctx: &Arc, ) -> Result<(PathBuf, PathBuf), Box> { - let changelog_path = Path::new(package_dir).join("debian/changelog"); - let changelog_content = ctx.read_file(&changelog_path)?; - let entry = crate::debian::parse_changelog_entry_from_str(&changelog_content)?; - // Build architecture: the machine inside the build context. let build_arch = ctx .command("dpkg") @@ -440,34 +436,37 @@ fn generate_upload_metadata( build_arch.clone() }; - // Vendor resolution inside the context (falls back to the host view). + // Vendor resolution inside the context (falls back to the host view); + // shared `Vendor:`/`Origin:` parsing with the source-build path. let vendor = ctx .read_file(Path::new("/etc/dpkg/origins/default")) .ok() - .and_then(|content| { - for line in content.lines() { - if let Some(v) = line.strip_prefix("Vendor:") { - let v = v.trim(); - if !v.is_empty() { - return Some(v.to_string()); - } - } - } - None - }) + .and_then(|content| crate::build::env::vendor_from_origins_content(&content)) .unwrap_or_else(crate::build::env::current_vendor); - let profiles = crate::build::env::resolve_build_profiles(&[], &vendor); - let source_date_epoch = env - .get("SOURCE_DATE_EPOCH") - .and_then(|v| v.parse::().ok()) - .unwrap_or(entry.timestamp); + // The recorded profiles must describe what the build actually ran with: + // the DEB_BUILD_PROFILES exported to the build steps ('cross' for cross + // builds), else the vendor defaults. + let profiles = match env.get("DEB_BUILD_PROFILES") { + Some(value) => value + .split(',') + .map(|p| p.trim().to_string()) + .filter(|p| !p.is_empty()) + .collect(), + None => crate::build::env::resolve_build_profiles(&[], &vendor), + }; + + // Record exactly the environment exported to the build steps + // (DEB_BUILD_OPTIONS with the real parallel count and 'nocheck', LANG=C, + // SOURCE_DATE_EPOCH, cross DEB_* variables, ...), not values recomputed + // from host state; buildinfo_environment filters out non-dpkg variables. + let exported_env: BTreeMap = + env.iter().map(|(k, v)| (k.clone(), v.clone())).collect(); let opts = crate::build::binary::BinaryMetadataOptions { profiles, vendor, - parallel: crate::build::env::num_parallel(), - source_date_epoch, + exported_env, build_arch, host_arch, };