1use anyhow::Result;
2use std::process::Command;
3
4const CHIPS: &[ChipInfo] = &[
9 ChipInfo { name: "ESP32", arch: Arch::Xtensa, riscv_target: None },
10 ChipInfo { name: "ESP32-S2", arch: Arch::Xtensa, riscv_target: None },
11 ChipInfo { name: "ESP32-S3", arch: Arch::Xtensa, riscv_target: None },
12 ChipInfo { name: "ESP32-C2", arch: Arch::RiscV, riscv_target: Some("riscv32imc-unknown-none-elf") },
13 ChipInfo { name: "ESP32-C3", arch: Arch::RiscV, riscv_target: Some("riscv32imc-unknown-none-elf") },
14 ChipInfo { name: "ESP32-C6", arch: Arch::RiscV, riscv_target: Some("riscv32imac-unknown-none-elf") },
15 ChipInfo { name: "ESP32-H2", arch: Arch::RiscV, riscv_target: Some("riscv32imac-unknown-none-elf") },
16];
17
18struct ChipInfo {
19 name: &'static str,
20 arch: Arch,
21 riscv_target: Option<&'static str>,
22}
23
24#[derive(PartialEq)]
25enum Arch {
26 Xtensa,
27 RiscV,
28}
29
30struct CheckResult {
31 name: &'static str,
32 status: Status,
33 version: Option<String>,
34 note: Option<&'static str>,
35}
36
37enum Status {
38 Ok,
39 Warning(String),
40 Missing,
41}
42
43struct EnvState {
45 has_cargo: bool,
46 has_esp_toolchain: bool, has_stable_toolchain: bool,
48 has_gcc_toolchain: bool, has_esp_generate: bool,
50 installed_riscv_targets: Vec<String>,
51}
52
53pub fn execute() -> Result<()> {
54 println!();
55 println!("đŦ espforge doctor â environment check");
56 println!("{}", "â".repeat(50));
57 println!();
58
59 let env = probe_environment();
60
61 let checks = vec![
62 check_cargo(&env),
63 check_esp_toolchain(&env),
64 check_esp_generate(&env),
65 check_gcc_toolchain(&env),
66 check_riscv_targets(&env),
67 ];
68
69 let mut all_ok = true;
70
71 for check in &checks {
72 let icon = match &check.status {
73 Status::Ok => "â
",
74 Status::Warning(_) => "â ī¸ ",
75 Status::Missing => "â",
76 };
77
78 let version_str = check
79 .version
80 .as_deref()
81 .map(|v| format!(" ({})", v))
82 .unwrap_or_default();
83
84 println!(" {} {}{}", icon, check.name, version_str);
85
86 match &check.status {
87 Status::Ok => {}
88 Status::Warning(msg) => {
89 println!(" {}", msg);
90 all_ok = false;
91 }
92 Status::Missing => {
93 all_ok = false;
94 }
95 }
96
97 if let Some(note) = check.note {
98 println!(" âšī¸ {}", note);
99 }
100 }
101
102 println!();
103 if all_ok {
104 println!("⨠All checks passed â you're ready to use espforge!");
105 } else {
106 println!("⥠Some issues were found. See above for details.");
107 println!();
108 println!(" Quick fix:");
109 println!(" cargo install cargo-binstall");
110 println!(" cargo binstall espup");
111 println!(" espup install");
112 println!(" cargo binstall esp-generate");
113 println!(" rustup target add riscv32imc-unknown-none-elf");
114 println!(" rustup target add riscv32imac-unknown-none-elf");
115 }
116
117 print_chip_summary(&env);
118
119 Ok(())
120}
121
122fn probe_environment() -> EnvState {
124 let has_cargo = run_version_cmd("cargo", &["--version"]).is_some();
125 let has_gcc_toolchain = run_version_cmd("xtensa-esp-elf-gcc", &["--version"]).is_some();
126 let has_esp_generate = run_version_cmd("esp-generate", &["--version"]).is_some();
127
128 let toolchain_list = Command::new("rustup")
129 .args(["toolchain", "list"])
130 .output()
131 .ok()
132 .map(|o| String::from_utf8_lossy(&o.stdout).to_string())
133 .unwrap_or_default();
134
135 let has_esp_toolchain = toolchain_list.lines().any(|l| l.starts_with("esp"));
136 let has_stable_toolchain = toolchain_list.lines().any(|l| l.starts_with("stable"));
137
138 let installed_riscv_targets = Command::new("rustup")
139 .args(["target", "list", "--installed"])
140 .output()
141 .ok()
142 .map(|o| {
143 String::from_utf8_lossy(&o.stdout)
144 .lines()
145 .map(|l| l.trim().to_string())
146 .collect()
147 })
148 .unwrap_or_default();
149
150 EnvState {
151 has_cargo,
152 has_esp_toolchain,
153 has_stable_toolchain,
154 has_gcc_toolchain,
155 has_esp_generate,
156 installed_riscv_targets,
157 }
158}
159
160fn print_chip_summary(env: &EnvState) {
161 println!();
162 println!("đ Chip readiness summary");
163 println!("{}", "â".repeat(50));
164 println!();
165
166 let name_w = 10;
167 let arch_w = 9;
168
169 println!(
170 " {:<name_w$} {:<arch_w$} {}",
171 "Chip", "Arch", "Status",
172 name_w = name_w,
173 arch_w = arch_w,
174 );
175 println!(" {}", "â".repeat(46));
176
177 for chip in CHIPS {
178 let (icon, reason) = chip_status(chip, env);
179 let arch_label = match chip.arch {
180 Arch::Xtensa => "Xtensa",
181 Arch::RiscV => "RISC-V",
182 };
183 println!(
184 " {:<name_w$} {:<arch_w$} {} {}",
185 chip.name,
186 arch_label,
187 icon,
188 reason,
189 name_w = name_w,
190 arch_w = arch_w,
191 );
192 }
193
194 println!();
195}
196
197fn chip_status(chip: &ChipInfo, env: &EnvState) -> (&'static str, String) {
199 let base_ok = env.has_cargo && env.has_esp_generate;
200
201 match chip.arch {
202 Arch::Xtensa => {
203 let mut missing: Vec<&str> = Vec::new();
204 if !base_ok { missing.push("cargo / esp-generate") }
205 if !env.has_esp_toolchain { missing.push("esp toolchain (espup install)") }
206 if !env.has_gcc_toolchain { missing.push("xtensa-esp-elf-gcc (espup install)") }
207
208 if missing.is_empty() {
209 ("â
", "Ready".to_string())
210 } else {
211 ("â", format!("Missing: {}", missing.join(", ")))
212 }
213 }
214
215 Arch::RiscV => {
216 let target = chip.riscv_target.unwrap_or("");
217 let has_target = env.installed_riscv_targets.iter().any(|t| t == target);
218 let has_toolchain = env.has_stable_toolchain || env.has_esp_toolchain;
219
220 let mut missing: Vec<String> = Vec::new();
221 if !base_ok { missing.push("cargo / esp-generate".to_string()) }
222 if !has_toolchain { missing.push("stable toolchain (rustup toolchain install stable)".to_string()) }
223 if !has_target { missing.push(format!("{} (rustup target add {})", target, target)) }
224
225 if missing.is_empty() {
226 ("â
", "Ready".to_string())
227 } else {
228 ("â", format!("Missing: {}", missing.join(", ")))
229 }
230 }
231 }
232}
233
234fn check_cargo(env: &EnvState) -> CheckResult {
235 if env.has_cargo {
236 CheckResult {
237 name: "cargo / Rust",
238 status: Status::Ok,
239 version: run_version_cmd("cargo", &["--version"]),
240 note: None,
241 }
242 } else {
243 CheckResult {
244 name: "cargo / Rust",
245 status: Status::Missing,
246 version: None,
247 note: Some("Install Rust from https://rustup.rs/"),
248 }
249 }
250}
251
252fn check_esp_toolchain(env: &EnvState) -> CheckResult {
253 if env.has_esp_toolchain {
257 let esp_version = Command::new("rustup")
258 .args(["toolchain", "list"])
259 .output()
260 .ok()
261 .and_then(|o| {
262 String::from_utf8_lossy(&o.stdout)
263 .lines()
264 .find(|l| l.starts_with("esp"))
265 .map(|l| l.trim().to_string())
266 });
267
268 CheckResult {
269 name: "ESP toolchain (Xtensa/RISC-V via espup)",
270 status: Status::Ok,
271 version: esp_version,
272 note: Some("'esp' channel required for Xtensa chips (esp32, esp32s3, esp32s2)"),
273 }
274 } else if env.has_stable_toolchain {
275 CheckResult {
276 name: "ESP toolchain (Xtensa/RISC-V via espup)",
277 status: Status::Warning(
278 "Only 'stable' toolchain found. Xtensa chips (esp32, esp32s3) require \
279 the 'esp' channel.\n Run: espup install"
280 .to_string(),
281 ),
282 version: None,
283 note: Some("RISC-V chips (esp32c3, esp32c6, etc.) work with stable"),
284 }
285 } else {
286 CheckResult {
287 name: "ESP toolchain (Xtensa/RISC-V via espup)",
288 status: Status::Missing,
289 version: None,
290 note: Some("Run: cargo binstall espup && espup install"),
291 }
292 }
293}
294
295fn check_esp_generate(env: &EnvState) -> CheckResult {
296 if !env.has_esp_generate {
297 return CheckResult {
298 name: "esp-generate",
299 status: Status::Missing,
300 version: None,
301 note: Some("Run: cargo binstall esp-generate"),
302 };
303 }
304
305 match run_version_cmd("esp-generate", &["--version"]) {
306 None => CheckResult {
307 name: "esp-generate",
308 status: Status::Missing,
309 version: None,
310 note: Some("Run: cargo binstall esp-generate"),
311 },
312 Some(raw_version) => {
313 let semver = raw_version
314 .split_whitespace()
315 .last()
316 .unwrap_or(&raw_version)
317 .to_string();
318
319 let status = Status::Ok;
320
321 CheckResult {
322 name: "esp-generate",
323 status,
324 version: Some(semver),
325 note: Some(
326 "",
327 ),
328 }
329 }
330 }
331}
332
333fn check_gcc_toolchain(env: &EnvState) -> CheckResult {
334 if env.has_gcc_toolchain {
337 CheckResult {
338 name: "GCC toolchain (xtensa-esp-elf-gcc)",
339 status: Status::Ok,
340 version: run_version_cmd("xtensa-esp-elf-gcc", &["--version"]),
341 note: Some("Required for linking Xtensa binaries (esp32, esp32s2, esp32s3)"),
342 }
343 } else {
344 CheckResult {
345 name: "GCC toolchain (xtensa-esp-elf-gcc)",
346 status: Status::Missing,
347 version: None,
348 note: Some(
349 "Installed by espup â run: cargo binstall espup && espup install\n \
350 Also ensure $HOME/.espup/... is on your PATH (source the export file)",
351 ),
352 }
353 }
354}
355
356fn check_riscv_targets(env: &EnvState) -> CheckResult {
357 let targets = [
358 ("riscv32imc-unknown-none-elf", "ESP32-C2, ESP32-C3"),
359 ("riscv32imac-unknown-none-elf", "ESP32-C6, ESP32-H2"),
360 ];
361
362 let mut missing: Vec<String> = Vec::new();
363 for (target, chips) in &targets {
364 if !env.installed_riscv_targets.iter().any(|t| t == target) {
365 missing.push(format!("{} ({})", target, chips));
366 }
367 }
368
369 if missing.is_empty() {
370 let names = targets.iter().map(|(t, _)| *t).collect::<Vec<_>>().join(", ");
371 CheckResult {
372 name: "RISC-V targets",
373 status: Status::Ok,
374 version: Some(names),
375 note: Some("Covers ESP32-C2/C3 (riscv32imc) and ESP32-C6/H2 (riscv32imac)"),
376 }
377 } else {
378 let fix = missing
379 .iter()
380 .map(|m| {
381 let target = m.split_whitespace().next().unwrap_or("");
382 format!("rustup target add {}", target)
383 })
384 .collect::<Vec<_>>()
385 .join("\n ");
386
387 CheckResult {
388 name: "RISC-V targets",
389 status: Status::Warning(format!(
390 "Missing targets:\n {}\n Fix:\n {}",
391 missing.join("\n "),
392 fix
393 )),
394 version: None,
395 note: Some("Required for RISC-V based ESP chips"),
396 }
397 }
398}
399
400fn run_version_cmd(program: &str, args: &[&str]) -> Option<String> {
402 Command::new(program)
403 .args(args)
404 .output()
405 .ok()
406 .filter(|o| o.status.success())
407 .and_then(|o| {
408 String::from_utf8(o.stdout)
409 .ok()
410 .map(|s| s.lines().next().unwrap_or("").trim().to_string())
411 })
412 .filter(|s| !s.is_empty())
413}