diff --git a/crates/fbuild-build-esp/src/esp32/fixups.rs b/crates/fbuild-build-esp/src/esp32/fixups.rs new file mode 100644 index 00000000..4a45bc26 --- /dev/null +++ b/crates/fbuild-build-esp/src/esp32/fixups.rs @@ -0,0 +1,277 @@ +//! Pure fixups that adapt an [`Esp32McuConfig`] to the packages a build +//! actually resolved. +//! +//! The embedded JSON recipes target the newest toolchain and SDK. Older +//! platforms pair them with older compilers and SDKs, so a few flags must go +//! or change. Each rule is a side-effect-free `config -> config` function; the +//! orchestrator applies a composed fixup once, as one line: +//! +//! ```ignore +//! let mcu_config = fixups::for_toolchain(mcu_config, &toolchain_info); +//! let mcu_config = fixups::for_sdk_ld_flags(mcu_config, &sdk_ld_flags); +//! ``` + +use fbuild_packages::PackageInfo; + +use super::mcu_config::Esp32McuConfig; +use crate::compiler::ProfileFlags; + +type Rule = fn(Esp32McuConfig) -> Esp32McuConfig; + +/// Adapt the recipe to the selected toolchain package. +/// +/// - GCC < 14: the paired SDKs (pioarduino 51.x/53.x, Arduino 3.0/3.1) do not +/// define `__dso_handle` for `-fuse-cxa-atexit`-generated references, on +/// Xtensa and RISC-V alike. +/// - Per-MCU Xtensa packages (GCC 8 and 12) reject the atomics switch. +/// - Per-MCU Xtensa GCC 8 (official `espressif32` 6.x/7.x) needs its own +/// linker recipe, older language standards, and no LTO. +pub fn for_toolchain(config: Esp32McuConfig, toolchain: &PackageInfo) -> Esp32McuConfig { + let per_mcu_xtensa = is_per_mcu_xtensa(&toolchain.name); + let config = apply_if(is_before_gcc14(&toolchain.version), config, drop_cxa_atexit); + let config = apply_if(per_mcu_xtensa, config, drop_hardware_atomics); + apply_if( + per_mcu_xtensa && is_gcc8(&toolchain.version), + config, + legacy_gcc8, + ) +} + +/// Drop LTO when the SDK links with `-fno-lto`: objects compiled with LTO +/// would not link. +pub fn for_sdk_ld_flags(config: Esp32McuConfig, sdk_ld_flags: &[String]) -> Esp32McuConfig { + apply_if( + sdk_ld_flags.iter().any(|flag| flag == "-fno-lto"), + config, + without_lto, + ) +} + +fn apply_if(condition: bool, config: Esp32McuConfig, rule: Rule) -> Esp32McuConfig { + if condition { rule(config) } else { config } +} + +fn gcc_major(version: &str) -> Option { + version.split('.').next()?.parse().ok() +} + +fn is_before_gcc14(version: &str) -> bool { + gcc_major(version).is_some_and(|major| major < 14) +} + +fn is_gcc8(version: &str) -> bool { + version.starts_with("8.") +} + +fn is_per_mcu_xtensa(package_name: &str) -> bool { + package_name.starts_with("toolchain-xtensa-esp32") +} + +fn drop_cxa_atexit(mut config: Esp32McuConfig) -> Esp32McuConfig { + config.compiler_flags.cxx = without(config.compiler_flags.cxx, "-fuse-cxa-atexit"); + config +} + +fn drop_hardware_atomics(mut config: Esp32McuConfig) -> Esp32McuConfig { + config.compiler_flags.common = + without(config.compiler_flags.common, "-mdisable-hardware-atomics"); + config +} + +fn legacy_gcc8(config: Esp32McuConfig) -> Esp32McuConfig { + without_lto(gcc8_language_standards(gcc8_linker_recipe(config))) +} + +/// Swap in the MCU's `legacy_gcc8` linker flags and scripts, if it has them. +fn gcc8_linker_recipe(mut config: Esp32McuConfig) -> Esp32McuConfig { + if let Some(recipe) = &config.legacy_gcc8 { + config.linker_flags = recipe.linker_flags.clone(); + config.linker_scripts = recipe.linker_scripts.clone(); + } + config +} + +/// Downgrade the recipe's `gnu17`/`gnu++2b` to the legacy GCC 8 `gnu99`/`gnu++11`. +fn gcc8_language_standards(mut config: Esp32McuConfig) -> Esp32McuConfig { + config.compiler_flags.c = replaced(config.compiler_flags.c, "-std=gnu17", "-std=gnu99"); + config.compiler_flags.cxx = replaced(config.compiler_flags.cxx, "-std=gnu++2b", "-std=gnu++11"); + config +} + +/// Remove LTO-related flags from every profile. +fn without_lto(mut config: Esp32McuConfig) -> Esp32McuConfig { + config.profiles = config + .profiles + .into_iter() + .map(|(name, profile)| { + let profile = ProfileFlags { + compile_flags: without_lto_flags(profile.compile_flags), + link_flags: without_lto_flags(profile.link_flags), + }; + (name, profile) + }) + .collect(); + config +} + +fn without_lto_flags(flags: Vec) -> Vec { + flags + .into_iter() + .filter(|flag| !flag.contains("lto") && flag != "-fuse-linker-plugin") + .collect() +} + +fn without(flags: Vec, unwanted: &str) -> Vec { + flags.into_iter().filter(|flag| flag != unwanted).collect() +} + +fn replaced(flags: Vec, from: &str, to: &str) -> Vec { + flags + .into_iter() + .map(|flag| if flag == from { to.to_string() } else { flag }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::esp32::mcu_config::get_mcu_config; + + fn toolchain(name: &str, version: &str) -> PackageInfo { + PackageInfo { + name: name.into(), + version: version.into(), + url: String::new(), + install_path: Default::default(), + checksum: None, + installed_bytes: None, + } + } + + fn has(flags: &[String], flag: &str) -> bool { + flags.iter().any(|f| f == flag) + } + + fn has_lto(config: &Esp32McuConfig) -> bool { + config.profiles.values().any(|p| { + p.compile_flags + .iter() + .chain(&p.link_flags) + .any(|f| f.contains("lto") || f == "-fuse-linker-plugin") + }) + } + + #[test] + fn gcc_major_parses_leading_component() { + assert_eq!(gcc_major("13.2.0+20240530"), Some(13)); + assert_eq!(gcc_major("8.4.0+2021r2-patch5"), Some(8)); + assert_eq!(gcc_major("riscv32-esp-elf-14.2.0_20241119"), None); + assert!(is_before_gcc14("12.2.0+20230208")); + assert!(!is_before_gcc14("14.2.0")); + assert!(!is_before_gcc14("riscv32-esp-elf-14.2.0_20241119")); + } + + #[test] + fn per_mcu_xtensa_toolchain_uses_gcc12_compatible_flags() { + let base = get_mcu_config("esp32s3").unwrap(); + let legacy = for_toolchain( + base.clone(), + &toolchain("toolchain-xtensa-esp32s3", "12.2.0+20230208"), + ); + let unified = for_toolchain(base, &toolchain("toolchain-xtensa-esp-elf", "14.2.0")); + + assert!(!has( + &legacy.compiler_flags.common, + "-mdisable-hardware-atomics" + )); + assert!(!has(&legacy.compiler_flags.cxx, "-fuse-cxa-atexit")); + assert!(has( + &unified.compiler_flags.common, + "-mdisable-hardware-atomics" + )); + assert!(has(&unified.compiler_flags.cxx, "-fuse-cxa-atexit")); + } + + #[test] + fn pre_gcc14_unified_toolchain_drops_cxa_atexit() { + // pioarduino 53.x: unified registry toolchains at GCC 13.2 on both + // architectures, with an SDK that lacks `__dso_handle`. + for (mcu, package) in [ + ("esp32", "toolchain-xtensa-esp-elf"), + ("esp32c3", "toolchain-riscv32-esp"), + ] { + let config = for_toolchain( + get_mcu_config(mcu).unwrap(), + &toolchain(package, "13.2.0+20240530"), + ); + assert!( + !has(&config.compiler_flags.cxx, "-fuse-cxa-atexit"), + "{mcu} kept -fuse-cxa-atexit on GCC 13" + ); + } + } + + #[test] + fn unparseable_version_keeps_recipe() { + // pioarduino 55.x URL toolchain: version is not a bare GCC version. + let base = get_mcu_config("esp32c6").unwrap(); + let config = for_toolchain( + base.clone(), + &toolchain("esp32-riscv-gcc", "riscv32-esp-elf-14.2.0_20241119"), + ); + assert_eq!(config.compiler_flags.cxx, base.compiler_flags.cxx); + assert_eq!(config.compiler_flags.common, base.compiler_flags.common); + } + + #[test] + fn platformio_gcc8_uses_supported_cpp_standard() { + let config = for_toolchain( + get_mcu_config("esp32s3").unwrap(), + &toolchain("toolchain-xtensa-esp32s3", "8.4.0+2021r2-patch5"), + ); + assert!(has(&config.compiler_flags.c, "-std=gnu99")); + assert!(has(&config.compiler_flags.cxx, "-std=gnu++11")); + assert!(!has(&config.compiler_flags.cxx, "-std=gnu++2b")); + assert!(has(&config.linker_flags, "-fno-lto")); + assert!(!has(&config.linker_flags, "-Wl,--no-warn-rwx-segments")); + assert!(has(&config.linker_scripts, "esp32s3.rom.newlib-time.ld")); + assert!(!has_lto(&config)); + } + + #[test] + fn gcc8_rules_need_a_per_mcu_xtensa_package() { + let base = get_mcu_config("esp32s3").unwrap(); + let config = for_toolchain( + base.clone(), + &toolchain("toolchain-xtensa-esp-elf", "8.4.0"), + ); + assert_eq!(config.linker_flags, base.linker_flags); + assert_eq!(config.compiler_flags.c, base.compiler_flags.c); + assert_eq!(has_lto(&config), has_lto(&base)); + } + + #[test] + fn sdk_fno_lto_strips_profile_lto() { + let base = get_mcu_config("esp32s3").unwrap(); + let kept = for_sdk_ld_flags(base.clone(), &["-Wl,--gc-sections".into()]); + let stripped = for_sdk_ld_flags(base.clone(), &["-fno-lto".into()]); + assert_eq!(has_lto(&kept), has_lto(&base)); + assert!(!has_lto(&stripped)); + assert_eq!(stripped.profiles.len(), base.profiles.len()); + } + + #[test] + fn rules_are_independent() { + let base = get_mcu_config("esp32s3").unwrap(); + let dropped = drop_hardware_atomics(base.clone()); + assert!(!has( + &dropped.compiler_flags.common, + "-mdisable-hardware-atomics" + )); + assert_eq!(dropped.compiler_flags.cxx, base.compiler_flags.cxx); + + let standards = gcc8_language_standards(base.clone()); + assert_eq!(standards.linker_flags, base.linker_flags); + assert_eq!(standards.compiler_flags.common, base.compiler_flags.common); + } +} diff --git a/crates/fbuild-build-esp/src/esp32/mcu_config.rs b/crates/fbuild-build-esp/src/esp32/mcu_config.rs index 61f77f8c..9de1b7b4 100644 --- a/crates/fbuild-build-esp/src/esp32/mcu_config.rs +++ b/crates/fbuild-build-esp/src/esp32/mcu_config.rs @@ -85,38 +85,6 @@ pub struct LegacyGcc8Recipe { } impl Esp32McuConfig { - /// Drop GCC 14 recipe flags when the platform selects an older per-MCU - /// Xtensa toolchain. The GCC 12 compiler rejects the atomics switch, and - /// its matching Arduino 3.0 SDK does not define `__dso_handle` for - /// `-fuse-cxa-atexit`-generated references. - pub fn adapt_to_toolchain(&mut self, package_name: &str, package_version: &str) { - if package_name.starts_with("toolchain-xtensa-esp32") { - self.compiler_flags - .common - .retain(|flag| flag != "-mdisable-hardware-atomics"); - self.compiler_flags - .cxx - .retain(|flag| flag != "-fuse-cxa-atexit"); - if package_version.starts_with("8.") { - if let Some(recipe) = &self.legacy_gcc8 { - self.linker_flags = recipe.linker_flags.clone(); - self.linker_scripts = recipe.linker_scripts.clone(); - } - for flag in &mut self.compiler_flags.c { - if flag == "-std=gnu17" { - *flag = "-std=gnu99".into(); - } - } - for flag in &mut self.compiler_flags.cxx { - if flag == "-std=gnu++2b" { - *flag = "-std=gnu++11".into(); - } - } - self.disable_lto(); - } - } - } - /// Whether this MCU uses RISC-V architecture. pub fn is_riscv(&self) -> bool { self.architecture.starts_with("riscv") @@ -225,21 +193,6 @@ impl Esp32McuConfig { .map(|(old, new)| format!("-D{}={}", old, new)) .collect() } - - /// Remove LTO-related flags from all profiles. - /// - /// Called when the SDK specifies `-fno-lto` in its linker flags, meaning - /// objects must not be compiled with LTO. - pub fn disable_lto(&mut self) { - for profile in self.profiles.values_mut() { - profile - .compile_flags - .retain(|f| !f.contains("lto") && f != "-fuse-linker-plugin"); - profile - .link_flags - .retain(|f| !f.contains("lto") && f != "-fuse-linker-plugin"); - } - } } /// Convert a flash size in bytes to an esptool-compatible size string (e.g. "4MB"). @@ -792,61 +745,4 @@ mod tests { ); } } - - #[test] - fn per_mcu_xtensa_toolchain_uses_gcc12_compatible_flags() { - let mut legacy = get_mcu_config("esp32s3").unwrap(); - let mut unified = legacy.clone(); - legacy.adapt_to_toolchain("toolchain-xtensa-esp32s3", "12.2.0+20230208"); - unified.adapt_to_toolchain("toolchain-xtensa-esp-elf", "14.2.0"); - - assert!( - !legacy - .compiler_flags - .common - .iter() - .any(|flag| flag == "-mdisable-hardware-atomics") - ); - assert!( - !legacy - .compiler_flags - .cxx - .iter() - .any(|flag| flag == "-fuse-cxa-atexit") - ); - assert!( - unified - .compiler_flags - .common - .iter() - .any(|flag| flag == "-mdisable-hardware-atomics") - ); - assert!( - unified - .compiler_flags - .cxx - .iter() - .any(|flag| flag == "-fuse-cxa-atexit") - ); - } - - #[test] - fn platformio_gcc8_uses_supported_cpp_standard() { - let mut config = get_mcu_config("esp32s3").unwrap(); - config.adapt_to_toolchain("toolchain-xtensa-esp32s3", "8.4.0+2021r2-patch5"); - assert!(config.compiler_flags.c.contains(&"-std=gnu99".into())); - assert!(config.compiler_flags.cxx.contains(&"-std=gnu++11".into())); - assert!(!config.compiler_flags.cxx.contains(&"-std=gnu++2b".into())); - assert!(config.linker_flags.contains(&"-fno-lto".into())); - assert!( - !config - .linker_flags - .contains(&"-Wl,--no-warn-rwx-segments".into()) - ); - assert!( - config - .linker_scripts - .contains(&"esp32s3.rom.newlib-time.ld".into()) - ); - } } diff --git a/crates/fbuild-build-esp/src/esp32/mod.rs b/crates/fbuild-build-esp/src/esp32/mod.rs index 369efed3..bdbc4142 100644 --- a/crates/fbuild-build-esp/src/esp32/mod.rs +++ b/crates/fbuild-build-esp/src/esp32/mod.rs @@ -2,6 +2,7 @@ pub mod esp32_compiler; pub mod esp32_linker; +pub mod fixups; pub mod mcu_config; pub mod orchestrator; pub(crate) mod size_report; diff --git a/crates/fbuild-build-esp/src/esp32/orchestrator/build.rs b/crates/fbuild-build-esp/src/esp32/orchestrator/build.rs index c7a8d4f0..89ecd079 100644 --- a/crates/fbuild-build-esp/src/esp32/orchestrator/build.rs +++ b/crates/fbuild-build-esp/src/esp32/orchestrator/build.rs @@ -11,6 +11,7 @@ use fbuild_packages::Framework; use super::super::esp32_compiler::Esp32Compiler; use super::super::esp32_linker::Esp32Linker; +use super::super::fixups; use super::super::mcu_config::get_mcu_config; use super::Esp32Orchestrator; use super::boot_artifacts::prepare_boot_artifacts; @@ -69,7 +70,7 @@ impl BuildOrchestrator for Esp32Orchestrator { ); // 3. Load MCU config from embedded JSON - let mut mcu_config = get_mcu_config(&ctx.board.mcu)?; + let mcu_config = get_mcu_config(&ctx.board.mcu)?; tracing::info!( "ESP32 build: {} ({}, {})", @@ -102,7 +103,7 @@ impl BuildOrchestrator for Esp32Orchestrator { &framework, &ctx.board.mcu, )); - mcu_config.adapt_to_toolchain(&toolchain_info.name, &toolchain_info.version); + let mcu_config = fixups::for_toolchain(mcu_config, &toolchain_info); let _toolchain_cache_dir = fbuild_packages::Package::get_info(&toolchain).install_path; let _framework_cache_dir = fbuild_packages::Package::get_info(&framework).install_path; // Aliases for build dirs (already set up by BuildContext::new()) @@ -119,9 +120,7 @@ impl BuildOrchestrator for Esp32Orchestrator { let sdk_ld_flags = framework.get_sdk_ld_flags(&sdk_variant); let sdk_defines = framework.get_sdk_defines(&sdk_variant); - if sdk_ld_flags.iter().any(|f| f == "-fno-lto") { - mcu_config.disable_lto(); - } + let mcu_config = fixups::for_sdk_ld_flags(mcu_config, &sdk_ld_flags); let embed_files = ctx.config.get_embed_files(¶ms.env_name)?; let embed_txtfiles = ctx.config.get_embed_txtfiles(¶ms.env_name)?; @@ -294,9 +293,7 @@ impl BuildOrchestrator for Esp32Orchestrator { // If SDK specifies -fno-lto, disable LTO in MCU config profiles to avoid // compiling objects with LTO that the linker can't handle. - if sdk_ld_flags.iter().any(|f| f == "-fno-lto") { - mcu_config.disable_lto(); - } + let mcu_config = fixups::for_sdk_ld_flags(mcu_config, &sdk_ld_flags); // 8.5. Library dependencies // diff --git a/crates/fbuild-build-esp/src/esp32/orchestrator/packages.rs b/crates/fbuild-build-esp/src/esp32/orchestrator/packages.rs index 325ee0e0..4ac12528 100644 --- a/crates/fbuild-build-esp/src/esp32/orchestrator/packages.rs +++ b/crates/fbuild-build-esp/src/esp32/orchestrator/packages.rs @@ -277,12 +277,31 @@ fn primary_toolchain_name(is_riscv: bool) -> &'static str { } } -fn selected_toolchain_name(mcu_config: &Esp32McuConfig, unified: bool) -> String { - if !unified && !mcu_config.is_riscv() { - format!("toolchain-xtensa-{}", mcu_config.mcu) - } else { - primary_toolchain_name(mcu_config.is_riscv()).to_string() - } +/// The MCU's own toolchain package. Each Xtensa MCU has one; RISC-V MCUs +/// share the unified package and have none. +fn per_mcu_toolchain_name(mcu_config: &Esp32McuConfig) -> Option { + (!mcu_config.is_riscv()).then(|| format!("toolchain-xtensa-{}", mcu_config.mcu)) +} + +/// The toolchain package the platform actually declares. +/// +/// A declared per-MCU package wins: pioarduino 51.x declares only those, and +/// the official PlatformIO `espressif32` 6.x/7.x declares both per-MCU GCC 8 +/// toolchains and a unified one, but its Arduino builds use the per-MCU +/// package. pioarduino 53.x/54.x declare only the unified +/// `toolchain-xtensa-esp-elf`, by a registry version rather than a metadata +/// URL, so "no metadata URL" does not imply a per-MCU package. +fn platform_toolchain_name( + platform: &fbuild_packages::library::Esp32Platform, + mcu_config: &Esp32McuConfig, +) -> String { + toolchain_name_for(mcu_config, |name| platform.get_package_url(name).is_ok()) +} + +fn toolchain_name_for(mcu_config: &Esp32McuConfig, declares: impl Fn(&str) -> bool) -> String { + per_mcu_toolchain_name(mcu_config) + .filter(|name| declares(name)) + .unwrap_or_else(|| primary_toolchain_name(mcu_config.is_riscv()).to_string()) } async fn resolve_and_create_toolchain( @@ -294,7 +313,7 @@ async fn resolve_and_create_toolchain( let prefix = mcu_config.toolchain_prefix(); if !platform.has_unified_toolchain(is_riscv) { - let name = selected_toolchain_name(mcu_config, false); + let name = platform_toolchain_name(platform, mcu_config); let requirement = platform.get_package_requirement(&name)?; let registry = requirement.spec.registry().ok_or_else(|| { fbuild_core::FbuildError::PackageError(format!( @@ -398,7 +417,7 @@ async fn provision_toolchain( mode: ProvisionMode, ) -> ProvisionedPackage { let is_riscv = mcu_config.is_riscv(); - let name = selected_toolchain_name(mcu_config, platform.has_unified_toolchain(is_riscv)); + let name = platform_toolchain_name(platform, mcu_config); let toolchain = if mode.fetches() { resolve_and_create_toolchain(platform, project_dir, mcu_config) .await @@ -459,7 +478,7 @@ fn cached_toolchain( let is_riscv = mcu_config.is_riscv(); let prefix = mcu_config.toolchain_prefix(); if !platform.has_unified_toolchain(is_riscv) { - let name = selected_toolchain_name(mcu_config, false); + let name = platform_toolchain_name(platform, mcu_config); let requirement = platform.get_package_requirement(&name)?; let registry = requirement.spec.registry().ok_or_else(|| { fbuild_core::FbuildError::PackageError(format!( @@ -770,14 +789,39 @@ mod registry_toolchain_tests { fn legacy_esp32s3_manifest_selects_its_per_mcu_toolchain() { let mcu = get_mcu_config("esp32s3").unwrap(); assert_eq!( - selected_toolchain_name(&mcu, false), - "toolchain-xtensa-esp32s3" + per_mcu_toolchain_name(&mcu).as_deref(), + Some("toolchain-xtensa-esp32s3") ); assert_eq!( - selected_toolchain_name(&mcu, true), + primary_toolchain_name(mcu.is_riscv()), "toolchain-xtensa-esp-elf" ); assert_eq!(mcu.toolchain_prefix(), "xtensa-esp32s3-elf-"); + assert_eq!( + per_mcu_toolchain_name(&get_mcu_config("esp32c3").unwrap()), + None + ); + } + + #[test] + fn declared_per_mcu_toolchain_wins_over_unified() { + let s3 = get_mcu_config("esp32s3").unwrap(); + let c3 = get_mcu_config("esp32c3").unwrap(); + // official espressif32 6.x/7.x: both declared, Arduino uses per-MCU. + let both = |n: &str| n == "toolchain-xtensa-esp32s3" || n == "toolchain-xtensa-esp-elf"; + assert_eq!(toolchain_name_for(&s3, both), "toolchain-xtensa-esp32s3"); + // pioarduino 51.x: per-MCU only. + assert_eq!( + toolchain_name_for(&s3, |n| n == "toolchain-xtensa-esp32s3"), + "toolchain-xtensa-esp32s3" + ); + // pioarduino 53.x/54.x: unified only, by registry version. + assert_eq!( + toolchain_name_for(&s3, |n| n == "toolchain-xtensa-esp-elf"), + "toolchain-xtensa-esp-elf" + ); + // RISC-V always uses the shared package. + assert_eq!(toolchain_name_for(&c3, both), "toolchain-riscv32-esp"); } #[test] diff --git a/crates/fbuild-library/src/library/esp32_platform.rs b/crates/fbuild-library/src/library/esp32_platform.rs index b3c69137..d412af4e 100644 --- a/crates/fbuild-library/src/library/esp32_platform.rs +++ b/crates/fbuild-library/src/library/esp32_platform.rs @@ -480,6 +480,32 @@ mod tests { assert_eq!(registry.requirement.as_deref(), Some("12.2.0+20230208")); } + /// The published pioarduino 53.x/54.x `platform.json` names the unified + /// toolchain by a PlatformIO registry version, not a metadata URL. It is + /// not "unified" in the metadata sense, yet it has no per-MCU entry: the + /// unified name is the registry package to resolve. + #[test] + fn published_54_03_20_names_unified_toolchain_by_registry_version() { + let tmp = tempfile::TempDir::new().unwrap(); + write_platform_json( + tmp.path(), + r#"{"packages":{ + "toolchain-xtensa-esp-elf":{"type":"toolchain","optional":true,"owner":"platformio","version":"14.2.0+20241119"}, + "toolchain-riscv32-esp":{"type":"toolchain","optional":true,"owner":"platformio","version":"14.2.0+20241119"} + }}"#, + ); + let p = platform_with_install_dir(tmp.path()); + assert!(!p.has_unified_toolchain(false)); + assert!(p.get_package_url("toolchain-xtensa-esp32s3").is_err()); + let requirement = p + .get_package_requirement("toolchain-xtensa-esp-elf") + .unwrap(); + let registry = requirement.spec.registry().unwrap(); + assert_eq!(registry.owner.as_deref(), Some("platformio")); + assert_eq!(registry.name, "toolchain-xtensa-esp-elf"); + assert_eq!(registry.requirement.as_deref(), Some("14.2.0+20241119")); + } + #[test] fn explicit_framework_registry_pin_overrides_manifest_without_changing_toolchain() { let tmp = tempfile::TempDir::new().unwrap();