//! 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::>().join(" ") } /// Compute the environment variables exported before running any build step. /// /// Mirrors dpkg behavior: /// - `SOURCE_DATE_EPOCH` from the changelog entry timestamp /// (), /// - `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 { 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 ]`). /// /// 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, 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 { 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 { 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 { if !explicit.is_empty() { return explicit.to_vec(); } if let Ok(value) = std::env::var("DEB_BUILD_PROFILES") { let profiles: Vec = 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 { let mut values: BTreeMap = 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::>() .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")); } }