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