441 lines
15 KiB
Rust
441 lines
15 KiB
Rust
//! Build environment setup: `SOURCE_DATE_EPOCH`, `DEB_BUILD_OPTIONS`,
|
|
//! architecture variables (native `dpkg-architecture` equivalent) and the
|
|
//! sanitized environment recorded in `.buildinfo` files.
|
|
|
|
use std::collections::BTreeMap;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
/// 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)
|
|
}
|
|
|
|
/// Merge an inherited `DEB_BUILD_OPTIONS` value with options pkh computes
|
|
/// itself.
|
|
///
|
|
/// `dpkg-buildpackage` prepends the environment's `DEB_BUILD_OPTIONS` to the
|
|
/// options it derives (`parallel=N`, ...), so caller-set options such as
|
|
/// `terse` or `nocheck` survive alongside pkh's own. The result is therefore
|
|
/// the inherited options followed by `computed`, space-separated; each side is
|
|
/// trimmed and its internal whitespace runs collapsed. An unset or blank
|
|
/// inherited value yields just `computed`.
|
|
pub fn merge_deb_build_options(inherited: Option<&str>, computed: &str) -> String {
|
|
let computed = normalize_build_options(computed);
|
|
match inherited.map(normalize_build_options) {
|
|
Some(inherited) if !inherited.is_empty() => format!("{} {}", inherited, computed),
|
|
_ => computed,
|
|
}
|
|
}
|
|
|
|
/// Trim and collapse internal whitespace in a `DEB_BUILD_OPTIONS` fragment.
|
|
fn normalize_build_options(options: &str) -> String {
|
|
options.split_whitespace().collect::<Vec<_>>().join(" ")
|
|
}
|
|
|
|
/// Compute the environment variables exported 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`: any value inherited from the invoking environment
|
|
/// (dpkg-buildpackage prepends it) followed by `parallel=N` (auto-detected
|
|
/// job count),
|
|
/// - `DEB_BUILD_PROFILES` when non-default profiles are requested.
|
|
///
|
|
/// The locale is pinned to `C` (`LC_ALL`, which takes precedence over any
|
|
/// inherited session setting, plus `LANG`) so build tools emit deterministic,
|
|
/// English diagnostics — required for reliable log classification and
|
|
/// reproducible builds.
|
|
pub fn build_env(
|
|
source_date_epoch: i64,
|
|
parallel: usize,
|
|
build_profiles: &[String],
|
|
) -> BTreeMap<String, String> {
|
|
let mut env = BTreeMap::new();
|
|
env.insert("LANG".to_string(), "C".to_string());
|
|
env.insert("LC_ALL".to_string(), "C".to_string());
|
|
env.insert(
|
|
"SOURCE_DATE_EPOCH".to_string(),
|
|
source_date_epoch.to_string(),
|
|
);
|
|
env.insert(
|
|
"DEB_BUILD_OPTIONS".to_string(),
|
|
merge_deb_build_options(
|
|
std::env::var("DEB_BUILD_OPTIONS").ok().as_deref(),
|
|
&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, computed natively by
|
|
/// [`crate::debian::arch`] (the equivalent of `dpkg-architecture -f
|
|
/// [-a <host-arch>]`).
|
|
///
|
|
/// 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> {
|
|
crate::debian::arch::arch_env(host_arch)
|
|
}
|
|
|
|
/// Read the current vendor name from the active dpkg origins `default` file
|
|
/// (`$DPKG_ORIGINS_DIR/default`, falling back to `/etc/dpkg/origins/default`;
|
|
/// its `Vendor:` or `Origin:` field), defaulting to `"debian"`.
|
|
pub fn current_vendor() -> String {
|
|
let path = resolve_origins_default(
|
|
std::env::var("DPKG_ORIGINS_DIR").ok().as_deref(),
|
|
"/etc/dpkg/origins",
|
|
);
|
|
std::fs::read_to_string(path)
|
|
.ok()
|
|
.and_then(|content| vendor_from_origins_content(&content))
|
|
.unwrap_or_else(|| "debian".to_string())
|
|
}
|
|
|
|
/// Resolve the path of the active dpkg origins file from the
|
|
/// `DPKG_ORIGINS_DIR` value (the directory holding the origin files, where
|
|
/// `default` selects the active one) and the fallback directory
|
|
/// (`/etc/dpkg/origins`). An unset or empty directory value falls back.
|
|
fn resolve_origins_default(origins_dir: Option<&str>, fallback_dir: &str) -> PathBuf {
|
|
let dir = origins_dir
|
|
.filter(|d| !d.is_empty())
|
|
.unwrap_or(fallback_dir);
|
|
Path::new(dir).join("default")
|
|
}
|
|
|
|
/// 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();
|
|
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 distro data carries them (`build_profiles` of the vendor's
|
|
/// distribution in `data/distro_info.yml` — the Ubuntu vendor activates
|
|
/// `derivative.ubuntu noudeb`, Debian applies none), mirroring what
|
|
/// `Dpkg::BuildProfiles` resolves when `DEB_BUILD_PROFILES` is unset. The
|
|
/// vendor is matched case-insensitively against the distro data keys
|
|
/// (dpkg's `Vendor:` field keeps its original casing); a vendor with no
|
|
/// distro entry gets no profiles.
|
|
pub fn default_build_profiles(vendor: &str) -> Vec<String> {
|
|
crate::distro_info::get_build_profiles(&vendor.to_lowercase()).unwrap_or_default()
|
|
}
|
|
|
|
/// 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("LANG").unwrap(), "C");
|
|
assert_eq!(env.get("LC_ALL").unwrap(), "C");
|
|
assert_eq!(env.get("SOURCE_DATE_EPOCH").unwrap(), "1787392800");
|
|
// Reading the var is race-free; the expected value goes through the
|
|
// same merge so the assertion holds whatever the ambient environment
|
|
// carries.
|
|
let expected = merge_deb_build_options(
|
|
std::env::var("DEB_BUILD_OPTIONS").ok().as_deref(),
|
|
"parallel=16",
|
|
);
|
|
assert_eq!(env.get("DEB_BUILD_OPTIONS").unwrap(), &expected);
|
|
assert!(!env.contains_key("DEB_BUILD_PROFILES"));
|
|
|
|
let env = build_env(1, 4, &["nodoc".to_string(), "cross".to_string()]);
|
|
assert_eq!(env.get("DEB_BUILD_PROFILES").unwrap(), "nodoc,cross");
|
|
}
|
|
|
|
/// dpkg-buildpackage prepends the inherited `DEB_BUILD_OPTIONS`, so
|
|
/// user-set options survive alongside the computed ones.
|
|
#[test]
|
|
fn merge_prepends_inherited_options() {
|
|
assert_eq!(
|
|
merge_deb_build_options(Some("terse"), "parallel=16"),
|
|
"terse parallel=16"
|
|
);
|
|
assert_eq!(
|
|
merge_deb_build_options(Some("nocheck terse"), "parallel=4"),
|
|
"nocheck terse parallel=4"
|
|
);
|
|
}
|
|
|
|
/// An unset, empty or blank inherited value yields just the computed
|
|
/// options.
|
|
#[test]
|
|
fn merge_skips_empty_inherited() {
|
|
assert_eq!(merge_deb_build_options(None, "parallel=8"), "parallel=8");
|
|
assert_eq!(
|
|
merge_deb_build_options(Some(""), "parallel=8"),
|
|
"parallel=8"
|
|
);
|
|
assert_eq!(
|
|
merge_deb_build_options(Some(" "), "parallel=8"),
|
|
"parallel=8"
|
|
);
|
|
}
|
|
|
|
/// Both sides are trimmed and internal whitespace runs collapsed: no
|
|
/// leading/trailing space, no double spaces in the merged result.
|
|
#[test]
|
|
fn merge_normalizes_whitespace() {
|
|
assert_eq!(
|
|
merge_deb_build_options(Some(" terse "), "parallel=2"),
|
|
"terse parallel=2"
|
|
);
|
|
assert_eq!(
|
|
merge_deb_build_options(Some("nocheck\t terse"), "parallel=2"),
|
|
"nocheck terse parallel=2"
|
|
);
|
|
}
|
|
|
|
#[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()]
|
|
);
|
|
// dpkg's Vendor field keeps its original casing; the distro data
|
|
// keys are lowercase.
|
|
assert_eq!(
|
|
default_build_profiles("Ubuntu"),
|
|
vec!["derivative.ubuntu".to_string(), "noudeb".to_string()]
|
|
);
|
|
// A vendor without a distro entry gets no profiles.
|
|
assert!(default_build_profiles("some-derivative").is_empty());
|
|
}
|
|
|
|
#[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);
|
|
}
|
|
|
|
/// `current_vendor` must honor `DPKG_ORIGINS_DIR` (already on the
|
|
/// `.buildinfo` allow-list) when locating the `default` origins file,
|
|
/// falling back to `/etc/dpkg/origins/default` when unset or empty.
|
|
#[test]
|
|
fn origins_default_path_honors_dpkg_origins_dir() {
|
|
assert_eq!(
|
|
resolve_origins_default(Some("/custom/origins"), "/etc/dpkg/origins"),
|
|
PathBuf::from("/custom/origins/default")
|
|
);
|
|
assert_eq!(
|
|
resolve_origins_default(None, "/etc/dpkg/origins"),
|
|
PathBuf::from("/etc/dpkg/origins/default")
|
|
);
|
|
// An empty value behaves as unset, like dpkg's `$dir || $default`.
|
|
assert_eq!(
|
|
resolve_origins_default(Some(""), "/etc/dpkg/origins"),
|
|
PathBuf::from("/etc/dpkg/origins/default")
|
|
);
|
|
}
|
|
|
|
#[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"));
|
|
}
|
|
}
|