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>,
|
pub profiles: Vec<String>,
|
||||||
/// Vendor name (`Build-Origin`).
|
/// Vendor name (`Build-Origin`).
|
||||||
pub vendor: String,
|
pub vendor: String,
|
||||||
/// Parallel job count advertised in `DEB_BUILD_OPTIONS`.
|
/// Environment variables pkh exported to the build steps (e.g. `LANG`,
|
||||||
pub parallel: usize,
|
/// `DEB_BUILD_OPTIONS` with the real parallel count and `nocheck`,
|
||||||
/// Reproducible-builds epoch exported to the build.
|
/// `SOURCE_DATE_EPOCH`, cross `DEB_*` variables). Recorded — filtered to
|
||||||
pub source_date_epoch: i64,
|
/// 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).
|
/// Build architecture (the machine inside the build context).
|
||||||
pub build_arch: String,
|
pub build_arch: String,
|
||||||
/// Host architecture (the packages' target); equals the build
|
/// Host architecture (the packages' target); equals the build
|
||||||
@@ -210,8 +213,10 @@ pub fn generate_binary_metadata(
|
|||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// .buildinfo generation, then registration in debian/files
|
// .buildinfo generation, then registration in debian/files
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
let pipeline_env = pipeline_environment(opts);
|
// Record exactly the environment that was exported to the build steps,
|
||||||
let environment = crate::build::env::buildinfo_environment(&pipeline_env);
|
// 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-genbuildinfo sorts the accumulated architecture values, while
|
||||||
// dpkg-genchanges keeps encounter order.
|
// dpkg-genchanges keeps encounter order.
|
||||||
@@ -294,24 +299,6 @@ pub fn generate_binary_metadata(
|
|||||||
Ok((buildinfo_path, changes_path))
|
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
|
/// Compute md5/sha1/sha256 digests and sizes for the named files inside the
|
||||||
/// context directory `dir`, using coreutils.
|
/// context directory `dir`, using coreutils.
|
||||||
fn hashes_in_context(
|
fn hashes_in_context(
|
||||||
@@ -463,6 +450,39 @@ fn include_dsc_artifacts(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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
|
/// A minimal previous-version `.dsc` with a 3-column Checksums-Sha1
|
||||||
/// field, a 4-column Checksums-Sha256 line and a 3-column `Files`.
|
/// field, a 4-column Checksums-Sha256 line and a 3-column `Files`.
|
||||||
/// Regression: the old code filled `names` from any line with a third
|
/// 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`
|
/// Read the current vendor name from `/etc/dpkg/origins/default`
|
||||||
/// (its `Vendor:` or `Origin:` field), defaulting to `"debian"`.
|
/// (its `Vendor:` or `Origin:` field), defaulting to `"debian"`.
|
||||||
pub fn current_vendor() -> String {
|
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> {
|
/// Extract the vendor name from the content of a dpkg origins file: its
|
||||||
let content = std::fs::read_to_string(path).ok()?;
|
/// `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() {
|
for line in content.lines() {
|
||||||
if let Some(value) = line.strip_prefix("Vendor:") {
|
if let Some(value) = line.strip_prefix("Vendor:") {
|
||||||
let v = value.trim();
|
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]
|
#[test]
|
||||||
fn environment_escaping() {
|
fn environment_escaping() {
|
||||||
// The function reads the process env; just verify formatting helpers
|
// 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 vendor = env::current_vendor();
|
||||||
let profiles = env::resolve_build_profiles(&[], &vendor);
|
let profiles = env::resolve_build_profiles(&[], &vendor);
|
||||||
let parallel = env::num_parallel();
|
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()),
|
("LANG".to_string(), "C".to_string()),
|
||||||
(
|
(
|
||||||
"DEB_BUILD_OPTIONS".to_string(),
|
"DEB_BUILD_OPTIONS".to_string(),
|
||||||
@@ -1467,8 +1467,9 @@ Provides: virtual-thing (= 2.0), plain-virtual
|
|||||||
let opts = crate::build::binary::BinaryMetadataOptions {
|
let opts = crate::build::binary::BinaryMetadataOptions {
|
||||||
profiles,
|
profiles,
|
||||||
vendor,
|
vendor,
|
||||||
parallel,
|
// The metadata records exactly the environment exported to the
|
||||||
source_date_epoch: entry.timestamp,
|
// build steps above.
|
||||||
|
exported_env: build_env_vars,
|
||||||
build_arch: native_arch.clone(),
|
build_arch: native_arch.clone(),
|
||||||
host_arch: native_arch,
|
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::deb::{DebUi, Phase};
|
||||||
use crate::ui::logfmt::QuiltClassifier;
|
use crate::ui::logfmt::QuiltClassifier;
|
||||||
use log::warn;
|
use log::warn;
|
||||||
use std::collections::HashMap;
|
use std::collections::{BTreeMap, HashMap};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -420,10 +420,6 @@ fn generate_upload_metadata(
|
|||||||
env: &HashMap<String, String>,
|
env: &HashMap<String, String>,
|
||||||
ctx: &Arc<Context>,
|
ctx: &Arc<Context>,
|
||||||
) -> Result<(PathBuf, PathBuf), Box<dyn Error>> {
|
) -> 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.
|
// Build architecture: the machine inside the build context.
|
||||||
let build_arch = ctx
|
let build_arch = ctx
|
||||||
.command("dpkg")
|
.command("dpkg")
|
||||||
@@ -440,34 +436,37 @@ fn generate_upload_metadata(
|
|||||||
build_arch.clone()
|
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
|
let vendor = ctx
|
||||||
.read_file(Path::new("/etc/dpkg/origins/default"))
|
.read_file(Path::new("/etc/dpkg/origins/default"))
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|content| {
|
.and_then(|content| crate::build::env::vendor_from_origins_content(&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
|
|
||||||
})
|
|
||||||
.unwrap_or_else(crate::build::env::current_vendor);
|
.unwrap_or_else(crate::build::env::current_vendor);
|
||||||
|
|
||||||
let profiles = crate::build::env::resolve_build_profiles(&[], &vendor);
|
// The recorded profiles must describe what the build actually ran with:
|
||||||
let source_date_epoch = env
|
// the DEB_BUILD_PROFILES exported to the build steps ('cross' for cross
|
||||||
.get("SOURCE_DATE_EPOCH")
|
// builds), else the vendor defaults.
|
||||||
.and_then(|v| v.parse::<i64>().ok())
|
let profiles = match env.get("DEB_BUILD_PROFILES") {
|
||||||
.unwrap_or(entry.timestamp);
|
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 {
|
let opts = crate::build::binary::BinaryMetadataOptions {
|
||||||
profiles,
|
profiles,
|
||||||
vendor,
|
vendor,
|
||||||
parallel: crate::build::env::num_parallel(),
|
exported_env,
|
||||||
source_date_epoch,
|
|
||||||
build_arch,
|
build_arch,
|
||||||
host_arch,
|
host_arch,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user