diff --git a/crates/fbuild-build-esp/src/esp32/orchestrator/helpers.rs b/crates/fbuild-build-esp/src/esp32/orchestrator/helpers.rs index e11772279..c286d962b 100644 --- a/crates/fbuild-build-esp/src/esp32/orchestrator/helpers.rs +++ b/crates/fbuild-build-esp/src/esp32/orchestrator/helpers.rs @@ -13,8 +13,63 @@ use std::path::{Path, PathBuf}; use fbuild_core::Result; -/// Record the requested ESP32 package pins beside the installed stack so a -/// successful build cannot conceal a cross-version framework or SDK. +/// How fbuild integrates ESP-IDF for an environment. +/// +/// fbuild has no CMake/`idf.py` component build: every ESP32 environment is +/// compiled with the Arduino core. Recording the mode beside the requested and +/// resolved frameworks keeps a build log from claiming IDF component coverage +/// it never produced (FastLED/fbuild#1664). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum Esp32Integration { + /// `framework = arduino`: the Arduino core only. + ArduinoCore, + /// `framework = arduino, espidf`: the Arduino core links the precompiled + /// ESP-IDF SDK. Not an ESP-IDF component build. + ArduinoOnIdf, +} + +impl Esp32Integration { + pub(super) fn label(self) -> &'static str { + match self { + Self::ArduinoCore => "arduino-core", + Self::ArduinoOnIdf => "arduino-on-idf", + } + } +} + +/// The frameworks an environment's `framework =` value requests. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub(super) struct RequestedFrameworks { + pub arduino: bool, + pub espidf: bool, +} + +impl RequestedFrameworks { + /// `None` when the request is a bare `espidf` build fbuild cannot perform, + /// otherwise the integration mode the build will actually use. + pub(super) fn integration(self) -> Option { + match (self.arduino, self.espidf) { + (true, true) => Some(Esp32Integration::ArduinoOnIdf), + (false, true) => None, + _ => Some(Esp32Integration::ArduinoCore), + } + } +} + +/// Parse a PlatformIO `framework =` value into the requested frameworks. +pub(super) fn requested_frameworks(framework: &str) -> RequestedFrameworks { + let mut requested = RequestedFrameworks::default(); + for part in framework.split(',').map(str::trim) { + requested.arduino |= part.eq_ignore_ascii_case("arduino"); + requested.espidf |= part.eq_ignore_ascii_case("espidf"); + } + requested +} + +/// Record the requested framework, the requested package pins and the actual +/// integration mode beside the installed stack so a successful build cannot +/// conceal a cross-version framework, an SDK mismatch or an integration mode +/// other than the one requested (FastLED/fbuild#1664). pub(super) fn esp32_package_stack_summary( env_config: Option<&HashMap>, platform: &fbuild_packages::PackageInfo, @@ -27,6 +82,9 @@ pub(super) fn esp32_package_stack_summary( .and_then(|env| env.get("platform")) .map_or("", String::as_str); let requested_framework = env_config + .and_then(|env| env.get("framework")) + .map_or("", String::as_str); + let framework_pin = env_config .and_then(|env| env.get("platform_packages")) .and_then(|raw| { raw.lines().find(|line| { @@ -36,11 +94,14 @@ pub(super) fn esp32_package_stack_summary( }) }) .map_or("", str::trim); + let integration = requested_frameworks(requested_framework) + .integration() + .map_or("unsupported-espidf", Esp32Integration::label); let sdk_version = installed_framework .bundled_esp_idf_version(mcu) .unwrap_or_else(|| "unknown (bundled with framework)".to_string()); format!( - "ESP32 packages: requested platform={requested_platform}, framework={requested_framework}; resolved platform={}@{}, framework={}@{}, toolchain={}@{}, ESP-IDF SDK={sdk_version}", + "ESP32 packages: requested platform={requested_platform}, framework={requested_framework}, framework_pin={framework_pin}, integration={integration}; resolved platform={}@{}, framework={}@{}, toolchain={}@{}, ESP-IDF SDK={sdk_version}", platform.name, platform.version, framework.name, @@ -87,11 +148,7 @@ pub(super) fn reject_unsupported_esp_framework( let Some(framework) = env.get("framework") else { return Ok(()); }; - let parts: Vec<&str> = framework.split(',').map(str::trim).collect(); - let espidf = parts.iter().any(|part| part.eq_ignore_ascii_case("espidf")); - let arduino = parts - .iter() - .any(|part| part.eq_ignore_ascii_case("arduino")); + let RequestedFrameworks { arduino, espidf } = requested_frameworks(framework); if espidf && !arduino { return Err(fbuild_core::FbuildError::ConfigError(format!( "framework = {framework} is unsupported for environment '{env_name}': fbuild builds \ diff --git a/crates/fbuild-build-esp/src/esp32/orchestrator/tests.rs b/crates/fbuild-build-esp/src/esp32/orchestrator/tests.rs index 002783144..e04b3d7af 100644 --- a/crates/fbuild-build-esp/src/esp32/orchestrator/tests.rs +++ b/crates/fbuild-build-esp/src/esp32/orchestrator/tests.rs @@ -3,8 +3,8 @@ use super::Esp32Orchestrator; use super::cdc::{cdc_on_boot_enabled, is_esp32_project, warn_if_cdc_on_boot}; use super::helpers::{ - apply_effective_define_flags, reject_unsupported_esp_framework, - reject_unsupported_sdkconfig_overlay, + Esp32Integration, apply_effective_define_flags, esp32_package_stack_summary, + reject_unsupported_esp_framework, reject_unsupported_sdkconfig_overlay, requested_frameworks, }; use super::helpers::{ framework_failure_marker, framework_signature, record_failed_framework_lib, @@ -12,6 +12,7 @@ use super::helpers::{ }; use crate::BuildOrchestrator; use fbuild_core::Platform; +use std::collections::HashMap; use std::path::PathBuf; use std::time::Duration; @@ -353,3 +354,80 @@ fn deploy_esptool_requires_v5_cli() { "C:/cache/fbuild-tools/esptool/Scripts/esptool.py.exe" ))); } + +fn package_info(name: &str, version: &str) -> fbuild_packages::PackageInfo { + fbuild_packages::PackageInfo { + name: name.to_string(), + version: version.to_string(), + url: format!("https://example.invalid/{name}-{version}.tar.gz"), + install_path: PathBuf::from("/nonexistent"), + checksum: None, + installed_bytes: None, + } +} + +#[test] +fn requested_frameworks_parses_single_and_dual_requests() { + let arduino = requested_frameworks("arduino"); + assert!(arduino.arduino && !arduino.espidf); + assert_eq!(arduino.integration(), Some(Esp32Integration::ArduinoCore)); + + let dual = requested_frameworks("arduino, espidf"); + assert!(dual.arduino && dual.espidf); + assert_eq!(dual.integration(), Some(Esp32Integration::ArduinoOnIdf)); + + // Order and spacing must not change the classification. + let reversed = requested_frameworks(" espidf , arduino "); + assert_eq!(reversed, dual); + + let bare_idf = requested_frameworks("espidf"); + assert_eq!(bare_idf.integration(), None, "bare espidf is unsupported"); +} + +#[test] +fn stack_summary_records_requested_framework_and_integration_mode() { + let tmp = tempfile::TempDir::new().unwrap(); + let installed = fbuild_packages::library::Esp32Framework::new(tmp.path(), "esp32s3"); + let platform = package_info("espressif32", "6.13.0"); + let framework = package_info("framework-arduinoespressif32", "3.3.7"); + let toolchain = package_info("toolchain-xtensa-esp32s3", "8.4.0"); + + let mut dual_env = HashMap::new(); + dual_env.insert("framework".to_string(), "arduino, espidf".to_string()); + dual_env.insert( + "platform_packages".to_string(), + "framework-arduinoespressif32@https://example.invalid/fw.tar.gz".to_string(), + ); + let summary = esp32_package_stack_summary( + Some(&dual_env), + &platform, + &framework, + &toolchain, + &installed, + "esp32s3", + ); + assert!(summary.contains("framework=arduino, espidf"), "{summary}"); + assert!(summary.contains("integration=arduino-on-idf"), "{summary}"); + assert!( + summary.contains( + "framework_pin=framework-arduinoespressif32@https://example.invalid/fw.tar.gz" + ), + "{summary}" + ); + assert!( + summary.contains( + "resolved platform=espressif32@6.13.0, framework=framework-arduinoespressif32@3.3.7" + ), + "{summary}" + ); + + let plain = esp32_package_stack_summary( + None, &platform, &framework, &toolchain, &installed, "esp32s3", + ); + assert!(plain.contains("framework="), "{plain}"); + assert!( + plain.contains("framework_pin="), + "{plain}" + ); + assert!(plain.contains("integration=arduino-core"), "{plain}"); +}