executable_path_finder/
lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
//from rust-analyzer/crates/toolchain/src/lib.rs

use std::{env, iter, path::PathBuf};

use camino::Utf8PathBuf;

/// find return a `PathBuf` for the given executable, it tries to find it in PATH and environment variables.
///
/// The current implementation checks three places for an executable to use:
/// 1) $PATH/`<executable_name>`
///      example: for cargo, this tries all paths in $PATH with appended `cargo`, returning the
///      first that exists
/// 2) Appropriate environment variable (erroring if this is set but not a usable executable)
///      example: for cargo, this checks $CARGO environment variable; for rustc, $RUSTC; etc
pub fn find(exec: &str) -> Option<Utf8PathBuf> {
    find_in_path(exec).or_else(|| find_in_env(exec))
}

/// find_with_cargo_home return a `PathBuf` for the given executable, it tries to find it in PATH, environment variables and CARGO_HOME.
///
/// The current implementation checks three places for an executable to use:
/// 1) $PATH/`<executable_name>`
///      example: for cargo, this tries all paths in $PATH with appended `cargo`, returning the
///      first that exists
/// 2) Appropriate environment variable (erroring if this is set but not a usable executable)
///      example: for cargo, this checks $CARGO environment variable; for rustc, $RUSTC; etc
/// 3) `$CARGO_HOME/bin/<executable_name>`
///      where $CARGO_HOME defaults to ~/.cargo (see <https://doc.rust-lang.org/cargo/guide/cargo-home.html>)
///      example: for cargo, this tries $CARGO_HOME/bin/cargo, or ~/.cargo/bin/cargo if $CARGO_HOME is unset.
///      It seems that this is a reasonable place to try for cargo, rustc, and rustup
pub fn find_with_cargo_home(exec: &str) -> Option<Utf8PathBuf> {
    find_in_path(exec)
        .or_else(|| find_in_env(exec))
        .or_else(|| find_in_cargo_home(exec))
}

pub fn find_in_cargo_home(exec: &str) -> Option<Utf8PathBuf> {
    let mut path = get_cargo_home()?;
    path.push("bin");
    path.push(exec);
    probe_for_binary(path)
}

pub fn find_in_env(exec: &str) -> Option<Utf8PathBuf> {
    env::var_os(exec.to_ascii_uppercase())
        .map(PathBuf::from)
        .map(Utf8PathBuf::try_from)
        .and_then(Result::ok)
}

pub fn find_in_path(exec: &str) -> Option<Utf8PathBuf> {
    let paths = env::var_os("PATH").unwrap_or_default();
    env::split_paths(&paths)
        .map(|path| path.join(exec))
        .map(PathBuf::from)
        .map(Utf8PathBuf::try_from)
        .filter_map(Result::ok)
        .find_map(probe_for_binary)
}

pub fn probe_for_binary(path: Utf8PathBuf) -> Option<Utf8PathBuf> {
    let with_extension = match env::consts::EXE_EXTENSION {
        "" => None,
        it => Some(path.with_extension(it)),
    };
    iter::once(path)
        .chain(with_extension)
        .find(|it| it.is_file())
}

fn get_cargo_home() -> Option<Utf8PathBuf> {
    if let Some(path) = env::var_os("CARGO_HOME") {
        return Utf8PathBuf::try_from(PathBuf::from(path)).ok();
    }

    if let Some(mut path) = home::home_dir() {
        path.push(".cargo");
        return Utf8PathBuf::try_from(path).ok();
    }

    None
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::env;
    use std::fs;
    use tempfile::TempDir;

    #[test]
    fn test_find_in_path() {
        let temp_dir = TempDir::new().unwrap();
        let fake_bin = temp_dir.path().join("fake-binary");
        fs::write(&fake_bin, "").unwrap();

        let old_path = env::var_os("PATH");
        env::set_var("PATH", temp_dir.path());

        let expected_path = Utf8PathBuf::try_from(fake_bin).unwrap();
        assert_eq!(find_in_path("fake-binary"), Some(expected_path));
        assert_eq!(find_in_path("non-existent-binary"), None);

        // Restore the original PATH
        if let Some(path) = old_path {
            env::set_var("PATH", path);
        } else {
            env::remove_var("PATH");
        }
    }
    #[test]
    fn test_find_in_env() {
        env::set_var("TESTEXEC", "/path/to/testexec");
        assert!(find_in_env("testexec").is_some());
        assert!(find_in_env("nonexistent").is_none());
        env::remove_var("TESTEXEC");
    }

    #[test]
    fn test_find_with_cargo_home() {
        let temp_dir = TempDir::new().unwrap();
        let fake_cargo_home = temp_dir.path().join(".cargo");
        fs::create_dir_all(fake_cargo_home.join("bin")).unwrap();
        let fake_bin = fake_cargo_home.join("bin").join("fake-cargo-binary");
        fs::write(&fake_bin, "").unwrap();

        env::set_var("CARGO_HOME", fake_cargo_home);

        assert!(find_with_cargo_home("fake-cargo-binary").is_some());
        assert!(find_with_cargo_home("non-existent-binary").is_none());

        env::remove_var("CARGO_HOME");
    }

    #[test]
    fn test_probe_for_binary() {
        let temp_dir = TempDir::new().unwrap();
        let fake_bin = temp_dir.path().join("fake-binary");
        fs::write(&fake_bin, "").unwrap();

        assert!(probe_for_binary(Utf8PathBuf::try_from(fake_bin).unwrap()).is_some());
        assert!(probe_for_binary(
            Utf8PathBuf::try_from(temp_dir.path().join("non-existent")).unwrap()
        )
        .is_none());
    }
}