1use std::collections::BTreeMap;
7use std::path::PathBuf;
8
9use async_trait::async_trait;
10
11use crate::backend::{Backend, Ctx, InstallCtx};
12use crate::error::{Error, Result};
13use crate::pipeline::{self, HashAlgo};
14use crate::process;
15use crate::source::Source;
16use crate::version::{ToolVersion, VersionInfo};
17
18pub struct RustBackend;
19
20impl RustBackend {
21 fn rustup_env(ctx: &Ctx, source: Option<&Source>) -> BTreeMap<String, String> {
24 let mut env = BTreeMap::new();
25 env.insert(
26 "RUSTUP_HOME".to_string(),
27 ctx.dirs.rustup_home().display().to_string(),
28 );
29 env.insert(
30 "CARGO_HOME".to_string(),
31 ctx.dirs.cargo_home().display().to_string(),
32 );
33 if let Some(src) = source {
34 env.insert("RUSTUP_DIST_SERVER".to_string(), src.download_url.clone());
35 if let Some(update_root) = &src.index_url {
36 env.insert("RUSTUP_UPDATE_ROOT".to_string(), update_root.clone());
37 }
38 }
39 env
40 }
41
42 fn rustup_bin(ctx: &Ctx) -> PathBuf {
43 let exe = if ctx.platform.os == crate::platform::Os::Windows {
44 "rustup.exe"
45 } else {
46 "rustup"
47 };
48 ctx.dirs.cargo_home().join("bin").join(exe)
49 }
50
51 pub fn run_rustup(
52 ctx: &Ctx,
53 args: &[&str],
54 cwd: Option<&std::path::Path>,
55 ) -> Result<std::process::Output> {
56 let rustup = Self::rustup_bin(ctx);
57 if !rustup.is_file() {
58 return Err(Error::other(format!(
59 "isolated rustup is missing at {}; install Rust first",
60 rustup.display()
61 )));
62 }
63 let env = Self::rustup_env(ctx, None);
64 process::output(&rustup.display().to_string(), args, &env, cwd)
65 }
66
67 pub fn reconcile_markers(ctx: &Ctx) -> Result<(usize, usize)> {
68 let toolchains = ctx.dirs.rustup_home().join("toolchains");
69 let marker_root = ctx.dirs.installs.join("rust");
70 crate::dirs::create_dir_all(&marker_root)?;
71 let mut created = 0usize;
72 let mut removed = 0usize;
73 let mut actual = std::collections::BTreeSet::new();
74 if toolchains.is_dir() {
75 for entry in
76 std::fs::read_dir(&toolchains).map_err(|error| Error::io(&toolchains, error))?
77 {
78 let entry = entry.map_err(|error| Error::io(&toolchains, error))?;
79 let file_type = entry
80 .file_type()
81 .map_err(|error| Error::io(entry.path(), error))?;
82 if !file_type.is_dir() && !file_type.is_symlink() {
83 continue;
84 }
85 let name = entry.file_name().to_string_lossy().into_owned();
86 actual.insert(name.clone());
87 let marker = marker_root.join(&name);
88 if !marker.join(".osdk-complete").is_file() {
89 crate::dirs::create_dir_all(&marker)?;
90 std::fs::write(marker.join(".osdk-complete"), b"")
91 .map_err(|error| Error::io(marker.join(".osdk-complete"), error))?;
92 created += 1;
93 }
94 }
95 }
96 if marker_root.is_dir() {
97 for entry in
98 std::fs::read_dir(&marker_root).map_err(|error| Error::io(&marker_root, error))?
99 {
100 let entry = entry.map_err(|error| Error::io(&marker_root, error))?;
101 let name = entry.file_name().to_string_lossy().into_owned();
102 if name.starts_with('.') || actual.contains(&name) {
103 continue;
104 }
105 if entry.path().is_dir() {
106 std::fs::remove_dir_all(entry.path())
107 .map_err(|error| Error::io(entry.path(), error))?;
108 removed += 1;
109 }
110 }
111 }
112 Ok((created, removed))
113 }
114
115 pub fn record_linked_toolchain(ctx: &Ctx, name: &str, path: &std::path::Path) -> Result<()> {
116 let marker = ctx.dirs.install_path("rust", name);
117 crate::dirs::create_dir_all(&marker)?;
118 let canonical = dunce::canonicalize(path).map_err(|error| Error::io(path, error))?;
119 std::fs::write(marker.join(".osdk-linked"), canonical.display().to_string())
120 .map_err(|error| Error::io(marker.join(".osdk-linked"), error))?;
121 std::fs::write(marker.join(".osdk-complete"), b"")
122 .map_err(|error| Error::io(marker.join(".osdk-complete"), error))
123 }
124
125 pub fn linked_path(ctx: &Ctx, name: &str) -> Option<PathBuf> {
126 let marker = ctx.dirs.install_path("rust", name).join(".osdk-linked");
127 let value = std::fs::read_to_string(marker).ok()?;
128 let path = PathBuf::from(value.trim());
129 path.is_dir().then_some(path)
130 }
131
132 async fn ensure_rustup(ctx: &Ctx, sources: &[Source]) -> Result<PathBuf> {
134 let local = Self::rustup_bin(ctx);
135 if local.exists() {
136 return Ok(local);
137 }
138 let file_name = format!("rustup-init{}", ctx.platform.os.exe_suffix());
139 let triple = ctx.platform.llvm_triple();
140 let cached = ctx
141 .dirs
142 .downloads()
143 .join("rustup")
144 .join(&triple)
145 .join(&file_name);
146 if ctx.config.settings.offline && !cached.exists() {
147 return Err(Error::other(
148 "offline rustup bootstrap cache miss (install rust once without --offline)",
149 ));
150 }
151 let mut last_err = None;
152
153 for source in sources {
154 let update_root = source
155 .index_url
156 .clone()
157 .unwrap_or_else(|| crate::http::join_url(&source.download_url, "rustup"));
158 let url = crate::http::join_url(&update_root, &format!("dist/{triple}/{file_name}"));
159 let checksum_url = format!("{url}.sha256");
160 let checksum = match crate::http::get_cached_text(ctx, &checksum_url).await {
161 Ok(body) => match pipeline::verify::parse_sha256_token(&body) {
162 Some(checksum) => checksum,
163 None => {
164 last_err = Some(Error::other(format!(
165 "invalid rustup-init checksum from {checksum_url}"
166 )));
167 continue;
168 }
169 },
170 Err(error) => {
171 last_err = Some(error);
172 continue;
173 }
174 };
175 match pipeline::download::download(
176 &ctx.client,
177 &url,
178 &cached,
179 "rustup-init",
180 ctx.show_progress,
181 )
182 .await
183 {
184 Ok(()) => {
185 if let Err(error) = pipeline::verify::verify_file(
186 &cached,
187 &checksum,
188 HashAlgo::Sha256,
189 &file_name,
190 ) {
191 let _ = std::fs::remove_file(&cached);
192 last_err = Some(error);
193 continue;
194 }
195 }
196 Err(error) => {
197 last_err = Some(error);
198 continue;
199 }
200 }
201 #[cfg(unix)]
202 {
203 use std::os::unix::fs::PermissionsExt;
204 let permissions = std::fs::Permissions::from_mode(0o755);
205 std::fs::set_permissions(&cached, permissions)
206 .map_err(|error| Error::io(&cached, error))?;
207 }
208 let env = Self::rustup_env(ctx, Some(source));
209 process::run(
210 &cached.display().to_string(),
211 &[
212 "-y",
213 "--no-modify-path",
214 "--profile",
215 "minimal",
216 "--default-toolchain",
217 "none",
218 ],
219 &env,
220 None,
221 )?;
222 if local.exists() {
223 return Ok(local);
224 }
225 last_err = Some(Error::other(format!(
226 "rustup-init completed but {} was not created",
227 local.display()
228 )));
229 }
230
231 Err(last_err.unwrap_or_else(|| Error::NoUsableSource {
232 tool: "rust".to_string(),
233 tried: sources.len(),
234 }))
235 }
236}
237
238#[async_trait]
239impl Backend for RustBackend {
240 fn id(&self) -> &str {
241 "rust"
242 }
243
244 fn aliases(&self) -> &[&str] {
245 &["rustup"]
246 }
247
248 fn default_sources(&self) -> Vec<Source> {
249 vec![
250 Source::official("official", "https://static.rust-lang.org")
251 .with_index("https://static.rust-lang.org/rustup"),
252 Source::mirror("rsproxy", "https://rsproxy.cn", 5)
253 .with_index("https://rsproxy.cn/rustup"),
254 Source::mirror("tuna", "https://mirrors.tuna.tsinghua.edu.cn/rustup", 10)
255 .with_index("https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup"),
256 ]
257 }
258
259 fn probe_url(&self, _ctx: &Ctx, source: &Source) -> Option<String> {
260 Some(crate::http::join_url(
262 &source.download_url,
263 "dist/channel-rust-stable.toml",
264 ))
265 }
266
267 async fn list_remote_versions(&self, _ctx: &Ctx) -> Result<Vec<VersionInfo>> {
268 Ok(vec![
271 VersionInfo::stable("stable"),
272 VersionInfo::stable("beta"),
273 VersionInfo::stable("nightly"),
274 ])
275 }
276
277 async fn resolve_version(
278 &self,
279 _ctx: &Ctx,
280 req: &crate::version::ToolRequest,
281 ) -> Result<ToolVersion> {
282 use crate::version::VersionSpec;
283 let version = match &req.spec {
285 VersionSpec::Latest => "stable".to_string(),
286 VersionSpec::Exact(v) => v.clone(),
287 VersionSpec::Prefix(p) => p.clone(),
288 VersionSpec::Range(requirement) => requirement.clone(),
289 VersionSpec::Lts(_) => "stable".to_string(),
290 VersionSpec::System => "stable".to_string(),
291 };
292 let mut tv = ToolVersion::new(self.id(), version);
293 tv.options = req.options.clone();
294 Ok(tv)
295 }
296
297 async fn install(&self, ictx: &InstallCtx<'_>, tv: &ToolVersion) -> Result<()> {
298 let ctx = ictx.ctx;
299 let sources = crate::source::select::ranked_source_list(ctx, self).await?;
300 let rustup = Self::ensure_rustup(ctx, &sources).await?;
301 let source = sources.first();
302 let env = Self::rustup_env(ctx, source);
303 if let Some(s) = source {
304 tracing::info!(source = %s.id, dist = %s.download_url, "{}", crate::i18n::tr("log.rustup_dist_server"));
305 }
306 let toolchain_bin = Self::toolchain_dir(ctx, &tv.version).join("bin");
307 let rustc = toolchain_bin.join(format!("rustc{}", ctx.platform.os.exe_suffix()));
308 if ctx.config.settings.offline && !rustc.exists() {
309 return Err(Error::other(format!(
310 "offline rust toolchain cache miss for {}",
311 tv.version
312 )));
313 }
314
315 let profile = tv
317 .options
318 .get("profile")
319 .map(|s| s.as_str())
320 .unwrap_or("default");
321 let mut args: Vec<String> = vec![
322 "toolchain".into(),
323 "install".into(),
324 tv.version.clone(),
325 "--profile".into(),
326 profile.into(),
327 ];
328 if let Some(components) = tv.options.get("components") {
329 for c in components.split(',').filter(|s| !s.is_empty()) {
330 args.push("--component".into());
331 args.push(c.to_string());
332 }
333 }
334 if let Some(targets) = tv.options.get("targets") {
335 for t in targets.split(',').filter(|s| !s.is_empty()) {
336 args.push("--target".into());
337 args.push(t.to_string());
338 }
339 }
340 let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
341 if !rustc.exists() {
346 let run_res = process::run(&rustup.display().to_string(), &arg_refs, &env, None);
347 if let Err(e) = run_res {
348 if !rustc.exists() {
349 return Err(e);
350 }
351 tracing::debug!("rustup returned an error but the toolchain installed; continuing");
352 }
353 }
354
355 let install_dir = ctx.dirs.install_path(self.id(), &tv.version);
359 crate::dirs::create_dir_all(&install_dir)?;
360 std::fs::write(install_dir.join(".osdk-complete"), b"")
361 .map_err(|e| Error::io(install_dir.join(".osdk-complete"), e))?;
362 Ok(())
363 }
364
365 async fn uninstall(&self, ctx: &Ctx, tv: &ToolVersion) -> Result<()> {
366 let toolchain_dir = Self::toolchain_dir(ctx, &tv.version);
367 if toolchain_dir.exists() {
368 let rustup = Self::rustup_bin(ctx);
369 if !rustup.exists() {
370 return Err(Error::other(format!(
371 "cannot uninstall rust toolchain {}: isolated rustup is missing",
372 tv.version
373 )));
374 }
375 let env = Self::rustup_env(ctx, None);
376 process::run(
377 &rustup.display().to_string(),
378 &["toolchain", "uninstall", &tv.version],
379 &env,
380 None,
381 )?;
382 }
383 let marker_dir = ctx.dirs.install_path(self.id(), &tv.version);
384 if marker_dir.exists() {
385 std::fs::remove_dir_all(&marker_dir).map_err(|error| Error::io(&marker_dir, error))?;
386 }
387 Ok(())
388 }
389
390 fn bin_paths(&self, ctx: &Ctx, tv: &ToolVersion) -> Result<Vec<PathBuf>> {
391 if let Some(linked) = Self::linked_path(ctx, &tv.version) {
392 return Ok(vec![linked.join("bin"), ctx.dirs.cargo_home().join("bin")]);
393 }
394 let toolchain_dir = Self::toolchain_dir(ctx, &tv.version);
396 Ok(vec![
397 toolchain_dir.join("bin"),
398 ctx.dirs.cargo_home().join("bin"),
399 ])
400 }
401
402 fn exec_env(&self, ctx: &Ctx, _tv: &ToolVersion) -> Result<BTreeMap<String, String>> {
403 let mut env = BTreeMap::new();
404 env.insert(
405 "RUSTUP_HOME".to_string(),
406 ctx.dirs.rustup_home().display().to_string(),
407 );
408 env.insert(
409 "CARGO_HOME".to_string(),
410 ctx.dirs.cargo_home().display().to_string(),
411 );
412 Ok(env)
413 }
414
415 fn bin_names(&self, _ctx: &Ctx, _tv: &ToolVersion) -> Result<Vec<String>> {
416 Ok(vec![
417 "rustc".into(),
418 "cargo".into(),
419 "rustup".into(),
420 "clippy-driver".into(),
421 "rustfmt".into(),
422 ])
423 }
424
425 fn idiomatic_files(&self) -> &[&str] {
426 &["rust-toolchain.toml", "rust-toolchain"]
427 }
428}
429
430impl RustBackend {
431 pub(crate) fn toolchain_dir(ctx: &Ctx, version: &str) -> PathBuf {
434 Self::toolchain_dir_for_dirs(&ctx.dirs, ctx.platform, version)
435 }
436
437 pub(crate) fn toolchain_dir_for_dirs(
438 dirs: &crate::dirs::Dirs,
439 platform: crate::platform::Platform,
440 version: &str,
441 ) -> PathBuf {
442 let toolchains = dirs.rustup_home().join("toolchains");
443 let exact = toolchains.join(version);
444 if exact.exists() {
445 return exact;
446 }
447 let triple = platform.llvm_triple();
449 let with_triple = toolchains.join(format!("{version}-{triple}"));
450 if with_triple.exists() {
451 return with_triple;
452 }
453 if let Ok(rd) = std::fs::read_dir(&toolchains) {
455 for entry in rd.flatten() {
456 let name = entry.file_name().to_string_lossy().to_string();
457 if name.starts_with(version) {
458 return entry.path();
459 }
460 }
461 }
462 exact
463 }
464
465 pub(crate) fn exact_toolchain_dir_for_dirs(
469 dirs: &crate::dirs::Dirs,
470 platform: crate::platform::Platform,
471 version: &str,
472 ) -> Option<PathBuf> {
473 let toolchains = dirs.rustup_home().join("toolchains");
474 let exact = toolchains.join(version);
475 if exact.is_dir() {
476 return Some(exact);
477 }
478 let triple = platform.llvm_triple();
479 let with_triple = toolchains.join(format!("{version}-{triple}"));
480 if with_triple.is_dir() {
481 return Some(with_triple);
482 }
483 let prefix = format!("{version}-");
484 let mut matches = std::fs::read_dir(toolchains)
485 .ok()?
486 .flatten()
487 .filter(|entry| entry.path().is_dir())
488 .filter(|entry| entry.file_name().to_string_lossy().starts_with(&prefix))
489 .map(|entry| entry.path());
490 let selected = matches.next()?;
491 matches.next().is_none().then_some(selected)
492 }
493}
494
495#[cfg(test)]
496mod tests {
497 #[cfg(unix)]
498 #[tokio::test]
499 async fn uninstall_delegates_to_isolated_rustup() {
500 use super::*;
501 use std::os::unix::fs::PermissionsExt;
502
503 let temp = tempfile::tempdir().unwrap();
504 let dirs = crate::dirs::Dirs::resolve_from(|key| match key {
505 "OSDK_DATA_DIR" => Some(temp.path().join("data").display().to_string()),
506 "OSDK_CACHE_DIR" => Some(temp.path().join("cache").display().to_string()),
507 "OSDK_CONFIG_DIR" => Some(temp.path().join("config").display().to_string()),
508 _ => None,
509 })
510 .unwrap();
511 dirs.ensure().unwrap();
512 let log = temp.path().join("rustup.log");
513 let rustup = dirs.cargo_home().join("bin/rustup");
514 std::fs::create_dir_all(rustup.parent().unwrap()).unwrap();
515 std::fs::write(
516 &rustup,
517 format!("#!/bin/sh\nprintf '%s' \"$*\" > '{}'\n", log.display()),
518 )
519 .unwrap();
520 std::fs::set_permissions(&rustup, std::fs::Permissions::from_mode(0o755)).unwrap();
521 std::fs::create_dir_all(dirs.rustup_home().join("toolchains/stable/bin")).unwrap();
522 let marker = dirs.install_path("rust", "stable");
523 std::fs::create_dir_all(&marker).unwrap();
524 std::fs::write(marker.join(".osdk-complete"), b"").unwrap();
525
526 let ctx = Ctx {
527 dirs: dirs.clone(),
528 platform: crate::platform::Platform::current(),
529 config: crate::config::Config {
530 settings: Default::default(),
531 sources: Default::default(),
532 tools: Default::default(),
533 tool_configs: Default::default(),
534 global_tools: Default::default(),
535 global_tool_configs: Default::default(),
536 tool_origins: Default::default(),
537 aliases: Default::default(),
538 project_config_path: None,
539 },
540 client: reqwest::Client::new(),
541 cas: std::sync::Arc::new(crate::store::Cas::new(dirs.store.clone())),
542 show_progress: false,
543 };
544 RustBackend
545 .uninstall(&ctx, &ToolVersion::new("rust", "stable"))
546 .await
547 .unwrap();
548
549 assert_eq!(
550 std::fs::read_to_string(log).unwrap(),
551 "toolchain uninstall stable"
552 );
553 assert!(!marker.exists());
554 }
555}