cargo_config2/
resolve.rs

1// SPDX-License-Identifier: Apache-2.0 OR MIT
2
3use core::{
4    cell::{OnceCell, RefCell},
5    cmp,
6    hash::Hash,
7    iter,
8    str::FromStr,
9};
10use std::{
11    borrow::Cow,
12    collections::{HashMap, HashSet},
13    ffi::{OsStr, OsString},
14    path::{Path, PathBuf},
15};
16
17use serde::{
18    de::{Deserialize, Deserializer},
19    ser::{Serialize, Serializer},
20};
21use serde_derive::{Deserialize, Serialize};
22
23use crate::{
24    PathAndArgs,
25    cfg_expr::expr::{Expression, Predicate},
26    easy,
27    error::{Context as _, Error, Result},
28    process::ProcessBuilder,
29    value::{Definition, Value},
30    walk,
31};
32
33#[derive(Debug, Clone, Default)]
34#[must_use]
35pub struct ResolveOptions {
36    env: Option<HashMap<String, OsString>>,
37    rustc: Option<PathAndArgs>,
38    cargo: Option<OsString>,
39    #[allow(clippy::option_option)]
40    cargo_home: Option<Option<PathBuf>>,
41    host_triple: Option<String>,
42}
43
44impl ResolveOptions {
45    /// Sets `rustc` path and args.
46    ///
47    /// # Default value
48    ///
49    /// [`Config::rustc`](crate::Config::rustc)
50    pub fn rustc<P: Into<PathAndArgs>>(mut self, rustc: P) -> Self {
51        self.rustc = Some(rustc.into());
52        self
53    }
54    /// Sets `cargo` path.
55    ///
56    /// # Default value
57    ///
58    /// The value of the `CARGO` environment variable if it is set. Otherwise, "cargo".
59    pub fn cargo<S: Into<OsString>>(mut self, cargo: S) -> Self {
60        self.cargo = Some(cargo.into());
61        self
62    }
63    /// Sets `CARGO_HOME` path.
64    ///
65    /// # Default value
66    ///
67    /// [`home::cargo_home_with_cwd`] if the current directory was specified when
68    /// loading config. Otherwise, [`home::cargo_home`].
69    ///
70    /// [`home::cargo_home_with_cwd`]: https://docs.rs/home/latest/home/fn.cargo_home_with_cwd.html
71    /// [`home::cargo_home`]: https://docs.rs/home/latest/home/fn.cargo_home.html
72    pub fn cargo_home<P: Into<Option<PathBuf>>>(mut self, cargo_home: P) -> Self {
73        self.cargo_home = Some(cargo_home.into());
74        self
75    }
76    /// Sets host target triple.
77    ///
78    /// # Default value
79    ///
80    /// Parse the version output of `cargo` specified by [`Self::cargo`].
81    pub fn host_triple<S: Into<String>>(mut self, triple: S) -> Self {
82        self.host_triple = Some(triple.into());
83        self
84    }
85    /// Sets the specified key-values as environment variables to be read during
86    /// config resolution.
87    ///
88    /// This is mainly intended for use in tests where it is necessary to adjust
89    /// the kinds of environment variables that are referenced.
90    ///
91    /// # Default value
92    ///
93    /// [`std::env::vars_os`]
94    pub fn env<I: IntoIterator<Item = (K, V)>, K: Into<OsString>, V: Into<OsString>>(
95        mut self,
96        vars: I,
97    ) -> Self {
98        let mut env = HashMap::default();
99        for (k, v) in vars {
100            if let Ok(k) = k.into().into_string() {
101                if k.starts_with("CARGO") || k.starts_with("RUST") || k == "BROWSER" {
102                    env.insert(k, v.into());
103                }
104            }
105        }
106        self.env = Some(env);
107        self
108    }
109
110    #[doc(hidden)] // Not public API.
111    pub fn into_context(mut self, current_dir: PathBuf) -> ResolveContext {
112        if self.env.is_none() {
113            self = self.env(std::env::vars_os());
114        }
115        let env = self.env.unwrap();
116        let rustc = match self.rustc {
117            Some(rustc) => OnceCell::from(rustc),
118            None => OnceCell::new(),
119        };
120        let cargo = match self.cargo {
121            Some(cargo) => cargo,
122            None => env.get("CARGO").cloned().unwrap_or_else(|| "cargo".into()),
123        };
124        let cargo_home = match self.cargo_home {
125            Some(cargo_home) => OnceCell::from(cargo_home),
126            None => OnceCell::new(),
127        };
128        let host_triple = match self.host_triple {
129            Some(host_triple) => OnceCell::from(host_triple),
130            None => OnceCell::new(),
131        };
132
133        ResolveContext {
134            env,
135            rustc,
136            cargo,
137            cargo_home,
138            host_triple,
139            rustc_version: OnceCell::new(),
140            cargo_version: OnceCell::new(),
141            cfg: RefCell::default(),
142            current_dir,
143        }
144    }
145}
146
147#[doc(hidden)] // Not public API.
148#[allow(unknown_lints, unnameable_types)] // Not public API. unnameable_types is available on Rust 1.79+
149#[derive(Debug, Clone)]
150#[must_use]
151pub struct ResolveContext {
152    pub(crate) env: HashMap<String, OsString>,
153    rustc: OnceCell<easy::PathAndArgs>,
154    pub(crate) cargo: OsString,
155    cargo_home: OnceCell<Option<PathBuf>>,
156    host_triple: OnceCell<String>,
157    rustc_version: OnceCell<RustcVersion>,
158    cargo_version: OnceCell<CargoVersion>,
159    cfg: RefCell<CfgMap>,
160    pub(crate) current_dir: PathBuf,
161}
162
163impl ResolveContext {
164    pub(crate) fn rustc(&self, build_config: &easy::BuildConfig) -> &PathAndArgs {
165        self.rustc.get_or_init(|| {
166            // https://github.com/rust-lang/cargo/pull/10896
167            // https://github.com/rust-lang/cargo/pull/13648
168            let rustc =
169                build_config.rustc.as_ref().map_or_else(|| rustc_path(&self.cargo), PathBuf::from);
170            let rustc_wrapper = build_config.rustc_wrapper.clone();
171            let rustc_workspace_wrapper = build_config.rustc_workspace_wrapper.clone();
172            let mut rustc =
173                rustc_wrapper.into_iter().chain(rustc_workspace_wrapper).chain(iter::once(rustc));
174            PathAndArgs {
175                path: rustc.next().unwrap(),
176                args: rustc.map(PathBuf::into_os_string).collect(),
177            }
178        })
179    }
180    pub(crate) fn rustc_for_version(&self, build_config: &easy::BuildConfig) -> PathAndArgs {
181        // Do not apply RUSTC_WORKSPACE_WRAPPER: https://github.com/cuviper/autocfg/issues/58#issuecomment-2067625980
182        let rustc =
183            build_config.rustc.as_ref().map_or_else(|| rustc_path(&self.cargo), PathBuf::from);
184        let rustc_wrapper = build_config.rustc_wrapper.clone();
185        let mut rustc = rustc_wrapper.into_iter().chain(iter::once(rustc));
186        PathAndArgs {
187            path: rustc.next().unwrap(),
188            args: rustc.map(PathBuf::into_os_string).collect(),
189        }
190    }
191    pub(crate) fn cargo_home(&self, cwd: &Path) -> Option<&Path> {
192        self.cargo_home.get_or_init(|| walk::cargo_home_with_cwd(cwd)).as_deref()
193    }
194    pub(crate) fn host_triple(&self, build_config: &easy::BuildConfig) -> Result<&str> {
195        if let Some(host) = self.host_triple.get() {
196            return Ok(host);
197        }
198        let cargo_host = verbose_version(cmd!(&self.cargo)).and_then(|ref vv| {
199            let r = self.cargo_version.set(cargo_version(vv)?);
200            debug_assert!(r.is_ok());
201            host_triple(vv)
202        });
203        let host = match cargo_host {
204            Ok(host) => host,
205            Err(_) => {
206                let vv = &verbose_version((&self.rustc_for_version(build_config)).into())?;
207                let r = self.rustc_version.set(rustc_version(vv)?);
208                debug_assert!(r.is_ok());
209                host_triple(vv)?
210            }
211        };
212        Ok(self.host_triple.get_or_init(|| host))
213    }
214    pub(crate) fn rustc_version(&self, build_config: &easy::BuildConfig) -> Result<RustcVersion> {
215        if let Some(&rustc_version) = self.rustc_version.get() {
216            return Ok(rustc_version);
217        }
218        let _ = self.host_triple(build_config);
219        if let Some(&rustc_version) = self.rustc_version.get() {
220            return Ok(rustc_version);
221        }
222        let vv = &verbose_version((&self.rustc_for_version(build_config)).into())?;
223        let rustc_version = rustc_version(vv)?;
224        Ok(*self.rustc_version.get_or_init(|| rustc_version))
225    }
226    pub(crate) fn cargo_version(&self, build_config: &easy::BuildConfig) -> Result<CargoVersion> {
227        if let Some(&cargo_version) = self.cargo_version.get() {
228            return Ok(cargo_version);
229        }
230        let _ = self.host_triple(build_config);
231        if let Some(&cargo_version) = self.cargo_version.get() {
232            return Ok(cargo_version);
233        }
234        let vv = &verbose_version(cmd!(&self.cargo))?;
235        let cargo_version = cargo_version(vv)?;
236        Ok(*self.cargo_version.get_or_init(|| cargo_version))
237    }
238
239    // micro-optimization for static name -- avoiding name allocation can speed up
240    // de::Config::apply_env by up to 40% because most env var names we fetch are static.
241    pub(crate) fn env(&self, name: &'static str) -> Result<Option<Value<String>>> {
242        match self.env.get(name) {
243            None => Ok(None),
244            Some(v) => Ok(Some(Value {
245                val: v.clone().into_string().map_err(|var| Error::env_not_unicode(name, var))?,
246                definition: Some(Definition::Environment(name.into())),
247            })),
248        }
249    }
250    pub(crate) fn env_redacted(&self, name: &'static str) -> Result<Option<Value<String>>> {
251        match self.env.get(name) {
252            None => Ok(None),
253            Some(v) => Ok(Some(Value {
254                val: v
255                    .clone()
256                    .into_string()
257                    .map_err(|_var| Error::env_not_unicode_redacted(name))?,
258                definition: Some(Definition::Environment(name.into())),
259            })),
260        }
261    }
262    pub(crate) fn env_parse<T>(&self, name: &'static str) -> Result<Option<Value<T>>>
263    where
264        T: FromStr,
265        T::Err: std::error::Error + Send + Sync + 'static,
266    {
267        match self.env(name)? {
268            Some(v) => Ok(Some(
269                v.parse()
270                    .with_context(|| format!("failed to parse environment variable `{name}`"))?,
271            )),
272            None => Ok(None),
273        }
274    }
275    pub(crate) fn env_dyn(&self, name: &str) -> Result<Option<Value<String>>> {
276        match self.env.get(name) {
277            None => Ok(None),
278            Some(v) => Ok(Some(Value {
279                val: v.clone().into_string().map_err(|var| Error::env_not_unicode(name, var))?,
280                definition: Some(Definition::Environment(name.to_owned().into())),
281            })),
282        }
283    }
284
285    pub(crate) fn eval_cfg(
286        &self,
287        expr: &str,
288        target: &TargetTripleRef<'_>,
289        build_config: &easy::BuildConfig,
290    ) -> Result<bool> {
291        let expr = Expression::parse(expr).map_err(Error::new)?;
292        let mut cfg_map = self.cfg.borrow_mut();
293        cfg_map.eval_cfg(&expr, target, || self.rustc(build_config).into())
294    }
295}
296
297#[derive(Debug, Clone, Default)]
298pub(crate) struct CfgMap {
299    map: HashMap<TargetTripleBorrow<'static>, Cfg>,
300}
301
302impl CfgMap {
303    pub(crate) fn eval_cfg(
304        &mut self,
305        expr: &Expression,
306        target: &TargetTripleRef<'_>,
307        rustc: impl FnOnce() -> ProcessBuilder,
308    ) -> Result<bool> {
309        let cfg = match self.map.get(target.cli_target()) {
310            Some(cfg) => cfg,
311            None => {
312                let cfg = Cfg::from_rustc(rustc(), target)?;
313                self.map.insert(TargetTripleBorrow(target.clone().into_owned()), cfg);
314                &self.map[target.cli_target()]
315            }
316        };
317        Ok(expr.eval(|pred| match pred {
318            Predicate::Flag(flag) => {
319                match *flag {
320                    // https://github.com/rust-lang/cargo/pull/7660
321                    "test" | "debug_assertions" | "proc_macro" => false,
322                    flag => cfg.flags.contains(flag),
323                }
324            }
325            Predicate::KeyValue { key, val } => {
326                match *key {
327                    // https://github.com/rust-lang/cargo/pull/7660
328                    "feature" => false,
329                    key => cfg.key_values.get(key).is_some_and(|values| values.contains(*val)),
330                }
331            }
332        }))
333    }
334}
335
336#[derive(Debug, Clone)]
337struct Cfg {
338    flags: HashSet<String>,
339    key_values: HashMap<String, HashSet<String>>,
340}
341
342impl Cfg {
343    fn from_rustc(mut rustc: ProcessBuilder, target: &TargetTripleRef<'_>) -> Result<Self> {
344        let list =
345            rustc.args(["--print", "cfg", "--target", &*target.cli_target_string()]).read()?;
346        Ok(Self::parse(&list))
347    }
348
349    fn parse(list: &str) -> Self {
350        let mut flags = HashSet::default();
351        let mut key_values = HashMap::<String, HashSet<String>>::default();
352
353        for line in list.lines() {
354            let line = line.trim();
355            if line.is_empty() {
356                continue;
357            }
358            match line.split_once('=') {
359                None => {
360                    flags.insert(line.to_owned());
361                }
362                Some((name, value)) => {
363                    if value.len() < 2 || !value.starts_with('"') || !value.ends_with('"') {
364                        if cfg!(test) {
365                            panic!("invalid value '{value}'");
366                        }
367                        continue;
368                    }
369                    let value = &value[1..value.len() - 1];
370                    if value.is_empty() {
371                        continue;
372                    }
373                    if let Some(values) = key_values.get_mut(name) {
374                        values.insert(value.to_owned());
375                    } else {
376                        let mut values = HashSet::default();
377                        values.insert(value.to_owned());
378                        key_values.insert(name.to_owned(), values);
379                    }
380                }
381            }
382        }
383
384        Self { flags, key_values }
385    }
386}
387
388#[derive(Debug, Clone)]
389pub struct TargetTripleRef<'a> {
390    triple: Cow<'a, str>,
391    spec_path: Option<Cow<'a, Path>>,
392}
393
394pub type TargetTriple = TargetTripleRef<'static>;
395
396impl PartialEq for TargetTripleRef<'_> {
397    fn eq(&self, other: &Self) -> bool {
398        self.cli_target() == other.cli_target()
399    }
400}
401impl Eq for TargetTripleRef<'_> {}
402impl PartialOrd for TargetTripleRef<'_> {
403    fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
404        Some(self.cmp(other))
405    }
406}
407impl Ord for TargetTripleRef<'_> {
408    fn cmp(&self, other: &Self) -> cmp::Ordering {
409        self.cli_target().cmp(other.cli_target())
410    }
411}
412impl Hash for TargetTripleRef<'_> {
413    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
414        self.cli_target().hash(state);
415    }
416}
417
418// This wrapper is needed to support pre-1.63 Rust.
419// In pre-1.63 Rust you can't use TargetTripleRef<'non_static> as an index of
420// HashMap<TargetTripleRef<'static>, _> without this trick.
421#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
422#[serde(transparent)]
423pub(crate) struct TargetTripleBorrow<'a>(pub(crate) TargetTripleRef<'a>);
424impl core::borrow::Borrow<OsStr> for TargetTripleBorrow<'_> {
425    fn borrow(&self) -> &OsStr {
426        self.0.cli_target()
427    }
428}
429
430fn is_spec_path(triple_or_spec_path: &str) -> bool {
431    Path::new(triple_or_spec_path).extension() == Some(OsStr::new("json"))
432        || triple_or_spec_path.contains('/')
433        || triple_or_spec_path.contains('\\')
434}
435fn resolve_spec_path(
436    spec_path: &str,
437    def: Option<&Definition>,
438    current_dir: Option<&Path>,
439) -> Option<PathBuf> {
440    if let Some(def) = def {
441        if let Some(root) = def.root_opt(current_dir) {
442            return Some(root.join(spec_path));
443        }
444    }
445    None
446}
447
448impl<'a> TargetTripleRef<'a> {
449    pub(crate) fn new(
450        triple_or_spec_path: Cow<'a, str>,
451        def: Option<&Definition>,
452        current_dir: Option<&Path>,
453    ) -> Self {
454        // Handles custom target
455        if is_spec_path(&triple_or_spec_path) {
456            let triple = match &triple_or_spec_path {
457                // `triple_or_spec_path` is valid UTF-8, so unwrap here will never panic.
458                &Cow::Borrowed(v) => Path::new(v).file_stem().unwrap().to_str().unwrap().into(),
459                Cow::Owned(v) => {
460                    Path::new(v).file_stem().unwrap().to_str().unwrap().to_owned().into()
461                }
462            };
463            Self {
464                triple,
465                spec_path: Some(match resolve_spec_path(&triple_or_spec_path, def, current_dir) {
466                    Some(v) => v.into(),
467                    None => match triple_or_spec_path {
468                        Cow::Borrowed(v) => Path::new(v).into(),
469                        Cow::Owned(v) => PathBuf::from(v).into(),
470                    },
471                }),
472            }
473        } else {
474            Self { triple: triple_or_spec_path, spec_path: None }
475        }
476    }
477
478    pub fn into_owned(self) -> TargetTriple {
479        TargetTripleRef {
480            triple: self.triple.into_owned().into(),
481            spec_path: self.spec_path.map(|v| v.into_owned().into()),
482        }
483    }
484
485    pub fn triple(&self) -> &str {
486        &self.triple
487    }
488    pub fn spec_path(&self) -> Option<&Path> {
489        self.spec_path.as_deref()
490    }
491    pub(crate) fn cli_target_string(&self) -> Cow<'_, str> {
492        // Cargo converts spec path containing non-UTF8 byte to string with
493        // to_string_lossy before passing it to rustc.
494        // This is not good behavior but we just follow the behavior of cargo for now.
495        //
496        // ```
497        // $ pwd
498        // /tmp/��/a
499        // $ cat .cargo/config.toml
500        // [build]
501        // target = "avr-unknown-gnu-atmega2560.json"
502        // ```
503        // $ cargo build
504        // error: target path "/tmp/��/a/avr-unknown-gnu-atmega2560.json" is not a valid file
505        //
506        // Caused by:
507        //   No such file or directory (os error 2)
508        // ```
509        self.cli_target().to_string_lossy()
510    }
511    pub(crate) fn cli_target(&self) -> &OsStr {
512        match self.spec_path() {
513            Some(v) => v.as_os_str(),
514            None => OsStr::new(self.triple()),
515        }
516    }
517}
518
519impl<'a> From<&'a TargetTripleRef<'_>> for TargetTripleRef<'a> {
520    fn from(value: &'a TargetTripleRef<'_>) -> Self {
521        TargetTripleRef {
522            triple: value.triple().into(),
523            spec_path: value.spec_path().map(Into::into),
524        }
525    }
526}
527impl From<String> for TargetTripleRef<'static> {
528    fn from(value: String) -> Self {
529        Self::new(value.into(), None, None)
530    }
531}
532impl<'a> From<&'a String> for TargetTripleRef<'a> {
533    fn from(value: &'a String) -> Self {
534        Self::new(value.into(), None, None)
535    }
536}
537impl<'a> From<&'a str> for TargetTripleRef<'a> {
538    fn from(value: &'a str) -> Self {
539        Self::new(value.into(), None, None)
540    }
541}
542
543impl Serialize for TargetTripleRef<'_> {
544    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
545    where
546        S: Serializer,
547    {
548        self.cli_target_string().serialize(serializer)
549    }
550}
551impl<'de> Deserialize<'de> for TargetTripleRef<'static> {
552    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
553    where
554        D: Deserializer<'de>,
555    {
556        Ok(Self::new(String::deserialize(deserializer)?.into(), None, None))
557    }
558}
559
560#[derive(Debug, Clone, Copy, PartialEq, Eq)]
561#[non_exhaustive]
562pub struct RustcVersion {
563    pub major: u32,
564    pub minor: u32,
565    pub patch: Option<u32>,
566    pub nightly: bool,
567}
568#[derive(Debug, Clone, Copy, PartialEq, Eq)]
569#[non_exhaustive]
570pub struct CargoVersion {
571    pub major: u32,
572    pub minor: u32,
573    pub patch: u32,
574    pub nightly: bool,
575}
576
577impl RustcVersion {
578    /// Returns the pair of the major and minor versions.
579    ///
580    /// This is useful for comparing versions: `version.major_minor() < (1, 70)`
581    pub fn major_minor(&self) -> (u32, u32) {
582        (self.major, self.minor)
583    }
584}
585impl CargoVersion {
586    /// Returns the pair of the major and minor versions.
587    ///
588    /// This is useful for comparing versions: `version.major_minor() < (1, 70)`
589    pub fn major_minor(&self) -> (u32, u32) {
590        (self.major, self.minor)
591    }
592}
593
594fn verbose_version(mut rustc_or_cargo: ProcessBuilder) -> Result<(String, ProcessBuilder)> {
595    // Use verbose version output because the packagers add extra strings to the normal version output.
596    // Do not use long flags (--version --verbose) because clippy-deriver doesn't handle them properly.
597    // -vV is also matched with that cargo internally uses: https://github.com/rust-lang/cargo/blob/0.80.0/src/cargo/util/rustc.rs#L65
598    rustc_or_cargo.arg("-vV");
599    let verbose_version = rustc_or_cargo.read()?;
600    Ok((verbose_version, rustc_or_cargo))
601}
602
603fn parse_version(verbose_version: &str) -> Option<(u32, u32, Option<u32>, bool)> {
604    let release = verbose_version.lines().find_map(|line| line.strip_prefix("release: "))?;
605    let (version, channel) = release.split_once('-').unwrap_or((release, ""));
606    let mut digits = version.splitn(3, '.');
607    let major = digits.next()?.parse::<u32>().ok()?;
608    let minor = digits.next()?.parse::<u32>().ok()?;
609    let patch = match digits.next() {
610        Some(p) => Some(p.parse::<u32>().ok()?),
611        None => None,
612    };
613    let nightly = channel == "nightly" || channel == "dev";
614    Some((major, minor, patch, nightly))
615}
616
617fn rustc_version((verbose_version, cmd): &(String, ProcessBuilder)) -> Result<RustcVersion> {
618    let (major, minor, patch, nightly) = parse_version(verbose_version)
619        .ok_or_else(|| format_err!("unexpected version output from {cmd}: {verbose_version}"))?;
620    let nightly = match std::env::var_os("RUSTC_BOOTSTRAP") {
621        // When -1 is passed rustc works like stable, e.g., cfg(target_feature = "unstable_target_feature") will never be set. https://github.com/rust-lang/rust/pull/132993
622        Some(v) if v == "-1" => false,
623        _ => nightly,
624    };
625    Ok(RustcVersion { major, minor, patch, nightly })
626}
627fn cargo_version((verbose_version, cmd): &(String, ProcessBuilder)) -> Result<CargoVersion> {
628    let (major, minor, patch, nightly) = parse_version(verbose_version)
629        .and_then(|(major, minor, patch, nightly)| Some((major, minor, patch?, nightly)))
630        .ok_or_else(|| format_err!("unexpected version output from {cmd}: {verbose_version}"))?;
631    Ok(CargoVersion { major, minor, patch, nightly })
632}
633
634/// Gets host triple of the given `rustc` or `cargo`.
635fn host_triple((verbose_version, cmd): &(String, ProcessBuilder)) -> Result<String> {
636    let host = verbose_version
637        .lines()
638        .find_map(|line| line.strip_prefix("host: "))
639        .ok_or_else(|| format_err!("unexpected version output from {cmd}: {verbose_version}"))?
640        .to_owned();
641    Ok(host)
642}
643
644fn rustc_path(cargo: &OsStr) -> PathBuf {
645    // When toolchain override shorthand (`+toolchain`) is used, `rustc` in
646    // PATH and `CARGO` environment variable may be different toolchains.
647    // When Rust was installed using rustup, the same toolchain's rustc
648    // binary is in the same directory as the cargo binary, so we use it.
649    let mut rustc = PathBuf::from(cargo);
650    rustc.pop(); // cargo
651    rustc.push(format!("rustc{}", std::env::consts::EXE_SUFFIX));
652    if rustc.exists() { rustc } else { "rustc".into() }
653}
654
655#[allow(clippy::std_instead_of_alloc, clippy::std_instead_of_core)]
656#[cfg(test)]
657mod tests {
658    use std::{
659        fmt::Write as _,
660        io::{self, Write as _},
661    };
662
663    use fs_err as fs;
664
665    use super::*;
666
667    fn fixtures_dir() -> &'static Path {
668        Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures"))
669    }
670
671    #[test]
672    #[cfg_attr(miri, ignore)] // Miri doesn't support pipe2 (inside std::process::Command::output)
673    fn version_and_host() {
674        let rustc_vv = &verbose_version(cmd!("rustc")).unwrap();
675        let cargo_vv = &verbose_version(cmd!("cargo")).unwrap();
676        let rustc_version = rustc_version(rustc_vv).unwrap();
677        let cargo_version = cargo_version(cargo_vv).unwrap();
678        {
679            let mut out = String::new();
680            let _ = writeln!(out, "rustc version: {rustc_version:?}");
681            let _ = writeln!(out, "rustc host: {:?}", host_triple(rustc_vv).unwrap());
682            let _ = writeln!(out, "cargo version: {cargo_version:?}");
683            let _ = writeln!(out, "cargo host: {:?}", host_triple(cargo_vv).unwrap());
684            let mut stderr = io::stderr().lock(); // Not buffered because it is written at once.
685            let _ = stderr.write_all(out.as_bytes());
686            let _ = stderr.flush();
687        }
688
689        assert_eq!(rustc_version.major_minor(), (rustc_version.major, rustc_version.minor));
690        assert!(rustc_version.major_minor() < (2, 0));
691        assert!(rustc_version.major_minor() < (1, u32::MAX));
692        assert!(rustc_version.major_minor() >= (1, 70));
693        assert!(rustc_version.major_minor() > (1, 0));
694        assert!(rustc_version.major_minor() > (0, u32::MAX));
695
696        assert_eq!(cargo_version.major_minor(), (cargo_version.major, cargo_version.minor));
697        assert!(cargo_version.major_minor() < (2, 0));
698        assert!(cargo_version.major_minor() < (1, u32::MAX));
699        assert!(cargo_version.major_minor() >= (1, 70));
700        assert!(cargo_version.major_minor() > (1, 0));
701        assert!(cargo_version.major_minor() > (0, u32::MAX));
702    }
703
704    #[test]
705    fn target_triple() {
706        let t = TargetTripleRef::from("x86_64-unknown-linux-gnu");
707        assert_eq!(t.triple, "x86_64-unknown-linux-gnu");
708        assert!(matches!(t.triple, Cow::Borrowed(..)));
709        assert!(t.spec_path.is_none());
710    }
711
712    #[rustversion::attr(not(nightly), ignore)]
713    #[test]
714    #[cfg_attr(miri, ignore)] // Miri doesn't support pipe2 (inside std::process::Command::output)
715    fn parse_cfg_list() {
716        // builtin targets
717        for target in cmd!("rustc", "--print", "target-list").read().unwrap().lines() {
718            let _cfg = Cfg::from_rustc(cmd!("rustc"), &target.into()).unwrap();
719        }
720        // custom targets
721        for spec_path in
722            fs::read_dir(fixtures_dir().join("target-specs")).unwrap().map(|e| e.unwrap().path())
723        {
724            let _cfg = Cfg::from_rustc(cmd!("rustc"), &spec_path.to_str().unwrap().into()).unwrap();
725        }
726    }
727
728    #[test]
729    fn env_filter() {
730        // NB: sync with bench in bench/bench.rs
731        let env_list = [
732            ("CARGO_BUILD_JOBS", "-1"),
733            ("RUSTC", "rustc"),
734            ("CARGO_BUILD_RUSTC", "rustc"),
735            ("RUSTC_WRAPPER", "rustc_wrapper"),
736            ("CARGO_BUILD_RUSTC_WRAPPER", "rustc_wrapper"),
737            ("RUSTC_WORKSPACE_WRAPPER", "rustc_workspace_wrapper"),
738            ("CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER", "rustc_workspace_wrapper"),
739            ("RUSTDOC", "rustdoc"),
740            ("CARGO_BUILD_RUSTDOC", "rustdoc"),
741            ("CARGO_BUILD_TARGET", "triple"),
742            ("CARGO_TARGET_DIR", "target"),
743            ("CARGO_BUILD_TARGET_DIR", "target"),
744            ("CARGO_ENCODED_RUSTFLAGS", "1"),
745            ("RUSTFLAGS", "1"),
746            ("CARGO_BUILD_RUSTFLAGS", "1"),
747            ("CARGO_ENCODED_RUSTDOCFLAGS", "1"),
748            ("RUSTDOCFLAGS", "1"),
749            ("CARGO_BUILD_RUSTDOCFLAGS", "1"),
750            ("CARGO_INCREMENTAL", "false"),
751            ("CARGO_BUILD_INCREMENTAL", "1"),
752            ("CARGO_BUILD_DEP_INFO_BASEDIR", "1"),
753            ("BROWSER", "1"),
754            ("CARGO_FUTURE_INCOMPAT_REPORT_FREQUENCY", "always"),
755            ("CARGO_CARGO_NEW_VCS", "git"),
756            ("CARGO_HTTP_DEBUG", "true"),
757            ("CARGO_HTTP_PROXY", "-"),
758            ("CARGO_HTTP_TIMEOUT", "1"),
759            ("CARGO_HTTP_CAINFO", "-"),
760            ("CARGO_HTTP_CHECK_REVOKE", "true"),
761            ("CARGO_HTTP_LOW_SPEED_LIMIT", "1"),
762            ("CARGO_HTTP_MULTIPLEXING", "true"),
763            ("CARGO_HTTP_USER_AGENT", "-"),
764            ("CARGO_NET_RETRY", "1"),
765            ("CARGO_NET_GIT_FETCH_WITH_CLI", "false"),
766            ("CARGO_NET_OFFLINE", "false"),
767            ("CARGO_REGISTRIES_crates-io_INDEX", "https://github.com/rust-lang/crates.io-index"),
768            ("CARGO_REGISTRIES_crates-io_TOKEN", "00000000000000000000000000000000000"),
769            ("CARGO_REGISTRY_DEFAULT", "crates-io"),
770            ("CARGO_REGISTRY_TOKEN", "00000000000000000000000000000000000"),
771            ("CARGO_REGISTRIES_CRATES_IO_PROTOCOL", "git"),
772            ("CARGO_TERM_QUIET", "false"),
773            ("CARGO_TERM_VERBOSE", "false"),
774            ("CARGO_TERM_COLOR", "auto"),
775            ("CARGO_TERM_PROGRESS_WHEN", "auto"),
776            ("CARGO_TERM_PROGRESS_WIDTH", "100"),
777        ];
778        let mut config = crate::de::Config::default();
779        let cx =
780            &ResolveOptions::default().env(env_list).into_context(std::env::current_dir().unwrap());
781        config.apply_env(cx).unwrap();
782
783        // ResolveOptions::env attempts to avoid pushing unrelated envs.
784        let mut env_list = env_list.to_vec();
785        env_list.push(("A", "B"));
786        let cx = &ResolveOptions::default()
787            .env(env_list.iter().copied())
788            .into_context(std::env::current_dir().unwrap());
789        for (k, v) in env_list {
790            if k == "A" {
791                assert!(!cx.env.contains_key(k));
792            } else {
793                assert_eq!(cx.env[k], v, "key={k},value={v}");
794            }
795        }
796    }
797
798    #[test]
799    fn rustc_wrapper() {
800        for (env_list, expected) in [
801            (
802                &[
803                    ("RUSTC", "rustc"),
804                    ("CARGO_BUILD_RUSTC", "cargo_build_rustc"),
805                    ("RUSTC_WRAPPER", "rustc_wrapper"),
806                    ("CARGO_BUILD_RUSTC_WRAPPER", "cargo_build_rustc_wrapper"),
807                    ("RUSTC_WORKSPACE_WRAPPER", "rustc_workspace_wrapper"),
808                    ("CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER", "cargo_build_rustc_workspace_wrapper"),
809                ][..],
810                PathAndArgs {
811                    path: "rustc_wrapper".into(),
812                    args: vec!["rustc_workspace_wrapper".into(), "rustc".into()],
813                },
814            ),
815            (
816                &[
817                    ("RUSTC", "rustc"),
818                    ("CARGO_BUILD_RUSTC", "cargo_build_rustc"),
819                    ("RUSTC_WRAPPER", ""),
820                    ("CARGO_BUILD_RUSTC_WRAPPER", "cargo_build_rustc_wrapper"),
821                    ("RUSTC_WORKSPACE_WRAPPER", "rustc_workspace_wrapper"),
822                    ("CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER", "cargo_build_rustc_workspace_wrapper"),
823                ][..],
824                PathAndArgs { path: "rustc_workspace_wrapper".into(), args: vec!["rustc".into()] },
825            ),
826            (
827                &[
828                    ("RUSTC", "rustc"),
829                    ("CARGO_BUILD_RUSTC", "cargo_build_rustc"),
830                    ("RUSTC_WRAPPER", "rustc_wrapper"),
831                    ("CARGO_BUILD_RUSTC_WRAPPER", "cargo_build_rustc_wrapper"),
832                    ("RUSTC_WORKSPACE_WRAPPER", ""),
833                    ("CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER", "cargo_build_rustc_workspace_wrapper"),
834                ][..],
835                PathAndArgs { path: "rustc_wrapper".into(), args: vec!["rustc".into()] },
836            ),
837            (
838                &[
839                    ("CARGO_BUILD_RUSTC", "cargo_build_rustc"),
840                    ("CARGO_BUILD_RUSTC_WRAPPER", "cargo_build_rustc_wrapper"),
841                    ("CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER", "cargo_build_rustc_workspace_wrapper"),
842                ],
843                PathAndArgs {
844                    path: "cargo_build_rustc_wrapper".into(),
845                    args: vec![
846                        "cargo_build_rustc_workspace_wrapper".into(),
847                        "cargo_build_rustc".into(),
848                    ],
849                },
850            ),
851            (
852                &[
853                    ("RUSTC", "rustc"),
854                    ("RUSTC_WRAPPER", "rustc_wrapper"),
855                    ("RUSTC_WORKSPACE_WRAPPER", "rustc_workspace_wrapper"),
856                ],
857                PathAndArgs {
858                    path: "rustc_wrapper".into(),
859                    args: vec!["rustc_workspace_wrapper".into(), "rustc".into()],
860                },
861            ),
862            (
863                &[
864                    ("RUSTC", "rustc"),
865                    ("RUSTC_WRAPPER", "rustc_wrapper"),
866                    ("RUSTC_WORKSPACE_WRAPPER", ""),
867                ],
868                PathAndArgs { path: "rustc_wrapper".into(), args: vec!["rustc".into()] },
869            ),
870            (
871                &[
872                    ("RUSTC", "rustc"),
873                    ("RUSTC_WRAPPER", ""),
874                    ("RUSTC_WORKSPACE_WRAPPER", "rustc_workspace_wrapper"),
875                ],
876                PathAndArgs { path: "rustc_workspace_wrapper".into(), args: vec!["rustc".into()] },
877            ),
878            (&[("RUSTC", "rustc"), ("RUSTC_WRAPPER", "rustc_wrapper")], PathAndArgs {
879                path: "rustc_wrapper".into(),
880                args: vec!["rustc".into()],
881            }),
882            (
883                &[("RUSTC", "rustc"), ("RUSTC_WORKSPACE_WRAPPER", "rustc_workspace_wrapper")],
884                PathAndArgs { path: "rustc_workspace_wrapper".into(), args: vec!["rustc".into()] },
885            ),
886            (&[("RUSTC", "rustc"), ("RUSTC_WRAPPER", "")], PathAndArgs {
887                path: "rustc".into(),
888                args: vec![],
889            }),
890            (&[("RUSTC", "rustc"), ("RUSTC_WORKSPACE_WRAPPER", "")], PathAndArgs {
891                path: "rustc".into(),
892                args: vec![],
893            }),
894        ] {
895            let mut config = crate::de::Config::default();
896            let cx = &ResolveOptions::default()
897                .env(env_list.iter().copied())
898                .into_context(std::env::current_dir().unwrap());
899            config.apply_env(cx).unwrap();
900            let build = crate::easy::BuildConfig::from_unresolved(config.build, &cx.current_dir);
901            assert_eq!(*cx.rustc(&build), expected);
902        }
903    }
904
905    #[cfg(unix)]
906    #[test]
907    fn env_non_utf8() {
908        use std::{ffi::OsStr, os::unix::prelude::OsStrExt as _};
909
910        let cx = &ResolveOptions::default()
911            .env([("CARGO_ALIAS_a", OsStr::from_bytes(&[b'f', b'o', 0x80, b'o']))])
912            .cargo_home(None)
913            .rustc(PathAndArgs::new("rustc"))
914            .into_context(std::env::current_dir().unwrap());
915        assert_eq!(
916            cx.env("CARGO_ALIAS_a").unwrap_err().to_string(),
917            "failed to parse environment variable `CARGO_ALIAS_a`"
918        );
919        assert_eq!(
920            format!("{:#}", anyhow::Error::from(cx.env("CARGO_ALIAS_a").unwrap_err())),
921            "failed to parse environment variable `CARGO_ALIAS_a`: environment variable was not valid unicode: \"fo\\x80o\""
922        );
923    }
924
925    // #[test]
926    // fn dump_all_env() {
927    //     let mut config = crate::de::Config::default();
928    //     let cx = &mut ResolveContext::no_env();
929    //     config.apply_env(cx).unwrap();
930    // }
931}