use std::{env, ffi::OsStr, fmt, process::Command, sync::Arc};
use crate::utilities::OnceLock;
pub(crate) type EnvVars = [(Arc<OsStr>, Arc<OsStr>)];
#[derive(Clone, Debug, Default)]
pub(crate) struct BuildEnv {
inherited: OnceLock<EnvSnapshot>,
pub(crate) explicit: Vec<(Arc<OsStr>, Arc<OsStr>)>,
}
impl BuildEnv {
pub(crate) fn with_inherited(
inherited: EnvSnapshot,
explicit: Vec<(Arc<OsStr>, Arc<OsStr>)>,
) -> Self {
let cell = OnceLock::new();
cell.get_or_init(|| inherited);
Self {
inherited: cell,
explicit,
}
}
pub(crate) fn inherited(&self) -> &EnvSnapshot {
self.inherited.get_or_init(EnvSnapshot::capture)
}
pub(crate) fn apply(&self, cmd: &mut Command) {
self.inherited().apply(cmd);
cmd.envs(pairs(&self.explicit));
}
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct EnvSnapshot {
vars: Arc<EnvVars>,
}
impl EnvSnapshot {
pub(crate) fn capture() -> Self {
Self {
vars: env::vars_os()
.map(|(key, value)| (key.into(), value.into()))
.collect(),
}
}
#[cfg(test)]
pub(crate) fn from_pairs(vars: &[(&str, &str)]) -> Self {
Self {
vars: vars
.iter()
.map(|(key, value)| (OsStr::new(key).into(), OsStr::new(value).into()))
.collect(),
}
}
pub(crate) fn get(&self, key: &OsStr) -> Option<&Arc<OsStr>> {
self.vars
.iter()
.find(|(k, _)| is_same_key(k, key))
.map(|(_, value)| value)
}
pub(crate) fn apply(&self, cmd: &mut Command) {
cmd.env_clear();
cmd.envs(pairs(&self.vars));
}
}
impl fmt::Debug for EnvSnapshot {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "EnvSnapshot(<{} variables>)", self.vars.len())
}
}
fn pairs(vars: &EnvVars) -> impl Iterator<Item = (&OsStr, &OsStr)> {
vars.iter().map(|(key, value)| (&**key, &**value))
}
fn is_same_key(a: &OsStr, b: &OsStr) -> bool {
if cfg!(windows) {
a.eq_ignore_ascii_case(b)
} else {
a == b
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::OsString;
fn snapshot(vars: &[(&str, &str)]) -> EnvSnapshot {
EnvSnapshot::from_pairs(vars)
}
fn envs(cmd: &Command) -> Vec<(OsString, Option<OsString>)> {
cmd.get_envs()
.map(|(key, value)| (key.to_owned(), value.map(OsStr::to_owned)))
.collect()
}
#[test]
fn apply_replaces_the_whole_environment() {
let mut cmd = Command::new("cc");
cmd.env("CC_TEST_SET_BEFORE", "1")
.env_remove("CC_TEST_REMOVED_BEFORE");
snapshot(&[("CC_TEST_IN_SNAPSHOT", "1")]).apply(&mut cmd);
assert_eq!(
envs(&cmd),
[("CC_TEST_IN_SNAPSHOT".into(), Some("1".into()))]
);
}
#[test]
fn build_env_wins_over_snapshot() {
let env = BuildEnv::with_inherited(
snapshot(&[("CC_TEST_ORDER", "inherited")]),
vec![(
OsStr::new("CC_TEST_ORDER").into(),
OsStr::new("explicit").into(),
)],
);
let mut cmd = Command::new("cc");
env.apply(&mut cmd);
assert_eq!(
envs(&cmd),
[("CC_TEST_ORDER".into(), Some("explicit".into()))]
);
}
#[test]
fn snapshot_lookup_shares_the_value() {
let snapshot = snapshot(&[("CC_TEST_SHARED", "a")]);
let value = snapshot.get(OsStr::new("CC_TEST_SHARED")).unwrap();
assert!(Arc::ptr_eq(value, &snapshot.vars[0].1));
}
#[test]
fn snapshot_lookup_follows_the_platform_case_rule() {
let snapshot = snapshot(&[("Path", "a")]);
assert_eq!(
snapshot.get(OsStr::new("Path")).map(|value| &**value),
Some(OsStr::new("a"))
);
let other_case = snapshot.get(OsStr::new("PATH")).map(|value| &**value);
if cfg!(windows) {
assert_eq!(other_case, Some(OsStr::new("a")));
} else {
assert_eq!(other_case, None);
}
}
#[test]
fn snapshot_debug_hides_values() {
let debug = format!("{:?}", snapshot(&[("CC_TEST_SECRET", "hunter2")]));
assert!(!debug.contains("hunter2"), "{debug}");
}
}