| //! Encapsulates snapshotting of environment variables. | |
| use crate::util::data_structures::HashMap; | |
| use std::ffi::{OsStr, OsString}; | |
| use crate::util::errors::CargoResult; | |
| use anyhow::{anyhow, bail}; | |
| /// A snapshot of the environment variables available to [`GlobalContext`]. | |
| /// | |
| /// Currently, the [`GlobalContext`] supports lookup of environment variables | |
| /// through two different means: | |
| /// | |
| /// - [`GlobalContext::get_env`](super::GlobalContext::get_env) | |
| /// and [`GlobalContext::get_env_os`](super::GlobalContext::get_env_os) | |
| /// for process environment variables (similar to [`std::env::var`] and [`std::env::var_os`]), | |
| /// - Typed Config Value API via [`GlobalContext::get`](super::GlobalContext::get). | |
| /// This is only available for `CARGO_` prefixed environment keys. | |
| /// | |
| /// This type contains the env var snapshot and helper methods for both APIs. | |
| /// | |
| /// [`GlobalContext`]: super::GlobalContext | |
| #[derive(Debug)] | |
| pub struct Env { | |
| /// A snapshot of the process's environment variables. | |
| env: HashMap<OsString, OsString>, | |
| /// Used in the typed Config value API for warning messages when config keys are | |
| /// given in the wrong format. | |
| /// | |
| /// Maps from "normalized" (upper case and with "-" replaced by "_") env keys | |
| /// to the actual keys in the environment. | |
| /// The normalized format is the one expected by Cargo. | |
| /// | |
| /// This only holds env keys that are valid UTF-8, since [`super::ConfigKey`] only supports UTF-8 keys. | |
| /// In addition, this only holds env keys whose value in the environment is also valid UTF-8, | |
| /// since the typed Config value API only supports UTF-8 values. | |
| normalized_env: HashMap<String, String>, | |
| /// Used to implement `get_env` and `get_env_os` on Windows, where env keys are case-insensitive. | |
| /// | |
| /// Maps from uppercased env keys to the actual key in the environment. | |
| /// For example, this might hold a pair `("PATH", "Path")`. | |
| /// Currently only supports UTF-8 keys and values. | |
| #[cfg(windows)] | |
| case_insensitive_env: HashMap<String, String>, | |
| } | |
| impl Env { | |
| /// Create a new `Env` from process's environment variables. | |
| pub fn new() -> Self { | |
| #[expect(clippy::disallowed_methods, reason = "seeds `GlobalContext::get_env`")] | |
| let env: HashMap<_, _> = std::env::vars_os().collect(); | |
| Self::from_map_os(env) | |
| } | |
| /// Set the env directly from a `HashMap`. | |
| /// This should be used for debugging purposes only. | |
| pub(super) fn from_map(env: HashMap<String, String>) -> Self { | |
| let env = env.into_iter().map(|(k, v)| (k.into(), v.into())).collect(); | |
| Self::from_map_os(env) | |
| } | |
| pub(super) fn from_map_os(env: HashMap<OsString, OsString>) -> Self { | |
| #[cfg(windows)] | |
| let case_insensitive_env = make_case_insensitive_env(&env); | |
| let normalized_env = make_normalized_env(&env); | |
| Self { | |
| env, | |
| #[cfg(windows)] | |
| case_insensitive_env, | |
| normalized_env, | |
| } | |
| } | |
| /// Returns all environment variables as an iterator, | |
| /// keeping only entries where both the key and value are valid UTF-8. | |
| pub fn iter_str(&self) -> impl Iterator<Item = (&str, &str)> { | |
| self.env | |
| .iter() | |
| .filter_map(|(k, v)| Some((k.to_str()?, v.to_str()?))) | |
| } | |
| /// Returns all environment variable keys, filtering out keys that are not valid UTF-8. | |
| pub fn keys_str(&self) -> impl Iterator<Item = &str> { | |
| self.env.keys().filter_map(|k| k.to_str()) | |
| } | |
| /// Get the value of environment variable `key` through the snapshot in | |
| /// [`GlobalContext`](super::GlobalContext). | |
| /// | |
| /// This can be used similarly to [`std::env::var_os`]. | |
| /// On Windows, we check for case mismatch since environment keys are case-insensitive. | |
| pub fn get_env_os(&self, key: impl AsRef<OsStr>) -> Option<&OsStr> { | |
| #[cfg(windows)] | |
| match self.env.get(key.as_ref()) { | |
| Some(s) => Some(s), | |
| #[cfg(windows)] | |
| None => self.get_env_case_insensitive(key), | |
| } | |
| #[cfg(not(windows))] | |
| self.env.get(key.as_ref()).map(|s| s.as_os_str()) | |
| } | |
| /// Get the value of environment variable `key` through the `self.env` snapshot. | |
| /// | |
| /// This can be used similarly to `std::env::var`. | |
| /// On Windows, we check for case mismatch since environment keys are case-insensitive. | |
| pub fn get_env(&self, key: impl AsRef<OsStr>) -> CargoResult<&str> { | |
| let key = key.as_ref(); | |
| let s = self | |
| .get_env_os(key) | |
| .ok_or_else(|| anyhow!("{key:?} could not be found in the environment snapshot"))?; | |
| match s.to_str() { | |
| Some(s) => Ok(s), | |
| None => bail!("environment variable value is not valid unicode: {s:?}"), | |
| } | |
| } | |
| /// Performs a case-insensitive lookup of `key` in the environment. | |
| /// | |
| /// This is relevant on Windows, where environment variables are case-insensitive. | |
| /// Note that this only works on keys that are valid UTF-8 and it uses Unicode uppercase, | |
| /// which may differ from the OS's notion of uppercase. | |
| #[cfg(windows)] | |
| fn get_env_case_insensitive(&self, key: impl AsRef<OsStr>) -> Option<&OsStr> { | |
| let upper_case_key = key.as_ref().to_str()?.to_uppercase(); | |
| let env_key: &OsStr = self.case_insensitive_env.get(&upper_case_key)?.as_ref(); | |
| self.env.get(env_key).map(|v| v.as_ref()) | |
| } | |
| /// Get the value of environment variable `key` as a `&str`. | |
| /// Returns `None` if `key` is not in `self.env` or if the value is not valid UTF-8. | |
| /// | |
| /// This is intended for use in private methods of [`GlobalContext`](super::GlobalContext), | |
| /// and does not check for env key case mismatch. | |
| /// | |
| /// This is case-sensitive on Windows (even though environment keys on Windows are usually | |
| /// case-insensitive) due to an unintended regression in 1.28 (via #5552). | |
| /// This should only affect keys used for cargo's config-system env variables (`CARGO_` | |
| /// prefixed ones), which are currently all uppercase. | |
| /// We may want to consider rectifying it if users report issues. | |
| /// One thing that adds a wrinkle here is the unstable advanced-env option that *requires* | |
| /// case-sensitive keys. | |
| /// | |
| /// Do not use this for any other purposes. | |
| /// Use [`Env::get_env_os`] or [`Env::get_env`] instead, which properly handle case | |
| /// insensitivity on Windows. | |
| pub(super) fn get_str(&self, key: impl AsRef<OsStr>) -> Option<&str> { | |
| self.env.get(key.as_ref()).and_then(|s| s.to_str()) | |
| } | |
| /// Check if the environment contains `key`. | |
| /// | |
| /// This is intended for use in private methods of [`GlobalContext`](super::GlobalContext), | |
| /// and does not check for env key case mismatch. | |
| /// See the docstring of [`Env::get_str`] for more context. | |
| pub(super) fn contains_key(&self, key: impl AsRef<OsStr>) -> bool { | |
| self.env.contains_key(key.as_ref()) | |
| } | |
| /// Looks up a normalized `key` in the `normalized_env`. | |
| /// Returns the corresponding (non-normalized) env key if it exists, else `None`. | |
| /// | |
| /// This is used by [`super::GlobalContext::check_environment_key_case_mismatch`]. | |
| pub(super) fn get_normalized(&self, key: &str) -> Option<&str> { | |
| self.normalized_env.get(key).map(|s| s.as_ref()) | |
| } | |
| } | |
| /// Generate `case_insensitive_env` from the `env`. | |
| #[cfg(windows)] | |
| fn make_case_insensitive_env(env: &HashMap<OsString, OsString>) -> HashMap<String, String> { | |
| env.keys() | |
| .filter_map(|k| k.to_str()) | |
| .map(|k| (k.to_uppercase(), k.to_owned())) | |
| .collect() | |
| } | |
| /// Generate `normalized_env` from the `env`. | |
| fn make_normalized_env(env: &HashMap<OsString, OsString>) -> HashMap<String, String> { | |
| env.iter() | |
| // Only keep entries where both the key and value are valid UTF-8, | |
| // since the config env vars only support UTF-8 keys and values. | |
| // Otherwise, the normalized map warning could incorrectly warn about entries that can't be | |
| // read by the config system. | |
| // Please see the docs for `Env` for more context. | |
| .filter_map(|(k, v)| Some((k.to_str()?, v.to_str()?))) | |
| .map(|(k, _)| (k.to_uppercase().replace("-", "_"), k.to_owned())) | |
| .collect() | |
| } |