nextest_runner/cargo_config/
env.rs1use super::{CargoConfigSource, CargoConfigs, DiscoveredConfig};
5use camino::{Utf8Path, Utf8PathBuf};
6use std::{
7 collections::{BTreeMap, BTreeSet, btree_map::Entry},
8 ffi::OsString,
9 process::Command,
10};
11
12#[derive(Clone, Debug)]
14pub struct EnvironmentMap {
15 map: BTreeMap<imp::EnvKey, CargoEnvironmentVariable>,
16}
17
18impl EnvironmentMap {
19 pub fn new(configs: &CargoConfigs) -> Self {
21 let env_configs = configs
22 .discovered_configs()
23 .filter_map(|config| match config {
24 DiscoveredConfig::CliOption { config, source }
25 | DiscoveredConfig::File { config, source } => Some((config, source)),
26 DiscoveredConfig::Env => None,
27 })
28 .flat_map(|(config, source)| {
29 let source = match source {
30 CargoConfigSource::CliOption => None,
31 CargoConfigSource::File(path) => Some(path.clone()),
32 };
33 config
34 .env
35 .clone()
36 .into_iter()
37 .map(move |(name, value)| (source.clone(), name, value))
38 });
39
40 let mut map = BTreeMap::<imp::EnvKey, CargoEnvironmentVariable>::new();
41
42 for (source, name, value) in env_configs {
43 match map.entry(imp::EnvKey::from(name.clone())) {
44 Entry::Occupied(mut entry) => {
45 let var = entry.get_mut();
48 if var.force.is_none() && value.force().is_some() {
49 var.force = value.force();
50 }
51 if var.relative.is_none() && value.relative().is_some() {
52 var.relative = value.relative();
53 }
54 }
55 Entry::Vacant(entry) => {
56 let force = value.force();
57 let relative = value.relative();
58 let value = value.into_value();
59 entry.insert(CargoEnvironmentVariable {
60 source,
61 name,
62 value,
63 force,
64 relative,
65 });
66 }
67 }
68 }
69
70 Self { map }
71 }
72
73 #[cfg(test)]
74 pub(crate) fn empty() -> Self {
75 Self {
76 map: BTreeMap::new(),
77 }
78 }
79
80 pub(crate) fn apply_env(&self, command: &mut Command) {
81 #[cfg_attr(not(windows), expect(clippy::useless_conversion))]
82 let existing_keys: BTreeSet<imp::EnvKey> =
83 std::env::vars_os().map(|(k, _v)| k.into()).collect();
84
85 for (name, var) in &self.map {
86 let should_set_value = if existing_keys.contains(name) {
87 var.force.unwrap_or_default()
88 } else {
89 true
90 };
91 if !should_set_value {
92 continue;
93 }
94
95 let value = if var.relative.unwrap_or_default() {
96 let base_path = match &var.source {
97 Some(source_path) => source_path,
98 None => unreachable!(
99 "Cannot use a relative path for environment variable {name:?} \
100 whose source is not a config file (this should already have been checked)"
101 ),
102 };
103 relative_dir_for(base_path).map_or_else(
104 || var.value.clone(),
105 |rel_dir| rel_dir.join(&var.value).into_string(),
106 )
107 } else {
108 var.value.clone()
109 };
110
111 command.env(name, value);
112 }
113 }
114}
115
116#[derive(Clone, Debug, PartialEq, Eq)]
119pub struct CargoEnvironmentVariable {
120 pub source: Option<Utf8PathBuf>,
124
125 pub name: String,
127
128 pub value: String,
130
131 pub force: Option<bool>,
136
137 pub relative: Option<bool>,
142}
143
144pub fn relative_dir_for(config_path: &Utf8Path) -> Option<&Utf8Path> {
146 let relative_dir = config_path.parent()?.parent()?;
150
151 Some(imp::strip_unc_prefix(relative_dir))
153}
154
155#[cfg(windows)]
156mod imp {
157 use super::*;
158 use std::{borrow::Borrow, cmp, ffi::OsStr, os::windows::prelude::OsStrExt};
159 use windows_sys::Win32::Globalization::{
160 CSTR_EQUAL, CSTR_GREATER_THAN, CSTR_LESS_THAN, CompareStringOrdinal,
161 };
162
163 pub(super) fn strip_unc_prefix(path: &Utf8Path) -> &Utf8Path {
164 dunce::simplified(path.as_std_path())
165 .try_into()
166 .expect("stripping verbatim components from a UTF-8 path should result in a UTF-8 path")
167 }
168
169 #[derive(Clone, Debug, Eq)]
173 #[doc(hidden)]
174 pub(super) struct EnvKey {
175 os_string: OsString,
176 utf16: Vec<u16>,
181 }
182
183 impl Ord for EnvKey {
204 fn cmp(&self, other: &Self) -> cmp::Ordering {
205 unsafe {
206 let result = CompareStringOrdinal(
207 self.utf16.as_ptr(),
208 self.utf16.len() as _,
209 other.utf16.as_ptr(),
210 other.utf16.len() as _,
211 1, );
213 match result {
214 CSTR_LESS_THAN => cmp::Ordering::Less,
215 CSTR_EQUAL => cmp::Ordering::Equal,
216 CSTR_GREATER_THAN => cmp::Ordering::Greater,
217 _ => panic!(
219 "comparing environment keys failed: {}",
220 std::io::Error::last_os_error()
221 ),
222 }
223 }
224 }
225 }
226 impl PartialOrd for EnvKey {
227 fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
228 Some(self.cmp(other))
229 }
230 }
231 impl PartialEq for EnvKey {
232 fn eq(&self, other: &Self) -> bool {
233 if self.utf16.len() != other.utf16.len() {
234 false
235 } else {
236 self.cmp(other) == cmp::Ordering::Equal
237 }
238 }
239 }
240
241 impl From<OsString> for EnvKey {
244 fn from(k: OsString) -> Self {
245 EnvKey {
246 utf16: k.encode_wide().collect(),
247 os_string: k,
248 }
249 }
250 }
251
252 impl From<String> for EnvKey {
253 fn from(k: String) -> Self {
254 OsString::from(k).into()
255 }
256 }
257
258 impl From<EnvKey> for OsString {
259 fn from(k: EnvKey) -> Self {
260 k.os_string
261 }
262 }
263
264 impl Borrow<OsStr> for EnvKey {
265 fn borrow(&self) -> &OsStr {
266 &self.os_string
267 }
268 }
269
270 impl AsRef<OsStr> for EnvKey {
271 fn as_ref(&self) -> &OsStr {
272 &self.os_string
273 }
274 }
275}
276
277#[cfg(not(windows))]
278mod imp {
279 use super::*;
280
281 pub(super) fn strip_unc_prefix(path: &Utf8Path) -> &Utf8Path {
282 path
283 }
284
285 pub(super) type EnvKey = OsString;
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use crate::cargo_config::test_helpers::setup_temp_dir;
292 use std::ffi::OsStr;
293
294 #[test]
295 fn test_env_var_precedence() {
296 let dir = setup_temp_dir().unwrap();
297 let dir_path = Utf8PathBuf::try_from(dir.path().canonicalize().unwrap()).unwrap();
298 let dir_foo_path = dir_path.join("foo");
299 let dir_foo_bar_path = dir_foo_path.join("bar");
300
301 let configs = CargoConfigs::new_with_isolation(
302 &[] as &[&str],
303 &dir_foo_bar_path,
304 &dir_path,
305 Vec::new(),
306 )
307 .unwrap();
308 let env = EnvironmentMap::new(&configs);
309 let var = env
310 .map
311 .get(OsStr::new("SOME_VAR"))
312 .expect("SOME_VAR is specified in test config");
313 assert_eq!(var.value, "foo-bar-config");
314
315 let configs = CargoConfigs::new_with_isolation(
316 ["env.SOME_VAR=\"cli-config\""],
317 &dir_foo_bar_path,
318 &dir_path,
319 Vec::new(),
320 )
321 .unwrap();
322 let env = EnvironmentMap::new(&configs);
323 let var = env
324 .map
325 .get(OsStr::new("SOME_VAR"))
326 .expect("SOME_VAR is specified in test config");
327 assert_eq!(var.value, "cli-config");
328 }
329
330 #[test]
331 fn test_cli_env_var_relative() {
332 let dir = setup_temp_dir().unwrap();
333 let dir_path = Utf8PathBuf::try_from(dir.path().canonicalize().unwrap()).unwrap();
334 let dir_foo_path = dir_path.join("foo");
335 let dir_foo_bar_path = dir_foo_path.join("bar");
336
337 CargoConfigs::new_with_isolation(
338 ["env.SOME_VAR={value = \"path\", relative = true }"],
339 &dir_foo_bar_path,
340 &dir_path,
341 Vec::new(),
342 )
343 .expect_err("CLI configs can't be relative");
344
345 CargoConfigs::new_with_isolation(
346 ["env.SOME_VAR.value=\"path\"", "env.SOME_VAR.relative=true"],
347 &dir_foo_bar_path,
348 &dir_path,
349 Vec::new(),
350 )
351 .expect_err("CLI configs can't be relative");
352 }
353
354 #[test]
355 #[cfg(unix)]
356 fn test_relative_dir_for_unix() {
357 assert_eq!(
358 relative_dir_for("/foo/bar/.cargo/config.toml".as_ref()),
359 Some("/foo/bar".as_ref()),
360 );
361 assert_eq!(
362 relative_dir_for("/foo/bar/.cargo/config".as_ref()),
363 Some("/foo/bar".as_ref()),
364 );
365 assert_eq!(
366 relative_dir_for("/foo/bar/config".as_ref()),
367 Some("/foo".as_ref())
368 );
369 assert_eq!(relative_dir_for("/foo/config".as_ref()), Some("/".as_ref()));
370 assert_eq!(relative_dir_for("/config.toml".as_ref()), None);
371 }
372
373 #[test]
374 #[cfg(windows)]
375 fn test_relative_dir_for_windows() {
376 assert_eq!(
377 relative_dir_for("C:\\foo\\bar\\.cargo\\config.toml".as_ref()),
378 Some("C:\\foo\\bar".as_ref()),
379 );
380 assert_eq!(
381 relative_dir_for("C:\\foo\\bar\\.cargo\\config".as_ref()),
382 Some("C:\\foo\\bar".as_ref()),
383 );
384 assert_eq!(
385 relative_dir_for("C:\\foo\\bar\\config".as_ref()),
386 Some("C:\\foo".as_ref())
387 );
388 assert_eq!(
389 relative_dir_for("C:\\foo\\config".as_ref()),
390 Some("C:\\".as_ref())
391 );
392 assert_eq!(relative_dir_for("C:\\config.toml".as_ref()), None);
393 }
394}