broom/
toolchains_command.rs1use anyhow::Result;
2use runemark::{
3 ColorMode, Console, Finding, FindingGroup, Location, NextStep, Report, Tone, Verdict,
4};
5use std::collections::HashSet;
6use std::fs;
7use std::io::Write;
8use std::path::{Path, PathBuf};
9use walkdir::WalkDir;
10
11use crate::report::OutputFormat;
12
13pub fn run_toolchains(
14 root_path: &Path,
15 output_format: OutputFormat,
16 color_mode: ColorMode,
17 out: &mut dyn Write,
18) -> Result<()> {
19 let rustup_home = std::env::var_os("RUSTUP_HOME")
20 .map(PathBuf::from)
21 .or_else(|| dirs_home().map(|h| h.join(".rustup")));
22
23 let toolchains_dir = match rustup_home {
24 Some(home) => home.join("toolchains"),
25 None => {
26 anyhow::bail!("Could not determine $RUSTUP_HOME or $HOME directory");
27 }
28 };
29
30 if !toolchains_dir.exists() {
31 if output_format == OutputFormat::Tty {
32 let console = Console::new(color_mode, true);
33 let report = Report::new("cargo-broom toolchains", Verdict::Skipped);
34 writeln!(out, "{}", report.render(console))?;
35 }
36 return Ok(());
37 }
38
39 let mut installed_toolchains = Vec::new();
41 if let Ok(entries) = fs::read_dir(&toolchains_dir) {
42 for entry in entries.filter_map(|e| e.ok()) {
43 let path = entry.path();
44 if path.is_dir()
45 && let Some(name) = path.file_name().and_then(|n| n.to_str())
46 {
47 installed_toolchains.push((name.to_string(), path));
48 }
49 }
50 }
51
52 let referenced_toolchains = collect_referenced_toolchains(root_path);
54
55 let mut unreferenced = Vec::new();
57 for (name, path) in &installed_toolchains {
58 let is_referenced = referenced_toolchains
59 .iter()
60 .any(|ref_name| name.contains(ref_name) || ref_name.contains(name));
61 if !is_referenced {
62 unreferenced.push((name.clone(), path.clone()));
63 }
64 }
65
66 if output_format == OutputFormat::Json {
67 let json_report = serde_json::json!({
68 "root_path": root_path,
69 "installed_toolchains_count": installed_toolchains.len(),
70 "unreferenced_toolchains": unreferenced.iter().map(|(n, _)| n).collect::<Vec<_>>(),
71 });
72 writeln!(out, "{}", serde_json::to_string_pretty(&json_report)?)?;
73 return Ok(());
74 }
75
76 let console = Console::new(color_mode, true);
77 let title = console.paint(Tone::Title, "cargo-broom Toolchain Inspection");
78 writeln!(out, "{}", title)?;
79
80 let verdict = if unreferenced.is_empty() {
81 Verdict::Passed
82 } else {
83 Verdict::Info
84 };
85
86 let mut report = Report::new("cargo-broom toolchains", verdict);
87
88 if !unreferenced.is_empty() {
89 let mut group = FindingGroup::new("Unreferenced rustup Toolchains");
90 for (name, path) in &unreferenced {
91 let finding = Finding::new(
92 Tone::Info,
93 format!(
94 "Toolchain `{}` is not referenced by any local project",
95 name
96 ),
97 )
98 .with_location(Location::Artifact(path.clone()));
99 group = group.add_finding(finding);
100 }
101 report = report.add_group(group);
102
103 let step = NextStep::new("Uninstall unneeded toolchains using rustup")
104 .with_command(format!("rustup toolchain uninstall {}", unreferenced[0].0));
105 report = report.add_next_step(step);
106 }
107
108 writeln!(out, "{}", report.render(console))?;
109 Ok(())
110}
111
112fn collect_referenced_toolchains(root: &Path) -> HashSet<String> {
113 let mut ref_set = HashSet::new();
114 for entry in WalkDir::new(root).into_iter().filter_map(|e| e.ok()) {
115 let name = entry.file_name().to_string_lossy();
116 if (name == "rust-toolchain" || name == "rust-toolchain.toml")
117 && let Ok(content) = fs::read_to_string(entry.path())
118 {
119 if let Ok(toml_val) = toml::from_str::<toml::Value>(&content) {
120 if let Some(channel) = toml_val
121 .get("toolchain")
122 .and_then(|t| t.get("channel"))
123 .and_then(|c| c.as_str())
124 {
125 ref_set.insert(channel.to_string());
126 }
127 } else {
128 ref_set.insert(content.trim().to_string());
129 }
130 }
131 }
132 ref_set
133}
134
135fn dirs_home() -> Option<PathBuf> {
136 std::env::var_os("HOME").map(PathBuf::from)
137}