toolchain/
lib.rs

1//! Discovery of `cargo` & `rustc` executables.
2
3use std::{
4    env,
5    ffi::OsStr,
6    iter,
7    path::{Path, PathBuf},
8    process::Command,
9};
10
11use camino::{Utf8Path, Utf8PathBuf};
12
13#[derive(Copy, Clone)]
14pub enum Tool {
15    Cargo,
16    Rustc,
17    Rustup,
18    Rustfmt,
19}
20
21impl Tool {
22    pub fn proxy(self) -> Option<Utf8PathBuf> {
23        cargo_proxy(self.name())
24    }
25
26    /// Return a `PathBuf` to use for the given executable.
27    ///
28    /// The current implementation checks three places for an executable to use:
29    /// 1) `$CARGO_HOME/bin/<executable_name>`
30    ///    where $CARGO_HOME defaults to ~/.cargo (see <https://doc.rust-lang.org/cargo/guide/cargo-home.html>)
31    ///    example: for cargo, this tries $CARGO_HOME/bin/cargo, or ~/.cargo/bin/cargo if $CARGO_HOME is unset.
32    ///    It seems that this is a reasonable place to try for cargo, rustc, and rustup
33    /// 2) Appropriate environment variable (erroring if this is set but not a usable executable)
34    ///    example: for cargo, this checks $CARGO environment variable; for rustc, $RUSTC; etc
35    /// 3) $PATH/`<executable_name>`
36    ///    example: for cargo, this tries all paths in $PATH with appended `cargo`, returning the
37    ///    first that exists
38    /// 4) If all else fails, we just try to use the executable name directly
39    pub fn prefer_proxy(self) -> Utf8PathBuf {
40        invoke(&[cargo_proxy, lookup_as_env_var, lookup_in_path], self.name())
41    }
42
43    /// Return a `PathBuf` to use for the given executable.
44    ///
45    /// The current implementation checks three places for an executable to use:
46    /// 1) Appropriate environment variable (erroring if this is set but not a usable executable)
47    ///    example: for cargo, this checks $CARGO environment variable; for rustc, $RUSTC; etc
48    /// 2) $PATH/`<executable_name>`
49    ///    example: for cargo, this tries all paths in $PATH with appended `cargo`, returning the
50    ///    first that exists
51    /// 3) `$CARGO_HOME/bin/<executable_name>`
52    ///    where $CARGO_HOME defaults to ~/.cargo (see <https://doc.rust-lang.org/cargo/guide/cargo-home.html>)
53    ///    example: for cargo, this tries $CARGO_HOME/bin/cargo, or ~/.cargo/bin/cargo if $CARGO_HOME is unset.
54    ///    It seems that this is a reasonable place to try for cargo, rustc, and rustup
55    /// 4) If all else fails, we just try to use the executable name directly
56    pub fn path(self) -> Utf8PathBuf {
57        invoke(&[lookup_as_env_var, lookup_in_path, cargo_proxy], self.name())
58    }
59
60    pub fn path_in(self, path: &Utf8Path) -> Option<Utf8PathBuf> {
61        probe_for_binary(path.join(self.name()))
62    }
63
64    pub fn name(self) -> &'static str {
65        match self {
66            Tool::Cargo => "cargo",
67            Tool::Rustc => "rustc",
68            Tool::Rustup => "rustup",
69            Tool::Rustfmt => "rustfmt",
70        }
71    }
72}
73
74// Prevent rustup from automatically installing toolchains, see https://github.com/rust-lang/rust-analyzer/issues/20719.
75pub const NO_RUSTUP_AUTO_INSTALL_ENV: (&str, &str) = ("RUSTUP_AUTO_INSTALL", "0");
76
77#[allow(clippy::disallowed_types)] /* generic parameter allows for FxHashMap */
78pub fn command<H>(
79    cmd: impl AsRef<OsStr>,
80    working_directory: impl AsRef<Path>,
81    extra_env: &std::collections::HashMap<String, Option<String>, H>,
82) -> Command {
83    // we are `toolchain::command``
84    #[allow(clippy::disallowed_methods)]
85    let mut cmd = Command::new(cmd);
86    cmd.current_dir(working_directory);
87    cmd.env(NO_RUSTUP_AUTO_INSTALL_ENV.0, NO_RUSTUP_AUTO_INSTALL_ENV.1);
88    for env in extra_env {
89        match env {
90            (key, Some(val)) => cmd.env(key, val),
91            (key, None) => cmd.env_remove(key),
92        };
93    }
94    cmd
95}
96
97fn invoke(list: &[fn(&str) -> Option<Utf8PathBuf>], executable: &str) -> Utf8PathBuf {
98    list.iter().find_map(|it| it(executable)).unwrap_or_else(|| executable.into())
99}
100
101/// Looks up the binary as its SCREAMING upper case in the env variables.
102fn lookup_as_env_var(executable_name: &str) -> Option<Utf8PathBuf> {
103    env::var_os(executable_name.to_ascii_uppercase())
104        .map(PathBuf::from)
105        .map(Utf8PathBuf::try_from)
106        .and_then(Result::ok)
107}
108
109/// Looks up the binary in the cargo home directory if it exists.
110fn cargo_proxy(executable_name: &str) -> Option<Utf8PathBuf> {
111    let mut path = get_cargo_home()?;
112    path.push("bin");
113    path.push(executable_name);
114    probe_for_binary(path)
115}
116
117fn get_cargo_home() -> Option<Utf8PathBuf> {
118    if let Some(path) = env::var_os("CARGO_HOME") {
119        return Utf8PathBuf::try_from(PathBuf::from(path)).ok();
120    }
121
122    if let Some(mut path) = home::home_dir() {
123        path.push(".cargo");
124        return Utf8PathBuf::try_from(path).ok();
125    }
126
127    None
128}
129
130fn lookup_in_path(exec: &str) -> Option<Utf8PathBuf> {
131    let paths = env::var_os("PATH").unwrap_or_default();
132    env::split_paths(&paths)
133        .map(|path| path.join(exec))
134        .map(Utf8PathBuf::try_from)
135        .filter_map(Result::ok)
136        .find_map(probe_for_binary)
137}
138
139pub fn probe_for_binary(path: Utf8PathBuf) -> Option<Utf8PathBuf> {
140    let with_extension = match env::consts::EXE_EXTENSION {
141        "" => None,
142        it => Some(path.with_extension(it)),
143    };
144    iter::once(path).chain(with_extension).find(|it| it.is_file())
145}