diff --git a/README.md b/README.md index 343b2f24..4dd3e143 100644 --- a/README.md +++ b/README.md @@ -174,17 +174,17 @@ Both auto-detect the emulator backend from the board, or accept `--emulator path, Err(e) => { return ( @@ -1443,8 +1442,7 @@ impl EmulatorRunner for QemuRunner { mcu_config.default_flash_size(), )?; - let qemu = fbuild_packages::toolchain::EspQemuXtensa::new(&self.project_dir) - .and_then(|pkg| pkg.resolve_executable())?; + let qemu = resolve_esp_qemu_for_mcu(&self.project_dir, &self.board.mcu)?; let session_dir = qemu_session_dir(&self.project_dir, &self.env_name); std::fs::create_dir_all(&session_dir)?; @@ -1716,8 +1714,28 @@ impl EmulatorRunner for SimavrRunner { } /// Check whether a given MCU is supported by the QEMU runner. +/// +/// Supported MCUs: +/// - Xtensa (`qemu-system-xtensa`): `esp32`, `esp32s3` +/// - RISC-V (`qemu-system-riscv32`): `esp32c3`, `esp32c6`, `esp32h2` fn is_qemu_supported_esp32_mcu(mcu: &str) -> bool { - mcu.eq_ignore_ascii_case("esp32") || mcu.eq_ignore_ascii_case("esp32s3") + fbuild_packages::toolchain::EspQemuArch::for_mcu(mcu).is_some() +} + +/// Resolve the Espressif QEMU executable appropriate for the given MCU. +/// +/// Picks `qemu-system-xtensa` for ESP32/ESP32-S3 and `qemu-system-riscv32` +/// for ESP32-C3/C6/H2. Returns the resolved binary path (downloading into +/// the managed fbuild cache if required). +fn resolve_esp_qemu_for_mcu(project_dir: &Path, mcu: &str) -> fbuild_core::Result { + let arch = fbuild_packages::toolchain::EspQemuArch::for_mcu(mcu).ok_or_else(|| { + fbuild_core::FbuildError::DeployFailed(format!( + "no QEMU backend available for MCU '{}'", + mcu + )) + })?; + let pkg = fbuild_packages::toolchain::EspQemu::new(project_dir, arch)?; + pkg.resolve_executable() } /// Fail fast if the board's flash mode is incompatible with QEMU (DIO only). @@ -1761,7 +1779,8 @@ pub fn select_runner( } if !is_qemu_supported_esp32_mcu(&board.mcu) { return Err(fbuild_core::FbuildError::DeployFailed(format!( - "QEMU runner currently supports ESP32 and ESP32-S3, got '{}'", + "QEMU runner currently supports ESP32, ESP32-S3 (Xtensa) and \ + ESP32-C3, ESP32-C6, ESP32-H2 (RISC-V), got '{}'", board.mcu ))); } @@ -1841,7 +1860,8 @@ pub fn select_runner( } else { Err(fbuild_core::FbuildError::DeployFailed(format!( "no emulator runner available for ESP32 MCU '{}'; \ - ESP32 and ESP32-S3 are supported via QEMU", + ESP32, ESP32-S3 (Xtensa) and ESP32-C3, ESP32-C6, ESP32-H2 (RISC-V) \ + are supported via QEMU", board.mcu ))) } @@ -2722,7 +2742,7 @@ mod tests { } #[test] - fn select_runner_explicit_qemu_rejects_esp32c3() { + fn select_runner_explicit_qemu_accepts_esp32c3() { let result = select_runner( Path::new("/tmp/test"), "esp32-c3-devkitm-1", @@ -2731,9 +2751,60 @@ mod tests { &HashMap::new(), Some("qemu"), ); + assert!( + result.is_ok(), + "QEMU should accept ESP32-C3 via qemu-system-riscv32: {:?}", + result.err() + ); + } + + #[test] + fn select_runner_explicit_qemu_accepts_esp32c6() { + let result = select_runner( + Path::new("/tmp/test"), + "esp32-c6-devkitc-1", + fbuild_core::Platform::Espressif32, + "esp32-c6-devkitc-1", + &HashMap::new(), + Some("qemu"), + ); + assert!( + result.is_ok(), + "QEMU should accept ESP32-C6 via qemu-system-riscv32: {:?}", + result.err() + ); + } + + #[test] + fn select_runner_explicit_qemu_accepts_esp32h2() { + let result = select_runner( + Path::new("/tmp/test"), + "esp32-h2-devkitm-1", + fbuild_core::Platform::Espressif32, + "esp32-h2-devkitm-1", + &HashMap::new(), + Some("qemu"), + ); + assert!( + result.is_ok(), + "QEMU should accept ESP32-H2 via qemu-system-riscv32: {:?}", + result.err() + ); + } + + #[test] + fn select_runner_explicit_qemu_rejects_esp32s2() { + let result = select_runner( + Path::new("/tmp/test"), + "esp32-s2-saola-1", + fbuild_core::Platform::Espressif32, + "esp32-s2-saola-1", + &HashMap::new(), + Some("qemu"), + ); assert!( result.is_err(), - "QEMU should reject ESP32-C3 (RISC-V, not yet supported)" + "QEMU should reject ESP32-S2 (not emulated upstream)" ); } @@ -2774,18 +2845,25 @@ mod tests { } #[test] - fn is_qemu_supported_esp32_mcu_accepts_esp32() { + fn is_qemu_supported_esp32_mcu_accepts_xtensa() { assert!(is_qemu_supported_esp32_mcu("esp32")); assert!(is_qemu_supported_esp32_mcu("ESP32")); assert!(is_qemu_supported_esp32_mcu("esp32s3")); assert!(is_qemu_supported_esp32_mcu("ESP32S3")); } + #[test] + fn is_qemu_supported_esp32_mcu_accepts_riscv() { + assert!(is_qemu_supported_esp32_mcu("esp32c3")); + assert!(is_qemu_supported_esp32_mcu("ESP32C3")); + assert!(is_qemu_supported_esp32_mcu("esp32c6")); + assert!(is_qemu_supported_esp32_mcu("esp32h2")); + } + #[test] fn is_qemu_supported_esp32_mcu_rejects_unsupported() { - assert!(!is_qemu_supported_esp32_mcu("esp32c3")); assert!(!is_qemu_supported_esp32_mcu("esp32s2")); - assert!(!is_qemu_supported_esp32_mcu("esp32h2")); assert!(!is_qemu_supported_esp32_mcu("atmega328p")); + assert!(!is_qemu_supported_esp32_mcu("esp32p4")); } } diff --git a/crates/fbuild-deploy/src/esp32.rs b/crates/fbuild-deploy/src/esp32.rs index 17bd822e..b5fa6e08 100644 --- a/crates/fbuild-deploy/src/esp32.rs +++ b/crates/fbuild-deploy/src/esp32.rs @@ -442,7 +442,10 @@ pub fn create_qemu_flash_image( /// Build the QEMU argv for ESP32-family emulation. /// /// The `mcu` parameter selects the QEMU machine type and watchdog timer -/// driver name. Supported values: `esp32`, `esp32s2`, `esp32s3`. +/// driver name. Supported values: `esp32`, `esp32s3` (via +/// `qemu-system-xtensa`) and `esp32c3`, `esp32c6`, `esp32h2` (via +/// `qemu-system-riscv32`). Callers are responsible for launching the +/// matching QEMU binary; this function only emits the argv. pub fn build_qemu_args( mcu: &str, flash_image: &Path, diff --git a/crates/fbuild-packages/src/toolchain/esp_qemu.rs b/crates/fbuild-packages/src/toolchain/esp_qemu.rs index 35748491..7b87a167 100644 --- a/crates/fbuild-packages/src/toolchain/esp_qemu.rs +++ b/crates/fbuild-packages/src/toolchain/esp_qemu.rs @@ -1,8 +1,13 @@ -//! Espressif QEMU for Xtensa (`qemu-system-xtensa`) used for ESP32-S3 emulation. +//! Espressif QEMU (`qemu-system-xtensa` / `qemu-system-riscv32`) used for +//! ESP32-family emulation. //! -//! Resolution order: -//! 1. `FBUILD_QEMU_XTENSA_PATH` -//! 2. `qemu-system-xtensa` already on `PATH` +//! Two binaries are supported: +//! - **Xtensa**: `qemu-system-xtensa` for ESP32 and ESP32-S3 +//! - **RISC-V**: `qemu-system-riscv32` for ESP32-C3, ESP32-C6, ESP32-H2 +//! +//! Resolution order (per architecture): +//! 1. `FBUILD_QEMU_XTENSA_PATH` / `FBUILD_QEMU_RISCV32_PATH` +//! 2. Binary already on `PATH` //! 3. Managed `fbuild` cache install //! 4. Existing ESP-IDF tools install under `IDF_TOOLS_PATH` / default `.espressif` //! 5. Managed download into the `fbuild` cache @@ -14,90 +19,166 @@ use fbuild_core::{FbuildError, Result}; use crate::{CacheSubdir, Package, PackageBase, PackageInfo}; const QEMU_RELEASE_TAG: &str = "esp-develop-9.2.2-20250817"; -const QEMU_ARCHIVE_STEM: &str = "qemu-xtensa-softmmu-esp_develop_9.2.2_20250817"; +const QEMU_XTENSA_ARCHIVE_STEM: &str = "qemu-xtensa-softmmu-esp_develop_9.2.2_20250817"; +const QEMU_RISCV32_ARCHIVE_STEM: &str = "qemu-riscv32-softmmu-esp_develop_9.2.2_20250817"; + +/// Which Espressif QEMU binary this package represents. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EspQemuArch { + Xtensa, + Riscv32, +} + +impl EspQemuArch { + /// Pick the QEMU architecture for a given ESP32-family MCU string. + /// + /// Returns `None` for MCUs that do not have a supported Espressif QEMU + /// build (for example, `esp32s2` is not emulated by upstream QEMU). + pub fn for_mcu(mcu: &str) -> Option { + match mcu.to_ascii_lowercase().as_str() { + "esp32" | "esp32s3" => Some(Self::Xtensa), + "esp32c3" | "esp32c6" | "esp32h2" => Some(Self::Riscv32), + _ => None, + } + } + + fn package_name(self) -> &'static str { + match self { + Self::Xtensa => "esp-qemu-xtensa", + Self::Riscv32 => "esp-qemu-riscv32", + } + } + + fn subdir_name(self) -> &'static str { + match self { + Self::Xtensa => "qemu-xtensa", + Self::Riscv32 => "qemu-riscv32", + } + } + + fn archive_stem(self) -> &'static str { + match self { + Self::Xtensa => QEMU_XTENSA_ARCHIVE_STEM, + Self::Riscv32 => QEMU_RISCV32_ARCHIVE_STEM, + } + } -pub struct EspQemuXtensa { + fn env_var(self) -> &'static str { + match self { + Self::Xtensa => "FBUILD_QEMU_XTENSA_PATH", + Self::Riscv32 => "FBUILD_QEMU_RISCV32_PATH", + } + } + + fn binary_name(self) -> &'static str { + match (self, cfg!(windows)) { + (Self::Xtensa, true) => "qemu-system-xtensa.exe", + (Self::Xtensa, false) => "qemu-system-xtensa", + (Self::Riscv32, true) => "qemu-system-riscv32.exe", + (Self::Riscv32, false) => "qemu-system-riscv32", + } + } +} + +/// Package handle for an Espressif QEMU build (Xtensa or RISC-V). +pub struct EspQemu { base: PackageBase, + arch: EspQemuArch, } -impl EspQemuXtensa { - pub fn new(project_dir: &Path) -> Result { - let pkg = platform_package()?; +impl EspQemu { + pub fn new(project_dir: &Path, arch: EspQemuArch) -> Result { + let pkg = platform_package(arch)?; let url = format!( "https://github.com/espressif/qemu/releases/download/{}/{}-{}.tar.xz", - QEMU_RELEASE_TAG, QEMU_ARCHIVE_STEM, pkg.archive_suffix + QEMU_RELEASE_TAG, + arch.archive_stem(), + pkg.archive_suffix ); Ok(Self { base: PackageBase::new( - "esp-qemu-xtensa", + arch.package_name(), QEMU_RELEASE_TAG, &url, - "qemu-xtensa", + arch.subdir_name(), Some(pkg.sha256), CacheSubdir::Toolchains, project_dir, ), + arch, }) } + pub fn arch(&self) -> EspQemuArch { + self.arch + } + pub fn resolve_executable(&self) -> Result { - if let Ok(raw) = std::env::var("FBUILD_QEMU_XTENSA_PATH") { + if let Ok(raw) = std::env::var(self.arch.env_var()) { let path = PathBuf::from(raw); - let path = validate_qemu_path(path, "FBUILD_QEMU_XTENSA_PATH")?; + let path = validate_qemu_path(path, self.arch.env_var())?; hydrate_windows_runtime(&path)?; validate_windows_runtime(&path)?; return Ok(path); } - if let Some(path) = find_on_path(binary_name()) { + if let Some(path) = find_on_path(self.arch.binary_name()) { hydrate_windows_runtime(&path)?; validate_windows_runtime(&path)?; return Ok(path); } if self.is_installed() { - let path = find_qemu_binary(&self.base.install_path())?; + let path = find_qemu_binary(&self.base.install_path(), self.arch)?; hydrate_windows_runtime(&path)?; validate_windows_runtime(&path)?; return Ok(path); } - if let Some(path) = find_existing_idf_qemu() { + if let Some(path) = find_existing_idf_qemu(self.arch) { hydrate_windows_runtime(&path)?; validate_windows_runtime(&path)?; return Ok(path); } let _ = self.ensure_installed()?; - let path = find_qemu_binary(&self.base.install_path())?; + let path = find_qemu_binary(&self.base.install_path(), self.arch)?; hydrate_windows_runtime(&path)?; validate_windows_runtime(&path)?; Ok(path) } - fn validate_install(install_dir: &Path) -> Result<()> { - let _ = find_qemu_binary(install_dir)?; + fn validate_install_xtensa(install_dir: &Path) -> Result<()> { + let _ = find_qemu_binary(install_dir, EspQemuArch::Xtensa)?; + Ok(()) + } + + fn validate_install_riscv32(install_dir: &Path) -> Result<()> { + let _ = find_qemu_binary(install_dir, EspQemuArch::Riscv32)?; Ok(()) } } -impl Package for EspQemuXtensa { +impl Package for EspQemu { fn ensure_installed(&self) -> Result { if self.is_installed() { - return qemu_root(&self.base.install_path()); + return qemu_root(&self.base.install_path(), self.arch); } - let install_path = - crate::block_on_package_future(self.base.staged_install(Self::validate_install))?; + let validate = match self.arch { + EspQemuArch::Xtensa => Self::validate_install_xtensa, + EspQemuArch::Riscv32 => Self::validate_install_riscv32, + }; + let install_path = crate::block_on_package_future(self.base.staged_install(validate))?; - qemu_root(&install_path) + qemu_root(&install_path, self.arch) } fn is_installed(&self) -> bool { if !self.base.is_cached() { return false; } - find_qemu_binary(&self.base.install_path()).is_ok() + find_qemu_binary(&self.base.install_path(), self.arch).is_ok() } fn get_info(&self) -> PackageInfo { @@ -105,12 +186,73 @@ impl Package for EspQemuXtensa { } } +/// Backwards-compatible wrapper: Espressif QEMU for Xtensa. +pub struct EspQemuXtensa(EspQemu); + +impl EspQemuXtensa { + pub fn new(project_dir: &Path) -> Result { + Ok(Self(EspQemu::new(project_dir, EspQemuArch::Xtensa)?)) + } + + pub fn resolve_executable(&self) -> Result { + self.0.resolve_executable() + } +} + +impl Package for EspQemuXtensa { + fn ensure_installed(&self) -> Result { + self.0.ensure_installed() + } + + fn is_installed(&self) -> bool { + self.0.is_installed() + } + + fn get_info(&self) -> PackageInfo { + self.0.get_info() + } +} + +/// Convenience wrapper: Espressif QEMU for RISC-V (ESP32-C3/C6/H2). +pub struct EspQemuRiscv32(EspQemu); + +impl EspQemuRiscv32 { + pub fn new(project_dir: &Path) -> Result { + Ok(Self(EspQemu::new(project_dir, EspQemuArch::Riscv32)?)) + } + + pub fn resolve_executable(&self) -> Result { + self.0.resolve_executable() + } +} + +impl Package for EspQemuRiscv32 { + fn ensure_installed(&self) -> Result { + self.0.ensure_installed() + } + + fn is_installed(&self) -> bool { + self.0.is_installed() + } + + fn get_info(&self) -> PackageInfo { + self.0.get_info() + } +} + struct PlatformPackage { archive_suffix: &'static str, sha256: &'static str, } -fn platform_package() -> Result { +fn platform_package(arch: EspQemuArch) -> Result { + match arch { + EspQemuArch::Xtensa => xtensa_platform_package(), + EspQemuArch::Riscv32 => riscv32_platform_package(), + } +} + +fn xtensa_platform_package() -> Result { if cfg!(target_os = "windows") && cfg!(target_arch = "x86_64") { Ok(PlatformPackage { archive_suffix: "x86_64-w64-mingw32", @@ -138,18 +280,45 @@ fn platform_package() -> Result { }) } else { Err(FbuildError::PackageError(format!( - "native QEMU is not supported on {}-{}", + "native QEMU (xtensa) is not supported on {}-{}", std::env::consts::OS, std::env::consts::ARCH ))) } } -fn binary_name() -> &'static str { - if cfg!(windows) { - "qemu-system-xtensa.exe" +fn riscv32_platform_package() -> Result { + if cfg!(target_os = "windows") && cfg!(target_arch = "x86_64") { + Ok(PlatformPackage { + archive_suffix: "x86_64-w64-mingw32", + sha256: "9474015f24d27acb7516955ec932e5307226bd9d6652cdc870793ed36010ab73", + }) + } else if cfg!(target_os = "linux") && cfg!(target_arch = "x86_64") { + Ok(PlatformPackage { + archive_suffix: "x86_64-linux-gnu", + sha256: "373b37a68bae3ef441ead24a7bfc950fcbfc274cbdd2b628fc6915f179eb1d8e", + }) + } else if cfg!(target_os = "linux") && cfg!(target_arch = "aarch64") { + Ok(PlatformPackage { + archive_suffix: "aarch64-linux-gnu", + sha256: "f907a54313058f8a9681d2f48257d518950ff98bcd5a319194b4bee7c10cf223", + }) + } else if cfg!(target_os = "macos") && cfg!(target_arch = "x86_64") { + Ok(PlatformPackage { + archive_suffix: "x86_64-apple-darwin", + sha256: "820028ee7cd2dd8fe8cd8ca5519ab6e792d15fea9367c4525cf63c0f707c0b1f", + }) + } else if cfg!(target_os = "macos") && cfg!(target_arch = "aarch64") { + Ok(PlatformPackage { + archive_suffix: "aarch64-apple-darwin", + sha256: "234690b6fa7c1d5dfe3dbb2bdd0c2810755e7c98999a9f21c389a6046b7eb76d", + }) } else { - "qemu-system-xtensa" + Err(FbuildError::PackageError(format!( + "native QEMU (riscv32) is not supported on {}-{}", + std::env::consts::OS, + std::env::consts::ARCH + ))) } } @@ -164,8 +333,8 @@ fn validate_qemu_path(path: PathBuf, source: &str) -> Result { ))) } -fn find_qemu_binary(root: &Path) -> Result { - let file_name = binary_name(); +fn find_qemu_binary(root: &Path, arch: EspQemuArch) -> Result { + let file_name = arch.binary_name(); let direct = root.join(file_name); if direct.is_file() { return Ok(direct); @@ -196,13 +365,14 @@ fn find_qemu_binary(root: &Path) -> Result { } Err(FbuildError::PackageError(format!( - "qemu-system-xtensa not found in {}", + "{} not found in {}", + file_name, root.display() ))) } -fn qemu_root(install_dir: &Path) -> Result { - let exe = find_qemu_binary(install_dir)?; +fn qemu_root(install_dir: &Path, arch: EspQemuArch) -> Result { + let exe = find_qemu_binary(install_dir, arch)?; let in_bin_dir = exe .parent() .and_then(|p| p.file_name()) @@ -230,9 +400,10 @@ fn find_on_path(binary: &str) -> Option { None } -fn find_existing_idf_qemu() -> Option { +fn find_existing_idf_qemu(arch: EspQemuArch) -> Option { + let tools_subdir = arch.subdir_name(); for root in candidate_idf_tools_roots() { - let qemu_dir = root.join("tools").join("qemu-xtensa"); + let qemu_dir = root.join("tools").join(tools_subdir); if !qemu_dir.is_dir() { continue; } @@ -241,7 +412,7 @@ fn find_existing_idf_qemu() -> Option { versions.sort(); versions.reverse(); for version_dir in versions { - if let Ok(path) = find_qemu_binary(&version_dir) { + if let Ok(path) = find_qemu_binary(&version_dir, arch) { return Some(path); } } @@ -414,13 +585,41 @@ mod tests { use super::*; #[test] - fn find_qemu_binary_direct_bin() { + fn arch_for_mcu_maps_xtensa_and_riscv() { + assert_eq!(EspQemuArch::for_mcu("esp32"), Some(EspQemuArch::Xtensa)); + assert_eq!(EspQemuArch::for_mcu("ESP32"), Some(EspQemuArch::Xtensa)); + assert_eq!(EspQemuArch::for_mcu("esp32s3"), Some(EspQemuArch::Xtensa)); + assert_eq!(EspQemuArch::for_mcu("esp32c3"), Some(EspQemuArch::Riscv32)); + assert_eq!(EspQemuArch::for_mcu("esp32c6"), Some(EspQemuArch::Riscv32)); + assert_eq!(EspQemuArch::for_mcu("esp32h2"), Some(EspQemuArch::Riscv32)); + assert_eq!(EspQemuArch::for_mcu("esp32s2"), None); + assert_eq!(EspQemuArch::for_mcu("stm32"), None); + } + + #[test] + fn find_qemu_binary_direct_bin_xtensa() { let tmp = tempfile::TempDir::new().unwrap(); let bin = tmp.path().join("bin"); std::fs::create_dir_all(&bin).unwrap(); - let exe = bin.join(binary_name()); + let exe = bin.join(EspQemuArch::Xtensa.binary_name()); std::fs::write(&exe, b"").unwrap(); - assert_eq!(find_qemu_binary(tmp.path()).unwrap(), exe); + assert_eq!( + find_qemu_binary(tmp.path(), EspQemuArch::Xtensa).unwrap(), + exe + ); + } + + #[test] + fn find_qemu_binary_direct_bin_riscv() { + let tmp = tempfile::TempDir::new().unwrap(); + let bin = tmp.path().join("bin"); + std::fs::create_dir_all(&bin).unwrap(); + let exe = bin.join(EspQemuArch::Riscv32.binary_name()); + std::fs::write(&exe, b"").unwrap(); + assert_eq!( + find_qemu_binary(tmp.path(), EspQemuArch::Riscv32).unwrap(), + exe + ); } #[test] @@ -429,10 +628,13 @@ mod tests { let nested = tmp.path().join("qemu"); let bin = nested.join("bin"); std::fs::create_dir_all(&bin).unwrap(); - let exe = bin.join(binary_name()); + let exe = bin.join(EspQemuArch::Xtensa.binary_name()); std::fs::write(&exe, b"").unwrap(); - assert_eq!(find_qemu_binary(tmp.path()).unwrap(), exe); - assert_eq!(qemu_root(tmp.path()).unwrap(), nested); + assert_eq!( + find_qemu_binary(tmp.path(), EspQemuArch::Xtensa).unwrap(), + exe + ); + assert_eq!(qemu_root(tmp.path(), EspQemuArch::Xtensa).unwrap(), nested); } #[test] @@ -451,7 +653,7 @@ mod tests { let runtime_dir = tmp.path().join("runtime"); std::fs::create_dir_all(&exe_dir).unwrap(); std::fs::create_dir_all(&runtime_dir).unwrap(); - let exe = exe_dir.join(binary_name()); + let exe = exe_dir.join(EspQemuArch::Xtensa.binary_name()); std::fs::write(&exe, b"").unwrap(); std::fs::write(runtime_dir.join("libiconv-2.dll"), b"").unwrap(); let old_path = std::env::var_os("PATH"); @@ -478,7 +680,7 @@ mod tests { let runtime_dir = tmp.path().join("runtime"); std::fs::create_dir_all(&exe_dir).unwrap(); std::fs::create_dir_all(&runtime_dir).unwrap(); - let exe = exe_dir.join(binary_name()); + let exe = exe_dir.join(EspQemuArch::Xtensa.binary_name()); std::fs::write(&exe, b"").unwrap(); let iconv = runtime_dir.join("libiconv-2.dll"); std::fs::write(&iconv, b"iconv").unwrap(); diff --git a/crates/fbuild-packages/src/toolchain/mod.rs b/crates/fbuild-packages/src/toolchain/mod.rs index 112c77ab..938f9264 100644 --- a/crates/fbuild-packages/src/toolchain/mod.rs +++ b/crates/fbuild-packages/src/toolchain/mod.rs @@ -19,6 +19,6 @@ pub use esp32::Esp32Toolchain; pub use esp8266::Esp8266Toolchain; #[cfg(windows)] pub use esp_qemu::build_windows_qemu_path_env; -pub use esp_qemu::EspQemuXtensa; +pub use esp_qemu::{EspQemu, EspQemuArch, EspQemuRiscv32, EspQemuXtensa}; pub use riscv::RiscvToolchain; pub use rp2040_pqt::Rp2040PqtToolchain;