Route 'pkh build' through the DebUi capture machinery 'pkh deb' already uses instead of letting dpkg-source inherit the terminal: - pin LANG=C and LC_ALL=C so dpkg-source emits deterministic English diagnostics regardless of the session locale; - new DpkgSourceClassifier rewrites info:/warning:/error: lines into colored pane entries, telling benign tar warnings from failures; - DebUi generalizes for reuse (arbitrary phase labels, build-specific log naming); run_source_build() drives phases and pipes subprocess output through the sink when a UI is present; - glyph-free house-style summaries: 'Built in Ns:' plus artifact paths relative to cwd; failures print captured errors + log path; - drop/capitalize pipeline chatter, add 'pkh build --verbose' to bypass the view like 'pkh deb --verbose'.
296 lines
8.7 KiB
Rust
296 lines
8.7 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;
|
|
|
|
/// 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 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.
|
|
///
|
|
/// 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(),
|
|
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 `/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("LANG").unwrap(), "C");
|
|
assert_eq!(env.get("LC_ALL").unwrap(), "C");
|
|
assert_eq!(env.get("SOURCE_DATE_EPOCH").unwrap(), "1787392800");
|
|
assert_eq!(env.get("DEB_BUILD_OPTIONS").unwrap(), "parallel=16");
|
|
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");
|
|
}
|
|
|
|
#[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"));
|
|
}
|
|
}
|