use std::{
collections::hash_map::DefaultHasher,
env,
ffi::OsStr,
fmt,
hash::{Hash, Hasher},
process::Command,
sync::Arc,
};
use crate::{
target::{TargetInfo, TargetInfoParser},
utilities::OnceLock,
Error, ErrorKind,
};
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 {
Self {
inherited: inherited.into(),
explicit,
}
}
pub(crate) fn inherited(&self) -> &EnvSnapshot {
self.inherited.get_or_init(EnvSnapshot::capture)
}
pub(crate) fn set_inherited(&mut self, inherited: EnvSnapshot) {
self.inherited = inherited.into();
}
pub(crate) fn apply(&self, cmd: &mut Command) {
self.inherited().apply(cmd);
cmd.envs(pairs(&self.explicit));
}
}
#[derive(Clone)]
pub(crate) struct EnvSnapshot {
vars: Arc<EnvVars>,
hash: u64,
cargo_target: Arc<TargetInfoParser>,
}
impl EnvSnapshot {
fn new(vars: Arc<EnvVars>) -> Self {
let mut hasher = DefaultHasher::new();
vars.hash(&mut hasher);
Self {
hash: hasher.finish(),
vars,
cargo_target: Arc::default(),
}
}
pub(crate) fn capture() -> Self {
Self::from_vars(&mut env::vars_os().map(|(key, value)| (key.into(), value.into())))
}
pub(crate) fn from_vars(vars: &mut dyn Iterator<Item = (Arc<OsStr>, Arc<OsStr>)>) -> Self {
let mut deduped: Vec<(Arc<OsStr>, Arc<OsStr>)> = Vec::with_capacity(vars.size_hint().0);
for (key, value) in vars {
match deduped.iter_mut().find(|(k, _)| is_same_key(k, &key)) {
Some(entry) => entry.1 = value,
None => deduped.push((key, value)),
}
}
Self::new(deduped.into())
}
#[cfg(test)]
pub(crate) fn from_pairs(vars: &[(&str, &str)]) -> Self {
Self::new(
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 get_cargo_var(&self, key: &str) -> Result<&str, Error> {
let Some(value) = self.get(OsStr::new(key)) else {
return Err(Error::new(
ErrorKind::EnvVarNotFound,
format!("environment variable {key} not defined"),
));
};
value.to_str().ok_or_else(|| {
Error::new(
ErrorKind::EnvVarNotFound,
format!("environment variable {key} is not valid utf-8: {value:?}"),
)
})
}
pub(crate) fn cargo_target(&self) -> Result<TargetInfo<'_>, Error> {
self.cargo_target
.parse_from_cargo_environment_variables(self)
}
pub(crate) fn apply(&self, cmd: &mut Command) {
cmd.env_clear();
cmd.envs(pairs(&self.vars));
}
}
impl PartialEq for EnvSnapshot {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.vars, &other.vars) || (self.hash == other.hash && self.vars == other.vars)
}
}
impl Eq for EnvSnapshot {}
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 set_inherited_replaces_a_taken_snapshot() {
let mut env = BuildEnv::default();
env.inherited();
env.set_inherited(snapshot(&[("CC_TEST_REPLACED", "1")]));
assert_eq!(env.inherited(), &snapshot(&[("CC_TEST_REPLACED", "1")]));
}
#[test]
fn snapshot_from_vars_keeps_the_last_value() {
let snapshot = EnvSnapshot::from_vars(
&mut [
("CC_TEST_TWICE", "first"),
("Path", "a"),
("CC_TEST_TWICE", "last"),
("PATH", "b"),
]
.into_iter()
.map(|(key, value)| (OsStr::new(key).into(), OsStr::new(value).into())),
);
assert_eq!(
snapshot
.get(OsStr::new("CC_TEST_TWICE"))
.map(|value| &**value),
Some(OsStr::new("last"))
);
let mut cmd = Command::new("cc");
snapshot.apply(&mut cmd);
for key in ["CC_TEST_TWICE", "Path", "PATH"] {
let key = OsStr::new(key);
let in_child = cmd
.get_envs()
.filter(|(k, _)| is_same_key(k, key))
.last()
.and_then(|(_, value)| value);
assert_eq!(snapshot.get(key).map(|value| &**value), in_child, "{key:?}");
}
}
#[test]
fn snapshot_comparison_follows_the_variables() {
let a = snapshot(&[("CC_TEST_A", "1"), ("CC_TEST_B", "2")]);
let same = snapshot(&[("CC_TEST_A", "1"), ("CC_TEST_B", "2")]);
assert!(!Arc::ptr_eq(&a.vars, &same.vars));
assert_eq!(a, same);
assert_eq!(a.hash, same.hash);
assert_eq!(a, a.clone());
for other in [
snapshot(&[("CC_TEST_A", "1")]),
snapshot(&[("CC_TEST_A", "1"), ("CC_TEST_B", "3")]),
snapshot(&[("CC_TEST_A", "1"), ("CC_TEST_B2", "")]),
snapshot(&[("CC_TEST_A", "1"), ("cc_test_b", "2")]),
] {
assert_ne!(a, other);
}
}
#[test]
fn snapshot_debug_hides_values() {
let debug = format!("{:?}", snapshot(&[("CC_TEST_SECRET", "hunter2")]));
assert!(!debug.contains("hunter2"), "{debug}");
}
}