Skip to main content

toolchain_find/
lib.rs

1use std::cmp::Ordering;
2use std::path::{Path, PathBuf};
3use std::process::Command;
4use std::str;
5use std::sync::OnceLock;
6
7use regex::Regex;
8use semver::Version;
9use walkdir::WalkDir;
10
11#[derive(Clone, Copy, Eq, Ord, PartialEq, PartialOrd)]
12enum Toolchains {
13    All,
14    Nightly,
15}
16
17// A `Component` keeps track of the rustc version associated with the component in question.
18#[derive(Debug)]
19struct Component {
20    date_vers: Option<DateVersion>,
21    path: PathBuf,
22}
23
24// A `DateVersion` allows you to sort first by the semantic version and date second if the versions
25// are equal.
26#[derive(Debug, Eq, PartialEq)]
27struct DateVersion {
28    rustc_vers: Option<Version>,
29    date: String,
30}
31
32impl Ord for DateVersion {
33    fn cmp(&self, other: &DateVersion) -> Ordering {
34        let vers_cmp = self.rustc_vers.cmp(&other.rustc_vers);
35        if vers_cmp == Ordering::Equal {
36            return self.date.cmp(&other.date);
37        }
38        vers_cmp
39    }
40}
41
42impl PartialOrd for DateVersion {
43    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
44        Some(self.cmp(other))
45    }
46}
47
48impl DateVersion {
49    fn new(rustc_vers: Option<Version>, date: String) -> DateVersion {
50        DateVersion { rustc_vers, date }
51    }
52}
53
54impl Component {
55    fn new(date_vers: Option<DateVersion>, path: PathBuf) -> Component {
56        Component { date_vers, path }
57    }
58}
59
60// Given the version string from rustc, attempt to parse the date.
61fn parse_rustc_date(rustc_v: &[u8]) -> Option<DateVersion> {
62    static PATTERN: OnceLock<Regex> = OnceLock::new();
63
64    // This may not be the most ideal way to get the version.
65    // It assumes that the output looks like:
66    // rustc 1.32.0 (9fda7c223 2019-01-16)
67    let pattern = PATTERN.get_or_init(|| {
68        Regex::new(
69            r"rustc (\d+.\d+.\d+(?:-[\.0-9a-z]+)?)(?: \([[:alnum:]]+ (\d{4}-\d{2}-\d{2})\))?",
70        )
71        .unwrap()
72    });
73
74    let version = str::from_utf8(rustc_v).unwrap_or_default();
75    let captures = pattern.captures(version)?;
76    let vers = Version::parse(captures.get(1).map_or("", |v| v.as_str())).ok();
77    let date = String::from(captures.get(2).map_or("", |v| v.as_str()));
78
79    Some(DateVersion::new(vers, date))
80}
81
82// Try and parse the version from the Rust compiler.
83fn rustc_version(bin_path: &Path) -> Option<DateVersion> {
84    Command::new(bin_path)
85        .arg("-V")
86        .output()
87        .ok()
88        .and_then(|o| parse_rustc_date(&o.stdout))
89}
90
91/// Given a Rust component name, search through all of the available toolchains
92/// on the system to see if it is installed. It will return the path of the component that has
93/// the latest version.
94pub fn find_installed_component(name: &str) -> Option<PathBuf> {
95    find_installed(name, Toolchains::All)
96}
97
98/// Given a Rust component name, search through all of the available nightly toolchains
99/// on the system to see if it is installed. It will return the path of the component that has
100/// the latest version.
101pub fn find_nightly_installed_component(name: &str) -> Option<PathBuf> {
102    find_installed(name, Toolchains::Nightly)
103}
104
105/// Find an installed component name, optionally filtering to nightly toolchains.
106fn find_installed(name: &str, toolchains: Toolchains) -> Option<PathBuf> {
107    let mut components = Vec::new();
108    let mut root = home::rustup_home().ok()?;
109    root.push("toolchains");
110
111    // For Windows, we need to add an exe extension.
112    let mut n = String::from(name);
113    let name = if cfg!(windows) {
114        n.push_str(".exe");
115        n
116    } else {
117        n
118    };
119
120    for entry in WalkDir::new(root)
121        .max_depth(3)
122        .follow_links(true)
123        .into_iter()
124        .filter_entry(|e| {
125            e.depth() != 1
126                || match toolchains {
127                    Toolchains::All => true,
128                    Toolchains::Nightly => e
129                        .path()
130                        .file_name()
131                        .is_some_and(|f| f.to_string_lossy().starts_with("nightly")),
132                }
133        })
134        .filter_map(|e| e.ok())
135    {
136        let parent = entry.path().parent()?;
137        if parent.ends_with("bin") {
138            let bin_name = entry.path().file_name()?;
139
140            if bin_name == name.as_str() {
141                // This assumes that we will always have a rustc in this same toolchain location.
142                // I suppose a user could have a very custom build but I am not sure how much we
143                // need to support.
144                let mut rustc_path = PathBuf::from(parent);
145                if cfg!(windows) {
146                    rustc_path.push("rustc.exe");
147                } else {
148                    rustc_path.push("rustc");
149                }
150                components.push(Component::new(
151                    rustc_version(&rustc_path),
152                    PathBuf::from(&entry.path()),
153                ));
154            }
155        }
156    }
157
158    // Sort by the rustc version leaving the maximal one at the end.
159    components.sort_by(|a, b| a.date_vers.cmp(&b.date_vers));
160
161    components.pop().map(|c| c.path)
162}
163
164#[cfg(test)]
165mod test {
166    use std::cmp::Ordering;
167
168    use semver::Version;
169
170    use super::{parse_rustc_date, DateVersion};
171
172    #[test]
173    fn test_parse_rustc_date() {
174        let cases = vec![
175            "".as_bytes(),
176            "rustc not found".as_bytes(),
177            "rustc 1.34.0-nightly (097c04cf4 2019-02-24)".as_bytes(),
178            "rustc 1.34.0-beta.1 (744b374ab 2019-02-26)".as_bytes(),
179            "rustc 1.35.0-dev".as_bytes(),
180            "rustc 1.32.0 (9fda7c223 2019-01-16)".as_bytes(),
181        ];
182        let expected = vec![
183            None,
184            None,
185            Some(DateVersion::new(
186                Some(Version::parse("1.34.0-nightly").unwrap()),
187                String::from("2019-02-24"),
188            )),
189            Some(DateVersion::new(
190                Some(Version::parse("1.34.0-beta.1").unwrap()),
191                String::from("2019-02-26"),
192            )),
193            Some(DateVersion::new(
194                Some(Version::parse("1.35.0-dev").unwrap()),
195                String::from(""),
196            )),
197            Some(DateVersion::new(
198                Some(Version::parse("1.32.0").unwrap()),
199                String::from("2019-01-16"),
200            )),
201        ];
202
203        for (i, case) in cases.iter().enumerate() {
204            assert_eq!(parse_rustc_date(case), expected[i]);
205        }
206    }
207
208    #[test]
209    fn test_version_parse_fail() {
210        let v2 = Version::parse("1.1.0").unwrap();
211        let d1 = DateVersion::new(None, String::from("2019-01-01"));
212        let d2 = DateVersion::new(Some(v2), String::from("2019-01-01"));
213
214        assert!(d2 > d1);
215    }
216
217    #[test]
218    fn test_different_versions() {
219        let v1 = Version::parse("1.2.3").unwrap();
220        let v2 = Version::parse("1.1.0").unwrap();
221        let d1 = DateVersion::new(Some(v1), String::from("2019-01-01"));
222        let d2 = DateVersion::new(Some(v2), String::from("2019-01-01"));
223
224        assert!(d2 < d1);
225    }
226
227    #[test]
228    fn test_many_nightly_strings() {
229        let v = Version::parse("1.0.0-nightly").unwrap();
230        let mut versions = vec![
231            DateVersion::new(Some(v.clone()), String::from("2019-02-20")),
232            DateVersion::new(Some(v.clone()), String::from("2019-02-24")),
233            DateVersion::new(Some(v.clone()), String::from("2019-01-10")),
234        ];
235        versions.sort();
236
237        assert_eq!(
238            versions.pop().unwrap(),
239            DateVersion::new(Some(v.clone()), String::from("2019-02-24"))
240        );
241    }
242
243    #[test]
244    fn test_date_version_compare() {
245        let d1 = DateVersion::new(Some(Version::parse("1.34.0").unwrap()), String::from(""));
246        let d2 = DateVersion::new(
247            Some(Version::parse("1.33.0").unwrap()),
248            String::from("2019-04-20"),
249        );
250        let d3 = DateVersion::new(
251            Some(Version::parse("1.33.0").unwrap()),
252            String::from("2019-04-17"),
253        );
254
255        assert_eq!(d1.cmp(&d2), Ordering::Greater);
256        assert_eq!(d2.cmp(&d3), Ordering::Greater);
257    }
258}