1use std::path::{Path, PathBuf};
14
15use crate::{
16 esp32::{
17 chip::{Esp32Arch, Esp32Chip},
18 platform::newest_toolchain_subpath,
19 },
20 toolchain::{
21 Host, Installation, Toolchain, ToolchainError,
22 cargo_helpers::{CargoHelpersInstallation, FailToInstallCargoHelpers},
23 rust::rustup_toolchains_dir,
24 },
25 utils::CommandError,
26};
27
28#[derive(Debug, Clone)]
36pub struct Esp32Toolchain {
37 chips: Vec<Esp32Chip>,
38}
39
40impl Esp32Toolchain {
41 #[must_use]
43 pub fn new(chips: impl IntoIterator<Item = Esp32Chip>) -> Self {
44 Self {
45 chips: chips.into_iter().collect(),
46 }
47 }
48}
49
50#[derive(Debug, Clone)]
57pub struct Esp32ToolchainInstallation {
58 missing: Vec<String>,
60 manual: Vec<String>,
62 install_espup: bool,
64 run_espup: bool,
66 riscv_gcc: bool,
68 helpers: CargoHelpersInstallation,
70}
71
72impl Esp32ToolchainInstallation {
73 #[must_use]
75 pub fn describe(&self) -> String {
76 let manual = if self.manual.is_empty() {
77 String::new()
78 } else {
79 format!(". Manual steps: {}", self.manual.join("; "))
80 };
81 format!(
82 "ESP32 toolchain incomplete: {}{manual}",
83 self.missing.join(", ")
84 )
85 }
86}
87
88#[derive(Debug, thiserror::Error)]
90pub enum FailToInstallEsp32Toolchain {
91 #[error("Failed to install `espup`: {0}")]
93 InstallEspup(#[source] FailToInstallCargoHelpers),
94 #[error("`espup install` failed: {0}")]
96 EspupInstall(#[source] CommandError),
97 #[error(transparent)]
99 Helpers(#[from] FailToInstallCargoHelpers),
100}
101
102fn esp_toolchain_dir(host: &Host) -> Option<PathBuf> {
105 rustup_toolchains_dir(host).map(|dir| dir.join("esp"))
106}
107
108const fn qemu_install_hint() -> &'static str {
110 if cfg!(target_os = "macos") {
111 "brew install qemu"
112 } else if cfg!(target_os = "windows") {
113 "install QEMU from https://www.qemu.org/download/#windows or `winget install QEMU`"
114 } else {
115 "install the qemu-system package (e.g. `apt install qemu-system-misc`)"
116 }
117}
118
119#[derive(Default)]
121struct Esp32Findings {
122 missing: Vec<String>,
124 manual: Vec<String>,
126 run_espup: bool,
128 riscv_gcc: bool,
130 helpers: Vec<String>,
132}
133
134impl Esp32Toolchain {
135 fn probe_esp_toolchain(&self, host: &Host, findings: &mut Esp32Findings) {
140 let Some(esp_dir) = esp_toolchain_dir(host).filter(|dir| dir.is_dir()) else {
141 findings.missing.push("the `esp` Rust toolchain".to_owned());
142 findings.run_espup = true;
143 return;
144 };
145 if newest_toolchain_subpath(
146 &esp_dir.join("xtensa-esp32-elf-clang"),
147 Path::new("esp-clang/lib"),
148 )
149 .is_none()
150 {
151 findings
152 .missing
153 .push("the Espressif clang libraries".to_owned());
154 findings.run_espup = true;
155 }
156 if !esp_dir.join("lib/rustlib/src/rust").is_dir() {
157 findings
158 .missing
159 .push("the `rust-src` component on the `esp` toolchain".to_owned());
160 findings.run_espup = true;
161 }
162 if self
163 .chips
164 .iter()
165 .any(|chip| chip.arch() == Esp32Arch::Xtensa)
166 {
167 let gcc = Esp32Chip::Esp32S3.gcc_component();
168 if newest_toolchain_subpath(&esp_dir.join(gcc.component), Path::new(gcc.bin_subpath))
169 .is_none()
170 {
171 findings
172 .missing
173 .push(format!("the {} ({})", gcc.what, gcc.component));
174 findings.run_espup = true;
175 }
176 }
177 }
178
179 fn probe_riscv_gcc(&self, host: &Host, findings: &mut Esp32Findings) {
182 let Some(chip) = self
183 .chips
184 .iter()
185 .find(|chip| chip.arch() == Esp32Arch::RiscV)
186 else {
187 return;
188 };
189 let gcc = chip.gcc_component();
190 let base = host
191 .home_dir()
192 .map(|home| home.join(".espressif/tools").join(gcc.component));
193 let present = base.as_ref().is_some_and(|base| {
194 newest_toolchain_subpath(base, Path::new(gcc.bin_subpath)).is_some()
195 });
196 if present {
197 return;
198 }
199 findings.missing.push(format!(
200 "the {} (`{}` under ~/.espressif/tools)",
201 gcc.what, gcc.component
202 ));
203 findings.run_espup = true;
206 findings.riscv_gcc = true;
207 findings.manual.push(format!(
208 "if `espup install --esp-riscv-gcc` does not provide the {}, install it with ESP-IDF's `idf_tools.py install`",
209 gcc.what
210 ));
211 }
212
213 async fn probe_binaries(&self, host: &Host, findings: &mut Esp32Findings) {
217 for binary in ["espflash", "ldproxy"] {
218 if host.which(binary).await.is_err() {
219 findings.missing.push(format!("`{binary}` on PATH"));
220 findings.helpers.push(binary.to_owned());
221 }
222 }
223 let mut qemu_checked = Vec::<&'static str>::new();
224 for qemu in self.chips.iter().map(|chip| chip.qemu_binary()) {
225 if qemu_checked.contains(&qemu) {
226 continue;
227 }
228 qemu_checked.push(qemu);
229 if host.which(qemu).await.is_err() {
230 findings
231 .missing
232 .push(format!("`{qemu}` on PATH (for emulated `water run`)"));
233 findings
234 .manual
235 .push(format!("install QEMU with `{}`", qemu_install_hint()));
236 }
237 }
238 }
239}
240
241impl Toolchain for Esp32Toolchain {
242 type Installation = Esp32ToolchainInstallation;
243
244 async fn check(&self, host: &Host) -> Result<(), ToolchainError<Self::Installation>> {
245 let mut findings = Esp32Findings::default();
246 self.probe_esp_toolchain(host, &mut findings);
247 self.probe_riscv_gcc(host, &mut findings);
248 self.probe_binaries(host, &mut findings).await;
249
250 if findings.missing.is_empty() {
251 return Ok(());
252 }
253
254 let cargo_available = host.which("cargo").await.is_ok();
255 let espup_available = host.which("espup").await.is_ok();
256 let helpers_missing = !findings.helpers.is_empty();
257 let install_espup = findings.run_espup && !espup_available;
258 if !cargo_available && (install_espup || helpers_missing) {
261 let mut commands = Vec::new();
262 if install_espup {
263 commands.push("cargo install espup".to_owned());
264 }
265 if findings.run_espup {
266 commands.push("espup install".to_owned());
267 }
268 if helpers_missing {
269 commands.push(format!("cargo install {}", findings.helpers.join(" ")));
270 }
271 return Err(ToolchainError::unfixable(
272 format!(
273 "ESP32 toolchain incomplete: {}",
274 findings.missing.join(", ")
275 ),
276 format!(
277 "Install Rust via rustup first (see the `rust` doctor item), then run {}.",
278 commands.join("`, `")
279 ),
280 ));
281 }
282
283 let installation = Esp32ToolchainInstallation {
284 missing: findings.missing,
285 manual: findings.manual,
286 install_espup,
287 run_espup: findings.run_espup,
288 riscv_gcc: findings.riscv_gcc,
289 helpers: CargoHelpersInstallation::new(findings.helpers),
290 };
291 if install_espup || findings.run_espup || helpers_missing {
292 Err(ToolchainError::fixable(installation))
293 } else {
294 let manual = installation.manual.join("; ");
296 Err(ToolchainError::unfixable(installation.describe(), manual))
297 }
298 }
299}
300
301impl Installation for Esp32ToolchainInstallation {
302 type Error = FailToInstallEsp32Toolchain;
303
304 async fn install(&self, host: &Host) -> Result<(), Self::Error> {
305 if self.install_espup {
306 CargoHelpersInstallation::new(vec!["espup".to_owned()])
307 .install(host)
308 .await
309 .map_err(FailToInstallEsp32Toolchain::InstallEspup)?;
310 }
311 if self.run_espup {
312 let mut args = vec!["install"];
313 if self.riscv_gcc {
314 args.push("--esp-riscv-gcc");
315 }
316 host.run("espup", args)
317 .await
318 .map_err(FailToInstallEsp32Toolchain::EspupInstall)?;
319 }
320 self.helpers.install(host).await?;
321 Ok(())
322 }
323}
324
325#[cfg(test)]
326mod tests {
327 use super::Esp32Toolchain;
328 use crate::esp32::chip::Esp32Chip;
329 use crate::toolchain::testing::TestMachine;
330 use crate::toolchain::{Toolchain, ToolchainError};
331
332 fn stage_esp_toolchain(machine: &TestMachine, xtensa_gcc: bool) {
335 for subdir in [
336 "xtensa-esp32-elf-clang/1.0/esp-clang/lib",
337 "lib/rustlib/src/rust",
338 ] {
339 machine.dir(format!("home/.rustup/toolchains/esp/{subdir}"));
340 }
341 if xtensa_gcc {
342 machine.dir("home/.rustup/toolchains/esp/xtensa-esp-elf/1.0/xtensa-esp-elf/bin");
343 }
344 }
345
346 fn cargo_machine() -> TestMachine {
350 let machine = TestMachine::new();
351 machine.install("cargo");
352 machine
353 }
354
355 #[test]
356 fn esp32_unfixable_when_esp_missing_and_no_cargo() {
357 let machine = TestMachine::new();
358 let host = machine.host(Vec::<(String, String)>::new());
359 let result = smol::block_on(Esp32Toolchain::new([Esp32Chip::Esp32S3]).check(&host));
360 match &result {
361 Err(ToolchainError::Unfixable(error)) => {
362 assert!(
363 error.suggestion().contains("cargo install espup"),
364 "the manual path must name `cargo install espup`: {}",
365 error.suggestion()
366 );
367 }
368 other => panic!("missing esp toolchain without cargo must be manual: {other:?}"),
369 }
370 }
371
372 #[test]
373 fn esp32_fixable_when_esp_missing_and_cargo_present() {
374 let machine = cargo_machine();
375 machine.install("espup");
376 machine.install("espflash");
377 machine.install("ldproxy");
378 machine.install("qemu-system-xtensa");
379 let host = machine.host(Vec::<(String, String)>::new());
380 let result = smol::block_on(Esp32Toolchain::new([Esp32Chip::Esp32S3]).check(&host));
381 assert!(
382 matches!(result, Err(ToolchainError::Fixable(_))),
383 "missing esp toolchain with espup present must be fixable: {result:?}"
384 );
385 }
386
387 #[test]
388 fn esp32_ok_when_fully_staged() {
389 let machine = cargo_machine();
390 machine.install("espflash");
391 machine.install("ldproxy");
392 machine.install("qemu-system-xtensa");
393 stage_esp_toolchain(&machine, true);
394 let host = machine.host(Vec::<(String, String)>::new());
395 smol::block_on(Esp32Toolchain::new([Esp32Chip::Esp32S3]).check(&host))
396 .expect("a fully staged esp toolchain must be ok");
397 }
398
399 #[test]
400 fn esp32_riscv_checks_espressif_tools_tree() {
401 let machine = cargo_machine();
402 machine.install("espflash");
403 machine.install("ldproxy");
404 machine.install("qemu-system-riscv32");
405 stage_esp_toolchain(&machine, false);
406 let host = machine.host(Vec::<(String, String)>::new());
408 let result = smol::block_on(Esp32Toolchain::new([Esp32Chip::Esp32C3]).check(&host));
409 match &result {
410 Err(ToolchainError::Fixable(installation)) => {
411 assert!(
412 installation.describe().contains("riscv32-esp-elf"),
413 "the missing RISC-V GCC must be named: {}",
414 installation.describe()
415 );
416 }
417 other => panic!("a missing RISC-V GCC must be fixable via espup: {other:?}"),
418 }
419 }
420
421 #[test]
422 fn esp32_riscv_ok_when_gcc_staged() {
423 let machine = cargo_machine();
424 machine.install("espflash");
425 machine.install("ldproxy");
426 machine.install("qemu-system-riscv32");
427 stage_esp_toolchain(&machine, false);
428 machine.dir("home/.espressif/tools/riscv32-esp-elf/1.0/riscv32-esp-elf/bin");
429 let host = machine.host(Vec::<(String, String)>::new());
430 smol::block_on(Esp32Toolchain::new([Esp32Chip::Esp32C3]).check(&host))
431 .expect("staged RISC-V toolchain must be ok");
432 }
433
434 #[test]
435 fn esp32_qemu_alone_is_manual() {
436 let machine = cargo_machine();
437 machine.install("espflash");
438 machine.install("ldproxy");
439 stage_esp_toolchain(&machine, true);
440 let host = machine.host(Vec::<(String, String)>::new());
441 let result = smol::block_on(Esp32Toolchain::new([Esp32Chip::Esp32S3]).check(&host));
442 match &result {
443 Err(ToolchainError::Unfixable(error)) => {
444 assert!(
445 error.suggestion().contains("QEMU"),
446 "the QEMU-only gap must name its install: {}",
447 error.suggestion()
448 );
449 }
450 other => panic!("only QEMU missing must be a manual item: {other:?}"),
451 }
452 }
453}