From b65dea7f6324071b70274a46f9c6cf4fc661fc6d Mon Sep 17 00:00:00 2001 From: Zach Vorhies Date: Sun, 4 Oct 2026 02:40:21 -0700 Subject: [PATCH 1/3] fix: build legacy ESP32 IDF 3.3 platform --- .../src/esp32/esp32_linker.rs | 11 +++- .../src/esp32/esp32_linker_tests.rs | 14 ++++ crates/fbuild-build-esp/src/esp32/fixups.rs | 65 +++++++++++++++++-- .../src/esp32/orchestrator/build.rs | 7 +- .../src/esp32/orchestrator/packages.rs | 22 ++++++- .../src/library/esp32_framework/sdk_paths.rs | 38 ++++++++++- crates/fbuild-library/src/library/esptool.rs | 17 ++++- 7 files changed, 163 insertions(+), 11 deletions(-) diff --git a/crates/fbuild-build-esp/src/esp32/esp32_linker.rs b/crates/fbuild-build-esp/src/esp32/esp32_linker.rs index 23e76a1db..67bde94a3 100644 --- a/crates/fbuild-build-esp/src/esp32/esp32_linker.rs +++ b/crates/fbuild-build-esp/src/esp32/esp32_linker.rs @@ -179,6 +179,7 @@ pub struct Esp32Linker { /// The app partition's size, which is what the image must fit. `None` /// falls back to `max_flash` (FastLED/fbuild#1409). app_size_limit: Option, + print_memory_usage: bool, } impl Esp32Linker { @@ -219,6 +220,7 @@ impl Esp32Linker { verbose, caller_path: None, app_size_limit: None, + print_memory_usage: true, } } @@ -230,6 +232,11 @@ impl Esp32Linker { self } + pub fn with_memory_usage_reporting(mut self, enabled: bool) -> Self { + self.print_memory_usage = enabled; + self + } + fn flash_limit(&self) -> Option { self.app_size_limit.or(self.max_flash) } @@ -418,7 +425,9 @@ impl Esp32Linker { link_args.extend(self.linker_scripts.to_args()); // Memory usage reporting - link_args.push("-Wl,--print-memory-usage".to_string()); + if self.print_memory_usage { + link_args.push("-Wl,--print-memory-usage".to_string()); + } // Output link_args.extend(["-o".to_string(), elf_path.to_string_lossy().to_string()]); diff --git a/crates/fbuild-build-esp/src/esp32/esp32_linker_tests.rs b/crates/fbuild-build-esp/src/esp32/esp32_linker_tests.rs index 4dcce24a6..91354e2e3 100644 --- a/crates/fbuild-build-esp/src/esp32/esp32_linker_tests.rs +++ b/crates/fbuild-build-esp/src/esp32/esp32_linker_tests.rs @@ -195,6 +195,20 @@ fn test_esp32_link_command_emits_linker_map_next_to_elf() { ); } +#[test] +fn legacy_xtensa_linker_omits_unsupported_memory_report() { + let args = test_linker("esp32") + .with_memory_usage_reporting(false) + .build_link_args( + &[], + &[], + Path::new("/build/firmware.elf"), + &LinkExtraArgs::default(), + ); + assert!(!args.iter().any(|arg| arg == "-Wl,--print-memory-usage")); + assert!(args.iter().any(|arg| arg == "-Wl,-Map=/build/firmware.map")); +} + #[test] fn legacy_esp32s3_link_includes_cpp_runtime_once() { let mut linker = test_linker("esp32s3"); diff --git a/crates/fbuild-build-esp/src/esp32/fixups.rs b/crates/fbuild-build-esp/src/esp32/fixups.rs index 4a45bc267..53dd24e22 100644 --- a/crates/fbuild-build-esp/src/esp32/fixups.rs +++ b/crates/fbuild-build-esp/src/esp32/fixups.rs @@ -27,14 +27,21 @@ type Rule = fn(Esp32McuConfig) -> Esp32McuConfig; /// - 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 legacy_xtensa32 = toolchain.name == "toolchain-xtensa32"; + let per_mcu_xtensa = is_per_mcu_xtensa(&toolchain.name) || legacy_xtensa32; 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), + let config = apply_if( + (per_mcu_xtensa && is_gcc8(&toolchain.version)) || legacy_xtensa32, config, legacy_gcc8, - ) + ); + apply_if(legacy_xtensa32, config, legacy_gcc5) +} + +/// GCC 5 in the official legacy ESP32 package predates `-fmacro-prefix-map`. +pub fn supports_macro_prefix_map(toolchain: &PackageInfo) -> bool { + toolchain.name != "toolchain-xtensa32" } /// Drop LTO when the SDK links with `-fno-lto`: objects compiled with LTO @@ -82,6 +89,40 @@ fn legacy_gcc8(config: Esp32McuConfig) -> Esp32McuConfig { without_lto(gcc8_language_standards(gcc8_linker_recipe(config))) } +fn legacy_gcc5(mut config: Esp32McuConfig) -> Esp32McuConfig { + config.compiler_flags.cxx = replaced(config.compiler_flags.cxx, "-std=gnu++11", "-std=gnu++1z"); + config.linker_flags = [ + "-nostdlib", + "-Wl,-static", + "-u", + "call_user_start_cpu0", + "-Wl,--undefined=uxTopUsedPriority", + "-Wl,--gc-sections", + "-Wl,-EL", + "-u", + "ld_include_panic_highint_hdl", + "-u", + "__cxa_guard_dummy", + "-u", + "__cxx_fatal_exception", + ] + .into_iter() + .map(str::to_string) + .collect(); + config.linker_scripts = [ + "esp32_out.ld", + "esp32.common.ld", + "esp32.rom.ld", + "esp32.peripherals.ld", + "esp32.rom.libgcc.ld", + "esp32.rom.spiram_incompatible_fns.ld", + ] + .into_iter() + .map(str::to_string) + .collect(); + 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 { @@ -238,6 +279,22 @@ mod tests { assert!(!has_lto(&config)); } + #[test] + fn platformio_legacy_xtensa32_uses_supported_flags() { + let package = toolchain("toolchain-xtensa32", "2.50200.97"); + let config = for_toolchain(get_mcu_config("esp32").unwrap(), &package); + assert!(!has( + &config.compiler_flags.common, + "-mdisable-hardware-atomics" + )); + assert!(has(&config.compiler_flags.cxx, "-std=gnu++1z")); + assert!(!has(&config.compiler_flags.cxx, "-std=gnu++2b")); + assert!(!has_lto(&config)); + assert!(!supports_macro_prefix_map(&package)); + assert!(has(&config.linker_flags, "call_user_start_cpu0")); + assert!(!has(&config.linker_flags, "-Wl,--no-warn-rwx-segments")); + } + #[test] fn gcc8_rules_need_a_per_mcu_xtensa_package() { let base = get_mcu_config("esp32s3").unwrap(); diff --git a/crates/fbuild-build-esp/src/esp32/orchestrator/build.rs b/crates/fbuild-build-esp/src/esp32/orchestrator/build.rs index 89ecd0794..16df48c93 100644 --- a/crates/fbuild-build-esp/src/esp32/orchestrator/build.rs +++ b/crates/fbuild-build-esp/src/esp32/orchestrator/build.rs @@ -317,8 +317,10 @@ impl BuildOrchestrator for Esp32Orchestrator { // SDK defines (from flags/defines) are prepended so user flags can override them. let mut user_flags = sdk_defines.clone(); // Before the user's build_flags, so their own prefix maps still win. - user_flags.extend(framework_macro_prefix_map(&core_dir)); - user_flags.extend(sdk_farm.as_ref().map(SdkIncludeFarm::macro_prefix_map)); + if fixups::supports_macro_prefix_map(&toolchain_info) { + user_flags.extend(framework_macro_prefix_map(&core_dir)); + user_flags.extend(sdk_farm.as_ref().map(SdkIncludeFarm::macro_prefix_map)); + } let mut user_build_flags = ctx.config.get_build_flags(¶ms.env_name)?; user_build_flags.extend(params.extra_build_flags.clone()); user_flags.extend(user_build_flags.clone()); @@ -860,6 +862,7 @@ impl BuildOrchestrator for Esp32Orchestrator { params.verbose, ) .with_caller_path(params.caller_path.clone()) + .with_memory_usage_reporting(toolchain_info.name != "toolchain-xtensa32") .with_app_size_limit(super::boot_artifacts::app_partition_limit( ¶ms.project_dir, &framework, diff --git a/crates/fbuild-build-esp/src/esp32/orchestrator/packages.rs b/crates/fbuild-build-esp/src/esp32/orchestrator/packages.rs index 5e1519125..1f4ab0f7f 100644 --- a/crates/fbuild-build-esp/src/esp32/orchestrator/packages.rs +++ b/crates/fbuild-build-esp/src/esp32/orchestrator/packages.rs @@ -337,7 +337,8 @@ fn per_mcu_toolchain_name(mcu_config: &Esp32McuConfig) -> Option { /// 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. +/// URL, so "no metadata URL" does not imply a per-MCU package. The older +/// official espressif32 1.11.2 platform uses `toolchain-xtensa32` for ESP32. fn platform_toolchain_name( platform: &fbuild_packages::library::Esp32Platform, mcu_config: &Esp32McuConfig, @@ -348,6 +349,10 @@ fn platform_toolchain_name( fn toolchain_name_for(mcu_config: &Esp32McuConfig, declares: impl Fn(&str) -> bool) -> String { per_mcu_toolchain_name(mcu_config) .filter(|name| declares(name)) + .or_else(|| { + (mcu_config.mcu == "esp32" && declares("toolchain-xtensa32")) + .then(|| "toolchain-xtensa32".to_string()) + }) .unwrap_or_else(|| primary_toolchain_name(mcu_config.is_riscv()).to_string()) } @@ -871,6 +876,21 @@ mod registry_toolchain_tests { assert_eq!(toolchain_name_for(&c3, both), "toolchain-riscv32-esp"); } + #[test] + fn legacy_espressif32_1_11_2_uses_xtensa32() { + let esp32 = get_mcu_config("esp32").unwrap(); + // The official espressif32@1.11.2 platform manifest for IDF 3.3 + // declares this registry package instead of a per-MCU or unified one. + let manifest = serde_json::json!({ + "packages": { + "toolchain-xtensa32": {"type": "toolchain", "version": "~2.50200.0"} + } + }); + let declares = |name: &str| manifest["packages"].get(name).is_some(); + + assert_eq!(toolchain_name_for(&esp32, declares), "toolchain-xtensa32"); + } + #[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"}}}"#; diff --git a/crates/fbuild-library/src/library/esp32_framework/sdk_paths.rs b/crates/fbuild-library/src/library/esp32_framework/sdk_paths.rs index 08537f5d8..ea26ef632 100644 --- a/crates/fbuild-library/src/library/esp32_framework/sdk_paths.rs +++ b/crates/fbuild-library/src/library/esp32_framework/sdk_paths.rs @@ -51,9 +51,14 @@ fn pio_join_literals(line: &str) -> Option { /// Returns `None` when the script is absent or does not parse, so the caller /// can fall back to the tree scan. fn parse_pio_cpppath(root: &Path, mcu: &str) -> Option> { - let script = root + let per_mcu_script = root .join("tools") .join(format!("platformio-build-{mcu}.py")); + let script = if per_mcu_script.exists() { + per_mcu_script + } else { + root.join("tools").join("platformio-build.py") + }; let content = std::fs::read_to_string(&script).ok()?; let mut dirs = Vec::new(); @@ -103,7 +108,11 @@ pub(super) fn sdk_mcu_dir(fw: &Esp32Framework, mcu: &str) -> PathBuf { if new_path.exists() { return new_path; } - root.join("tools").join("sdk").join(mcu) + let flat_path = root.join("tools").join("sdk"); + if flat_path.join("include").exists() { + return flat_path; + } + flat_path.join(mcu) } fn sdk_memory_variant_dir(sdk_dir: &Path, requested: Option<&str>) -> Option { @@ -382,3 +391,28 @@ impl Esp32Framework { flags } } + +#[cfg(test)] +mod legacy_sdk_tests { + use super::*; + + #[test] + fn platformio_1_11_2_builder_supplies_flat_sdk_includes() { + let tmp = tempfile::TempDir::new().unwrap(); + let root = tmp.path(); + let mut script = String::from("CPPPATH=[\n"); + for i in 0..20 { + let dir = root.join(format!("tools/sdk/include/component{i}")); + std::fs::create_dir_all(&dir).unwrap(); + script.push_str(&format!( + "join(FRAMEWORK_DIR, \"tools\", \"sdk\", \"include\", \"component{i}\"),\n" + )); + } + script.push_str("]\n"); + std::fs::write(root.join("tools/platformio-build.py"), script).unwrap(); + + let dirs = parse_pio_cpppath(root, "esp32").unwrap(); + assert_eq!(dirs.len(), 20); + assert_eq!(dirs[0], root.join("tools/sdk/include/component0")); + } +} diff --git a/crates/fbuild-library/src/library/esptool.rs b/crates/fbuild-library/src/library/esptool.rs index 9e836aef0..549aaa8cb 100644 --- a/crates/fbuild-library/src/library/esptool.rs +++ b/crates/fbuild-library/src/library/esptool.rs @@ -288,7 +288,9 @@ impl Esptool { } fn validate_source_esptool(dir: &Path) -> Result<()> { - if dir.join("pyproject.toml").is_file() && dir.join("esptool.py").is_file() { + if (dir.join("pyproject.toml").is_file() || dir.join("setup.py").is_file()) + && dir.join("esptool.py").is_file() + { Ok(()) } else { Err(FbuildError::PackageError(format!( @@ -972,4 +974,17 @@ mod tests { ); assert!(validate_source_esptool(tmp.path()).is_err()); } + + #[test] + fn legacy_platformio_esptool_setup_py_archive_is_valid() { + let tmp = tempfile::TempDir::new().unwrap(); + std::fs::write( + tmp.path().join("setup.py"), + "from setuptools import setup\nsetup()\n", + ) + .unwrap(); + std::fs::write(tmp.path().join("esptool.py"), "print('esptool')\n").unwrap(); + + validate_source_esptool(tmp.path()).unwrap(); + } } From a8aa3d2eb877ab7295a5a48a9849ec839ad50e86 Mon Sep 17 00:00:00 2001 From: Zach Vorhies Date: Sun, 4 Oct 2026 02:43:23 -0700 Subject: [PATCH 2/3] test: keep legacy resolver coverage within source limit --- .../src/esp32/orchestrator/packages.rs | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/crates/fbuild-build-esp/src/esp32/orchestrator/packages.rs b/crates/fbuild-build-esp/src/esp32/orchestrator/packages.rs index 1f4ab0f7f..506d31188 100644 --- a/crates/fbuild-build-esp/src/esp32/orchestrator/packages.rs +++ b/crates/fbuild-build-esp/src/esp32/orchestrator/packages.rs @@ -332,13 +332,10 @@ fn per_mcu_toolchain_name(mcu_config: &Esp32McuConfig) -> Option { /// 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. The older -/// official espressif32 1.11.2 platform uses `toolchain-xtensa32` for ESP32. +/// Prefer a declared per-MCU package for pioarduino 51.x and official 6.x/7.x. +/// Official 1.11.2 calls ESP32's package `toolchain-xtensa32`. Pioarduino +/// 53.x/54.x declare only a unified registry package, so a missing metadata +/// URL does not imply a per-MCU package. fn platform_toolchain_name( platform: &fbuild_packages::library::Esp32Platform, mcu_config: &Esp32McuConfig, @@ -878,17 +875,14 @@ mod registry_toolchain_tests { #[test] fn legacy_espressif32_1_11_2_uses_xtensa32() { - let esp32 = get_mcu_config("esp32").unwrap(); - // The official espressif32@1.11.2 platform manifest for IDF 3.3 - // declares this registry package instead of a per-MCU or unified one. - let manifest = serde_json::json!({ - "packages": { - "toolchain-xtensa32": {"type": "toolchain", "version": "~2.50200.0"} - } - }); - let declares = |name: &str| manifest["packages"].get(name).is_some(); - - assert_eq!(toolchain_name_for(&esp32, declares), "toolchain-xtensa32"); + // The official espressif32@1.11.2 manifest declares this sole Xtensa package. + let packages = serde_json::json!({"toolchain-xtensa32": {"version": "~2.50200.0"}}); + assert_eq!( + toolchain_name_for(&get_mcu_config("esp32").unwrap(), |name| packages + .get(name) + .is_some()), + "toolchain-xtensa32" + ); } #[test] From 6d315c355a470d8d6148a2f258a366289a392735 Mon Sep 17 00:00:00 2001 From: Zach Vorhies Date: Sun, 4 Oct 2026 03:08:21 -0700 Subject: [PATCH 3/3] test: separate legacy resolver regression Local-Gate: v1 tree=4a0acbd525510c96fb5c1c84f97f76fe83d0f008 secs=413 lanes=linux-minimal:run Ci-Attestation: {"at":1791109282,"gate":"general/all/ubuntu-ci-guards","host":"linux-x86_64","key":"9cb81e225798f859a87e2486736cc589559434410eb407c5c97030fad62114f1","lane":"linux-minimal","parents":["a8aa3d2eb877ab7295a5a48a9849ec839ad50e86"],"secs":413,"stamp":"a5c1da251578c6e3a8bc08184314fb2b","tree":"4a0acbd525510c96fb5c1c84f97f76fe83d0f008","v":1,"via":"run"} Ci-Attestation: {"at":1791109282,"gate":"rust/x86_64-unknown-linux-gnu/workspace-clippy","host":"linux-x86_64","key":"9cb81e225798f859a87e2486736cc589559434410eb407c5c97030fad62114f1","lane":"linux-minimal","parents":["a8aa3d2eb877ab7295a5a48a9849ec839ad50e86"],"secs":413,"stamp":"3685a524f0521aa97c1bb1952ab1bde3","tree":"4a0acbd525510c96fb5c1c84f97f76fe83d0f008","v":1,"via":"run"} Ci-Attestation: {"at":1791109282,"gate":"rust/x86_64-unknown-linux-gnu/workspace-test","host":"linux-x86_64","key":"9cb81e225798f859a87e2486736cc589559434410eb407c5c97030fad62114f1","lane":"linux-minimal","parents":["a8aa3d2eb877ab7295a5a48a9849ec839ad50e86"],"secs":413,"stamp":"8245845a1184567850672a1e2570e58c","tree":"4a0acbd525510c96fb5c1c84f97f76fe83d0f008","v":1,"via":"run"} Ci-Attestation: {"at":1791109282,"gate":"rust/x86_64-unknown-linux-gnu/python-facade-test","host":"linux-x86_64","key":"9cb81e225798f859a87e2486736cc589559434410eb407c5c97030fad62114f1","lane":"linux-minimal","parents":["a8aa3d2eb877ab7295a5a48a9849ec839ad50e86"],"secs":413,"stamp":"c60ec54051dfbbacbfc5432b173819b5","tree":"4a0acbd525510c96fb5c1c84f97f76fe83d0f008","v":1,"via":"run"} --- .../src/esp32/orchestrator/packages.rs | 16 ++++------------ .../esp32/orchestrator/packages_legacy_tests.rs | 13 +++++++++++++ 2 files changed, 17 insertions(+), 12 deletions(-) create mode 100644 crates/fbuild-build-esp/src/esp32/orchestrator/packages_legacy_tests.rs diff --git a/crates/fbuild-build-esp/src/esp32/orchestrator/packages.rs b/crates/fbuild-build-esp/src/esp32/orchestrator/packages.rs index 506d31188..3c7e305b3 100644 --- a/crates/fbuild-build-esp/src/esp32/orchestrator/packages.rs +++ b/crates/fbuild-build-esp/src/esp32/orchestrator/packages.rs @@ -873,18 +873,6 @@ mod registry_toolchain_tests { assert_eq!(toolchain_name_for(&c3, both), "toolchain-riscv32-esp"); } - #[test] - fn legacy_espressif32_1_11_2_uses_xtensa32() { - // The official espressif32@1.11.2 manifest declares this sole Xtensa package. - let packages = serde_json::json!({"toolchain-xtensa32": {"version": "~2.50200.0"}}); - assert_eq!( - toolchain_name_for(&get_mcu_config("esp32").unwrap(), |name| packages - .get(name) - .is_some()), - "toolchain-xtensa32" - ); - } - #[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"}}}"#; @@ -998,3 +986,7 @@ mod registry_toolchain_tests { } } } + +#[cfg(test)] +#[path = "packages_legacy_tests.rs"] +mod legacy_tests; diff --git a/crates/fbuild-build-esp/src/esp32/orchestrator/packages_legacy_tests.rs b/crates/fbuild-build-esp/src/esp32/orchestrator/packages_legacy_tests.rs new file mode 100644 index 000000000..7b39767c2 --- /dev/null +++ b/crates/fbuild-build-esp/src/esp32/orchestrator/packages_legacy_tests.rs @@ -0,0 +1,13 @@ +use super::*; + +#[test] +fn legacy_espressif32_1_11_2_uses_xtensa32() { + // The official espressif32@1.11.2 manifest declares this sole Xtensa package. + let packages = serde_json::json!({"toolchain-xtensa32": {"version": "~2.50200.0"}}); + assert_eq!( + toolchain_name_for(&get_mcu_config("esp32").unwrap(), |name| packages + .get(name) + .is_some()), + "toolchain-xtensa32" + ); +}