use super::{CargoConfigSource, CargoConfigs, DiscoveredConfig};
use camino::{Utf8Path, Utf8PathBuf};
use std::{
collections::{btree_map::Entry, BTreeMap, BTreeSet},
ffi::OsString,
process::Command,
};
#[derive(Clone, Debug)]
pub struct EnvironmentMap {
map: BTreeMap<imp::EnvKey, CargoEnvironmentVariable>,
}
impl EnvironmentMap {
pub fn new(configs: &CargoConfigs) -> Self {
let env_configs = configs
.discovered_configs()
.filter_map(|config| match config {
DiscoveredConfig::CliOption { config, source }
| DiscoveredConfig::File { config, source } => Some((config, source)),
DiscoveredConfig::Env => None,
})
.flat_map(|(config, source)| {
let source = match source {
CargoConfigSource::CliOption => None,
CargoConfigSource::File(path) => Some(path.clone()),
};
config
.env
.clone()
.into_iter()
.map(move |(name, value)| (source.clone(), name, value))
});
let mut map = BTreeMap::<imp::EnvKey, CargoEnvironmentVariable>::new();
for (source, name, value) in env_configs {
#[allow(clippy::useless_conversion)]
match map.entry(OsString::from(name.clone()).into()) {
Entry::Occupied(mut entry) => {
let var = entry.get_mut();
if var.force.is_none() && value.force().is_some() {
var.force = value.force();
}
if var.relative.is_none() && value.relative().is_some() {
var.relative = value.relative();
}
}
Entry::Vacant(entry) => {
let force = value.force();
let relative = value.relative();
let value = value.into_value();
entry.insert(CargoEnvironmentVariable {
source,
name,
value,
force,
relative,
});
}
}
}
Self { map }
}
#[cfg(test)]
pub(crate) fn empty() -> Self {
Self {
map: BTreeMap::new(),
}
}
pub(crate) fn apply_env(&self, command: &mut Command) {
#[allow(clippy::useless_conversion)]
let existing_keys: BTreeSet<imp::EnvKey> =
std::env::vars_os().map(|(k, _v)| k.into()).collect();
for (name, var) in &self.map {
let should_set_value = if existing_keys.contains(name) {
var.force.unwrap_or_default()
} else {
true
};
if !should_set_value {
continue;
}
let value = if var.relative.unwrap_or_default() {
let base_path = match &var.source {
Some(source_path) => source_path,
None => unreachable!(
"Cannot use a relative path for environment variable {name:?} \
whose source is not a config file (this should already have been checked)"
),
};
relative_dir_for(base_path).map_or_else(
|| var.value.clone(),
|rel_dir| rel_dir.join(&var.value).into_string(),
)
} else {
var.value.clone()
};
command.env(name, value);
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CargoEnvironmentVariable {
pub source: Option<Utf8PathBuf>,
pub name: String,
pub value: String,
pub force: Option<bool>,
pub relative: Option<bool>,
}
pub fn relative_dir_for(config_path: &Utf8Path) -> Option<&Utf8Path> {
let relative_dir = config_path.parent()?.parent()?;
Some(imp::strip_unc_prefix(relative_dir))
}
#[cfg(windows)]
mod imp {
use super::*;
use std::{borrow::Borrow, cmp, ffi::OsStr, os::windows::prelude::OsStrExt};
use windows::Win32::Globalization::{
CompareStringOrdinal, CSTR_EQUAL, CSTR_GREATER_THAN, CSTR_LESS_THAN,
};
pub(super) fn strip_unc_prefix(path: &Utf8Path) -> &Utf8Path {
dunce::simplified(path.as_std_path())
.try_into()
.expect("stripping verbatim components from a UTF-8 path should result in a UTF-8 path")
}
#[derive(Clone, Debug, Eq)]
#[doc(hidden)]
pub(super) struct EnvKey {
os_string: OsString,
utf16: Vec<u16>,
}
impl Ord for EnvKey {
fn cmp(&self, other: &Self) -> cmp::Ordering {
unsafe {
let result = CompareStringOrdinal(&self.utf16, &other.utf16, true);
match result as u32 {
CSTR_LESS_THAN => cmp::Ordering::Less,
CSTR_EQUAL => cmp::Ordering::Equal,
CSTR_GREATER_THAN => cmp::Ordering::Greater,
_ => panic!(
"comparing environment keys failed: {}",
std::io::Error::last_os_error()
),
}
}
}
}
impl PartialOrd for EnvKey {
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for EnvKey {
fn eq(&self, other: &Self) -> bool {
if self.utf16.len() != other.utf16.len() {
false
} else {
self.cmp(other) == cmp::Ordering::Equal
}
}
}
impl From<OsString> for EnvKey {
fn from(k: OsString) -> Self {
EnvKey {
utf16: k.encode_wide().collect(),
os_string: k,
}
}
}
impl From<EnvKey> for OsString {
fn from(k: EnvKey) -> Self {
k.os_string
}
}
impl Borrow<OsStr> for EnvKey {
fn borrow(&self) -> &OsStr {
&self.os_string
}
}
impl AsRef<OsStr> for EnvKey {
fn as_ref(&self) -> &OsStr {
&self.os_string
}
}
}
#[cfg(not(windows))]
mod imp {
use super::*;
pub(super) fn strip_unc_prefix(path: &Utf8Path) -> &Utf8Path {
path
}
pub(super) type EnvKey = OsString;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cargo_config::{test_helpers::setup_temp_dir, CargoConfigs};
use camino::Utf8PathBuf;
use std::ffi::OsStr;
#[test]
fn test_env_var_precedence() {
let dir = setup_temp_dir().unwrap();
let dir_path = Utf8PathBuf::try_from(dir.path().canonicalize().unwrap()).unwrap();
let dir_foo_path = dir_path.join("foo");
let dir_foo_bar_path = dir_foo_path.join("bar");
let configs =
CargoConfigs::new_with_isolation(&[] as &[&str], &dir_foo_bar_path, &dir_path).unwrap();
let env = EnvironmentMap::new(&configs);
let var = env
.map
.get(OsStr::new("SOME_VAR"))
.expect("SOME_VAR is specified in test config");
assert_eq!(var.value, "foo-bar-config");
let configs = CargoConfigs::new_with_isolation(
["env.SOME_VAR=\"cli-config\""],
&dir_foo_bar_path,
&dir_path,
)
.unwrap();
let env = EnvironmentMap::new(&configs);
let var = env
.map
.get(OsStr::new("SOME_VAR"))
.expect("SOME_VAR is specified in test config");
assert_eq!(var.value, "cli-config");
}
#[test]
fn test_cli_env_var_relative() {
let dir = setup_temp_dir().unwrap();
let dir_path = Utf8PathBuf::try_from(dir.path().canonicalize().unwrap()).unwrap();
let dir_foo_path = dir_path.join("foo");
let dir_foo_bar_path = dir_foo_path.join("bar");
CargoConfigs::new_with_isolation(
["env.SOME_VAR={value = \"path\", relative = true }"],
&dir_foo_bar_path,
&dir_path,
)
.expect_err("CLI configs can't be relative");
CargoConfigs::new_with_isolation(
["env.SOME_VAR.value=\"path\"", "env.SOME_VAR.relative=true"],
&dir_foo_bar_path,
&dir_path,
)
.expect_err("CLI configs can't be relative");
}
#[test]
#[cfg(unix)]
fn test_relative_dir_for_unix() {
assert_eq!(
relative_dir_for("/foo/bar/.cargo/config.toml".as_ref()),
Some("/foo/bar".as_ref()),
);
assert_eq!(
relative_dir_for("/foo/bar/.cargo/config".as_ref()),
Some("/foo/bar".as_ref()),
);
assert_eq!(
relative_dir_for("/foo/bar/config".as_ref()),
Some("/foo".as_ref())
);
assert_eq!(relative_dir_for("/foo/config".as_ref()), Some("/".as_ref()));
assert_eq!(relative_dir_for("/config.toml".as_ref()), None);
}
#[test]
#[cfg(windows)]
fn test_relative_dir_for_windows() {
assert_eq!(
relative_dir_for("C:\\foo\\bar\\.cargo\\config.toml".as_ref()),
Some("C:\\foo\\bar".as_ref()),
);
assert_eq!(
relative_dir_for("C:\\foo\\bar\\.cargo\\config".as_ref()),
Some("C:\\foo\\bar".as_ref()),
);
assert_eq!(
relative_dir_for("C:\\foo\\bar\\config".as_ref()),
Some("C:\\foo".as_ref())
);
assert_eq!(
relative_dir_for("C:\\foo\\config".as_ref()),
Some("C:\\".as_ref())
);
assert_eq!(relative_dir_for("C:\\config.toml".as_ref()), None);
}
}