build: record the actual build environment in .buildinfo
The binary build exported DEB_BUILD_OPTIONS='parallel=<context nproc> 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).
This commit is contained in:
+44
-24
@@ -36,10 +36,13 @@ pub struct BinaryMetadataOptions {
|
||||
pub profiles: Vec<String>,
|
||||
/// 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<String, String>,
|
||||
/// 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<String, String> {
|
||||
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
|
||||
|
||||
+27
-3
@@ -60,11 +60,16 @@ pub fn arch_env(host_arch: Option<&str>) -> Result<BTreeMap<String, String>, 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<String> {
|
||||
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<String> {
|
||||
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
|
||||
|
||||
+4
-3
@@ -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<String, String> = [
|
||||
("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,
|
||||
};
|
||||
|
||||
+23
-24
@@ -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<String, String>,
|
||||
ctx: &Arc<Context>,
|
||||
) -> Result<(PathBuf, PathBuf), Box<dyn Error>> {
|
||||
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::<i64>().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<String, String> =
|
||||
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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user