From 0fdcb521caf301e8d620283272c61d2406abe196 Mon Sep 17 00:00:00 2001 From: Zach Vorhies Date: Fri, 2 Oct 2026 00:03:47 -0700 Subject: [PATCH 1/4] fix(esp32): resolve unified toolchain by registry version for pioarduino 53.x/54.x pioarduino 53.x/54.x platform.json names the unified toolchain-xtensa-esp-elf by a PlatformIO registry version (platformio/toolchain-xtensa-esp-elf@14.2.0+20241119), not a metadata URL. #1504 treated 'no metadata URL' as 'per-MCU package' and looked up toolchain-xtensa-, which those releases do not declare, so every ESP32 Xtensa build pinned to 53.x/54.x failed with "package 'toolchain-xtensa-esp32s3' not found in platform.json". Select the per-MCU name only when the platform omits the unified entry. Co-Authored-By: Claude --- .../src/esp32/orchestrator/packages.rs | 21 ++++++++++++--- .../src/library/esp32_platform.rs | 26 +++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/crates/fbuild-build-esp/src/esp32/orchestrator/packages.rs b/crates/fbuild-build-esp/src/esp32/orchestrator/packages.rs index 325ee0e0..152b9fe0 100644 --- a/crates/fbuild-build-esp/src/esp32/orchestrator/packages.rs +++ b/crates/fbuild-build-esp/src/esp32/orchestrator/packages.rs @@ -285,6 +285,21 @@ fn selected_toolchain_name(mcu_config: &Esp32McuConfig, unified: bool) -> String } } +/// The toolchain package the platform actually declares. pioarduino 53.x/54.x +/// name the unified `toolchain-xtensa-esp-elf` by a registry version +/// (`platformio/toolchain-xtensa-esp-elf@14.2.0+20241119`) rather than a +/// metadata URL, so "no metadata URL" does not imply a per-MCU package; only +/// releases that omit the unified entry (51.x) use `toolchain-xtensa-`. +fn platform_toolchain_name( + platform: &fbuild_packages::library::Esp32Platform, + mcu_config: &Esp32McuConfig, +) -> String { + let declares_unified = platform + .get_package_url(primary_toolchain_name(mcu_config.is_riscv())) + .is_ok(); + selected_toolchain_name(mcu_config, declares_unified) +} + async fn resolve_and_create_toolchain( platform: &fbuild_packages::library::Esp32Platform, project_dir: &Path, @@ -294,7 +309,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 +413,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 +474,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!( 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(); From fec7e75c655d32d903dfecb91c80c0509496bfb9 Mon Sep 17 00:00:00 2001 From: Zach Vorhies Date: Fri, 2 Oct 2026 01:11:59 -0700 Subject: [PATCH 2/4] fix(esp32): drop -fuse-cxa-atexit for every pre-GCC 14 toolchain pioarduino 53.x pairs GCC 13.2 unified toolchains with an SDK that, like 51.x, does not define __dso_handle. The flag was only dropped for per-MCU Xtensa packages, so once 53.x resolved its toolchain (previous commit) esp32dev and esp32c3 failed to link with undefined __dso_handle. Co-Authored-By: Claude --- .../fbuild-build-esp/src/esp32/mcu_config.rs | 42 +++++++++++++++---- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/crates/fbuild-build-esp/src/esp32/mcu_config.rs b/crates/fbuild-build-esp/src/esp32/mcu_config.rs index 61f77f8c..bd2caffa 100644 --- a/crates/fbuild-build-esp/src/esp32/mcu_config.rs +++ b/crates/fbuild-build-esp/src/esp32/mcu_config.rs @@ -85,18 +85,25 @@ 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. + /// Drop GCC 14 recipe flags when the platform selects an older toolchain. + /// The SDKs paired with pre-GCC 14 toolchains (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. + /// The per-MCU GCC 12 Xtensa compiler also rejects the atomics switch. pub fn adapt_to_toolchain(&mut self, package_name: &str, package_version: &str) { + let gcc_major = package_version + .split('.') + .next() + .and_then(|major| major.parse::().ok()); + if gcc_major.is_some_and(|major| major < 14) { + self.compiler_flags + .cxx + .retain(|flag| flag != "-fuse-cxa-atexit"); + } 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(); @@ -830,6 +837,27 @@ mod tests { ); } + #[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 mut config = get_mcu_config(mcu).unwrap(); + config.adapt_to_toolchain(package, "13.2.0+20240530"); + assert!( + !config + .compiler_flags + .cxx + .iter() + .any(|f| f == "-fuse-cxa-atexit"), + "{mcu} kept -fuse-cxa-atexit on GCC 13" + ); + } + } + #[test] fn platformio_gcc8_uses_supported_cpp_standard() { let mut config = get_mcu_config("esp32s3").unwrap(); From 5d225dbfd315121da7ca9a1184a94561be6d3b44 Mon Sep 17 00:00:00 2001 From: Zach Vorhies Date: Fri, 2 Oct 2026 01:30:33 -0700 Subject: [PATCH 3/4] fix(esp32): prefer a declared per-MCU toolchain over the unified one Review follow-up: official PlatformIO espressif32 6.x/7.x declares both per-MCU GCC 8.4 toolchains and toolchain-xtensa-esp-elf, and its Arduino builds use the per-MCU package. Preferring the unified entry would have moved those builds to GCC 14 and skipped the legacy_gcc8 recipe. Co-Authored-By: Claude --- .../src/esp32/orchestrator/packages.rs | 44 +++++++++++++++---- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/crates/fbuild-build-esp/src/esp32/orchestrator/packages.rs b/crates/fbuild-build-esp/src/esp32/orchestrator/packages.rs index 152b9fe0..5d592be7 100644 --- a/crates/fbuild-build-esp/src/esp32/orchestrator/packages.rs +++ b/crates/fbuild-build-esp/src/esp32/orchestrator/packages.rs @@ -285,19 +285,24 @@ fn selected_toolchain_name(mcu_config: &Esp32McuConfig, unified: bool) -> String } } -/// The toolchain package the platform actually declares. pioarduino 53.x/54.x -/// name the unified `toolchain-xtensa-esp-elf` by a registry version -/// (`platformio/toolchain-xtensa-esp-elf@14.2.0+20241119`) rather than a -/// metadata URL, so "no metadata URL" does not imply a per-MCU package; only -/// releases that omit the unified entry (51.x) use `toolchain-xtensa-`. +/// 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 { - let declares_unified = platform - .get_package_url(primary_toolchain_name(mcu_config.is_riscv())) - .is_ok(); - selected_toolchain_name(mcu_config, declares_unified) + 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 { + let per_mcu = selected_toolchain_name(mcu_config, false); + selected_toolchain_name(mcu_config, !declares(&per_mcu)) } async fn resolve_and_create_toolchain( @@ -795,6 +800,27 @@ mod registry_toolchain_tests { assert_eq!(mcu.toolchain_prefix(), "xtensa-esp32s3-elf-"); } + #[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] fn registry_payload_becomes_exact_checked_toolchain_package() { let manifest = r#"{"packages":{"toolchain-xtensa-esp32s3":{"type":"toolchain","owner":"espressif","version":"12.2.0+20230208"}}}"#; From c44ef4387b28a355327c32b0dc05bbde5d98e178 Mon Sep 17 00:00:00 2001 From: Zach Vorhies Date: Fri, 2 Oct 2026 01:42:46 -0700 Subject: [PATCH 4/4] refactor(esp32): apply toolchain and SDK fixups as pure config -> config rules Replace the in-place `Esp32McuConfig::adapt_to_toolchain` and `disable_lto` mutators with a `fixups` module of side-effect-free rules. Each rule (drop -fuse-cxa-atexit before GCC 14, drop the atomics switch on per-MCU Xtensa, the GCC 8 linker recipe, language standards, and LTO removal) is a named `Esp32McuConfig -> Esp32McuConfig` function, and the orchestrator applies the composed fixup once per input: let mcu_config = fixups::for_toolchain(mcu_config, &toolchain_info); let mcu_config = fixups::for_sdk_ld_flags(mcu_config, &sdk_ld_flags); Also replace the boolean-flag `selected_toolchain_name(mcu, unified)` with `per_mcu_toolchain_name(mcu) -> Option`, so `toolchain_name_for` reads as "the declared per-MCU package, else the unified one". Behaviour is unchanged. Co-Authored-By: Claude --- crates/fbuild-build-esp/src/esp32/fixups.rs | 277 ++++++++++++++++++ .../fbuild-build-esp/src/esp32/mcu_config.rs | 132 --------- crates/fbuild-build-esp/src/esp32/mod.rs | 1 + .../src/esp32/orchestrator/build.rs | 13 +- .../src/esp32/orchestrator/packages.rs | 25 +- 5 files changed, 297 insertions(+), 151 deletions(-) create mode 100644 crates/fbuild-build-esp/src/esp32/fixups.rs 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 bd2caffa..9de1b7b4 100644 --- a/crates/fbuild-build-esp/src/esp32/mcu_config.rs +++ b/crates/fbuild-build-esp/src/esp32/mcu_config.rs @@ -85,45 +85,6 @@ pub struct LegacyGcc8Recipe { } impl Esp32McuConfig { - /// Drop GCC 14 recipe flags when the platform selects an older toolchain. - /// The SDKs paired with pre-GCC 14 toolchains (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. - /// The per-MCU GCC 12 Xtensa compiler also rejects the atomics switch. - pub fn adapt_to_toolchain(&mut self, package_name: &str, package_version: &str) { - let gcc_major = package_version - .split('.') - .next() - .and_then(|major| major.parse::().ok()); - if gcc_major.is_some_and(|major| major < 14) { - self.compiler_flags - .cxx - .retain(|flag| flag != "-fuse-cxa-atexit"); - } - if package_name.starts_with("toolchain-xtensa-esp32") { - self.compiler_flags - .common - .retain(|flag| flag != "-mdisable-hardware-atomics"); - 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") @@ -232,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"). @@ -799,82 +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 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 mut config = get_mcu_config(mcu).unwrap(); - config.adapt_to_toolchain(package, "13.2.0+20240530"); - assert!( - !config - .compiler_flags - .cxx - .iter() - .any(|f| f == "-fuse-cxa-atexit"), - "{mcu} kept -fuse-cxa-atexit on GCC 13" - ); - } - } - - #[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 5d592be7..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,10 @@ 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. @@ -301,8 +299,9 @@ fn platform_toolchain_name( } fn toolchain_name_for(mcu_config: &Esp32McuConfig, declares: impl Fn(&str) -> bool) -> String { - let per_mcu = selected_toolchain_name(mcu_config, false); - selected_toolchain_name(mcu_config, !declares(&per_mcu)) + 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( @@ -790,14 +789,18 @@ 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]