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 pub fn empty() -> Self {
78 Self {
79 map: BTreeMap::new(),
80 }
81 }
82
83 pub(crate) fn apply_env(&self, command: &mut Command) {
84 #[cfg_attr(not(windows), expect(clippy::useless_conversion))]
85 let existing_keys: BTreeSet<imp::EnvKey> =
86 std::env::vars_os().map(|(k, _v)| k.into()).collect();
87
88 for (name, var) in &self.map {
89 let should_set_value = if existing_keys.contains(name) {
90 var.force.unwrap_or_default()
91 } else {
92 true
93 };
94 if !should_set_value {
95 continue;
96 }
97
98 let value = if var.relative.unwrap_or_default() {
99 let base_path = match &var.source {
100 Some(source_path) => source_path,
101 None => unreachable!(
102 "Cannot use a relative path for environment variable {name:?} \
103 whose source is not a config file (this should already have been checked)"
104 ),
105 };
106 relative_dir_for(base_path).map_or_else(
107 || var.value.clone(),
108 |rel_dir| rel_dir.join(&var.value).into_string(),
109 )
110 } else {
111 var.value.clone()
112 };
113
114 command.env(name, value);
115 }
116 }
117}
118
119#[derive(Clone, Debug, PartialEq, Eq)]
122pub struct CargoEnvironmentVariable {
123 pub source: Option<Utf8PathBuf>,
127
128 pub name: String,
130
131 pub value: String,
133
134 pub force: Option<bool>,
139
140 pub relative: Option<bool>,
145}
146
147pub fn relative_dir_for(config_path: &Utf8Path) -> Option<&Utf8Path> {
149 let relative_dir = config_path.parent()?.parent()?;
153
154 Some(imp::strip_unc_prefix(relative_dir))
156}
157
158#[cfg(windows)]
159mod imp {
160 use super::*;
161 use std::{borrow::Borrow, cmp, ffi::OsStr, os::windows::prelude::OsStrExt};
162 use windows_sys::Win32::Globalization::{
163 CSTR_EQUAL, CSTR_GREATER_THAN, CSTR_LESS_THAN, CompareStringOrdinal,
164 };
165
166 pub(super) fn strip_unc_prefix(path: &Utf8Path) -> &Utf8Path {
167 dunce::simplified(path.as_std_path())
168 .try_into()
169 .expect("stripping verbatim components from a UTF-8 path should result in a UTF-8 path")
170 }
171
172 #[derive(Clone, Debug, Eq)]
176 #[doc(hidden)]
177 pub(super) struct EnvKey {
178 os_string: OsString,
179 utf16: Vec<u16>,
184 }
185
186 impl Ord for EnvKey {
207 fn cmp(&self, other: &Self) -> cmp::Ordering {
208 unsafe {
209 let result = CompareStringOrdinal(
210 self.utf16.as_ptr(),
211 self.utf16.len() as _,
212 other.utf16.as_ptr(),
213 other.utf16.len() as _,
214 1, );
216 match result {
217 CSTR_LESS_THAN => cmp::Ordering::Less,
218 CSTR_EQUAL => cmp::Ordering::Equal,
219 CSTR_GREATER_THAN => cmp::Ordering::Greater,
220 _ => panic!(
222 "comparing environment keys failed: {}",
223 std::io::Error::last_os_error()
224 ),
225 }
226 }
227 }
228 }
229 impl PartialOrd for EnvKey {
230 fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
231 Some(self.cmp(other))
232 }
233 }
234 impl PartialEq for EnvKey {
235 fn eq(&self, other: &Self) -> bool {
236 if self.utf16.len() != other.utf16.len() {
237 false
238 } else {
239 self.cmp(other) == cmp::Ordering::Equal
240 }
241 }
242 }
243
244 impl From<OsString> for EnvKey {
247 fn from(k: OsString) -> Self {
248 EnvKey {
249 utf16: k.encode_wide().collect(),
250 os_string: k,
251 }
252 }
253 }
254
255 impl From<String> for EnvKey {
256 fn from(k: String) -> Self {
257 OsString::from(k).into()
258 }
259 }
260
261 impl From<EnvKey> for OsString {
262 fn from(k: EnvKey) -> Self {
263 k.os_string
264 }
265 }
266
267 impl Borrow<OsStr> for EnvKey {
268 fn borrow(&self) -> &OsStr {
269 &self.os_string
270 }
271 }
272
273 impl AsRef<OsStr> for EnvKey {
274 fn as_ref(&self) -> &OsStr {
275 &self.os_string
276 }
277 }
278}
279
280#[cfg(not(windows))]
281mod imp {
282 use super::*;
283
284 pub(super) fn strip_unc_prefix(path: &Utf8Path) -> &Utf8Path {
285 path
286 }
287
288 pub(super) type EnvKey = OsString;
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294 use crate::cargo_config::test_helpers::setup_temp_dir;
295 use std::ffi::OsStr;
296
297 #[test]
298 fn test_env_var_precedence() {
299 let dir = setup_temp_dir().unwrap();
300 let dir_path = Utf8PathBuf::try_from(dir.path().canonicalize().unwrap()).unwrap();
301 let dir_foo_path = dir_path.join("foo");
302 let dir_foo_bar_path = dir_foo_path.join("bar");
303
304 let configs = CargoConfigs::new_with_isolation(
305 &[] as &[&str],
306 &dir_foo_bar_path,
307 &dir_path,
308 Vec::new(),
309 )
310 .unwrap();
311 let env = EnvironmentMap::new(&configs);
312 let var = env
313 .map
314 .get(OsStr::new("SOME_VAR"))
315 .expect("SOME_VAR is specified in test config");
316 assert_eq!(var.value, "foo-bar-config");
317
318 let configs = CargoConfigs::new_with_isolation(
319 ["env.SOME_VAR=\"cli-config\""],
320 &dir_foo_bar_path,
321 &dir_path,
322 Vec::new(),
323 )
324 .unwrap();
325 let env = EnvironmentMap::new(&configs);
326 let var = env
327 .map
328 .get(OsStr::new("SOME_VAR"))
329 .expect("SOME_VAR is specified in test config");
330 assert_eq!(var.value, "cli-config");
331 }
332
333 #[test]
334 fn test_cli_env_var_relative() {
335 let dir = setup_temp_dir().unwrap();
336 let dir_path = Utf8PathBuf::try_from(dir.path().canonicalize().unwrap()).unwrap();
337 let dir_foo_path = dir_path.join("foo");
338 let dir_foo_bar_path = dir_foo_path.join("bar");
339
340 CargoConfigs::new_with_isolation(
341 ["env.SOME_VAR={value = \"path\", relative = true }"],
342 &dir_foo_bar_path,
343 &dir_path,
344 Vec::new(),
345 )
346 .expect_err("CLI configs can't be relative");
347
348 CargoConfigs::new_with_isolation(
349 ["env.SOME_VAR.value=\"path\"", "env.SOME_VAR.relative=true"],
350 &dir_foo_bar_path,
351 &dir_path,
352 Vec::new(),
353 )
354 .expect_err("CLI configs can't be relative");
355 }
356
357 #[test]
358 #[cfg(unix)]
359 fn test_relative_dir_for_unix() {
360 assert_eq!(
361 relative_dir_for("/foo/bar/.cargo/config.toml".as_ref()),
362 Some("/foo/bar".as_ref()),
363 );
364 assert_eq!(
365 relative_dir_for("/foo/bar/.cargo/config".as_ref()),
366 Some("/foo/bar".as_ref()),
367 );
368 assert_eq!(
369 relative_dir_for("/foo/bar/config".as_ref()),
370 Some("/foo".as_ref())
371 );
372 assert_eq!(relative_dir_for("/foo/config".as_ref()), Some("/".as_ref()));
373 assert_eq!(relative_dir_for("/config.toml".as_ref()), None);
374 }
375
376 #[test]
377 #[cfg(windows)]
378 fn test_relative_dir_for_windows() {
379 assert_eq!(
380 relative_dir_for("C:\\foo\\bar\\.cargo\\config.toml".as_ref()),
381 Some("C:\\foo\\bar".as_ref()),
382 );
383 assert_eq!(
384 relative_dir_for("C:\\foo\\bar\\.cargo\\config".as_ref()),
385 Some("C:\\foo\\bar".as_ref()),
386 );
387 assert_eq!(
388 relative_dir_for("C:\\foo\\bar\\config".as_ref()),
389 Some("C:\\foo".as_ref())
390 );
391 assert_eq!(
392 relative_dir_for("C:\\foo\\config".as_ref()),
393 Some("C:\\".as_ref())
394 );
395 assert_eq!(relative_dir_for("C:\\config.toml".as_ref()), None);
396 }
397}