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#[derive(Debug)]
19struct Component {
20 date_vers: Option<DateVersion>,
21 path: PathBuf,
22}
23
24#[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
60fn parse_rustc_date(rustc_v: &[u8]) -> Option<DateVersion> {
62 static PATTERN: OnceLock<Regex> = OnceLock::new();
63
64 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
82fn 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
91pub fn find_installed_component(name: &str) -> Option<PathBuf> {
95 find_installed(name, Toolchains::All)
96}
97
98pub fn find_nightly_installed_component(name: &str) -> Option<PathBuf> {
102 find_installed(name, Toolchains::Nightly)
103}
104
105fn 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 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 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 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}