diff --git a/.github/workflows/dylint.yml b/.github/workflows/dylint.yml index a4a3f3525..291a0083e 100644 --- a/.github/workflows/dylint.yml +++ b/.github/workflows/dylint.yml @@ -114,6 +114,10 @@ jobs: # Library UI tests invoke soldr cargo test separately, so preserve # the prebuilt dylint-link directory for their linker config. sed -n 's/^soldr: dylint prepare: tool bin dir //p' "$RUNNER_TEMP/dylint-prepare.log" >> "$GITHUB_PATH" + # setup-soldr prepares the nightly with a minimal profile. The Dylint + # library formatting check below needs rustfmt on that exact nightly. + - name: Install rustfmt for the Dylint toolchain + run: soldr rustup component add --toolchain nightly-2026-05-28 rustfmt - name: Install cross-target nightly standard libraries if: ${{ inputs.ref != '' || github.event_name == 'workflow_dispatch' }} run: | diff --git a/crates/fbuild-build-engine/src/package_override.rs b/crates/fbuild-build-engine/src/package_override.rs index d1cddb973..336cc9745 100644 --- a/crates/fbuild-build-engine/src/package_override.rs +++ b/crates/fbuild-build-engine/src/package_override.rs @@ -36,6 +36,12 @@ pub enum PlatformDefaultVersion<'a> { }, } +struct RegistryResolutionAccess<'a> { + client: &'a fbuild_packages::platformio_registry::RegistryClient, + cache_root: &'a Path, + fetch: bool, +} + /// Resolve a pinned PlatformIO platform and its selected package requirements /// before a family adapter constructs its framework/toolchain packages. The /// registry lookup and manifest parsing are intentionally independent of the @@ -61,6 +67,33 @@ pub async fn resolve_registry_overrides( .await } +/// Resolve only from cached registry metadata and an installed platform +/// manifest. `fbuild install --check/--dry-run` must never fetch a platform. +/// `None` means a required payload or platform archive is not cached yet. +pub async fn resolve_registry_overrides_offline( + project_dir: &Path, + env_config: &HashMap, + platform_name: &str, + package_names: &[&str], + platform_default_requirements: &[(&str, PlatformDefaultVersion<'_>)], +) -> fbuild_core::Result>> { + let cache_root = fbuild_packages::Cache::new(project_dir).platforms_dir(); + let client = fbuild_packages::platformio_registry::RegistryClient::default(); + resolve_registry_overrides_with_client_fetch( + project_dir, + env_config, + platform_name, + package_names, + platform_default_requirements, + RegistryResolutionAccess { + client: &client, + cache_root: &cache_root, + fetch: false, + }, + ) + .await +} + async fn resolve_registry_overrides_with_client( project_dir: &Path, env_config: &HashMap, @@ -70,6 +103,30 @@ async fn resolve_registry_overrides_with_client( client: &fbuild_packages::platformio_registry::RegistryClient, cache_root: &Path, ) -> fbuild_core::Result> { + resolve_registry_overrides_with_client_fetch( + project_dir, + env_config, + platform_name, + package_names, + platform_default_requirements, + RegistryResolutionAccess { + client, + cache_root, + fetch: true, + }, + ) + .await? + .ok_or_else(|| package_error("PlatformIO registry payload unavailable")) +} + +async fn resolve_registry_overrides_with_client_fetch( + project_dir: &Path, + env_config: &HashMap, + platform_name: &str, + package_names: &[&str], + platform_default_requirements: &[(&str, PlatformDefaultVersion<'_>)], + access: RegistryResolutionAccess<'_>, +) -> fbuild_core::Result>> { let raw_platform = env_config .get("platform") .map(String::as_str) @@ -115,7 +172,7 @@ async fn resolve_registry_overrides_with_client( .is_some() || explicit.iter().any(|spec| spec.registry().is_some()); if !has_registry_pin { - return Ok(HashMap::new()); + return Ok(Some(HashMap::new())); } let host = fbuild_core::platformio_package::host_system(fbuild_core::platform::host::current()) .ok_or_else(|| { @@ -137,19 +194,23 @@ async fn resolve_registry_overrides_with_client( // native adapter's existing default stack. explicit_registry_requirements(&explicit) } else { - let platform = client + let platform = access + .client .resolve_cached( &platform_registry, PackageKind::Platform, host, - cache_root, - true, + access.cache_root, + access.fetch, ) .await - .map_err(package_error)? - .ok_or_else(|| { - fbuild_core::FbuildError::PackageError("PlatformIO platform unavailable".into()) - })?; + .map_err(package_error)?; + let Some(platform) = platform else { + if !access.fetch { + return Ok(None); + } + return Err(package_error("PlatformIO platform unavailable")); + }; tracing::info!( "resolved requested PlatformIO platform {}@{}: {} (sha256 {})", platform.name, @@ -166,17 +227,23 @@ async fn resolve_registry_overrides_with_client( fbuild_packages::CacheSubdir::Platforms, project_dir, ); - let installed = platform_base - .staged_install(|dir| { - find_platform_manifest(dir).ok_or_else(|| { - fbuild_core::FbuildError::PackageError(format!( - "{} has no platform.json", - dir.display() - )) - })?; - Ok(()) - }) - .await?; + let installed = if access.fetch { + platform_base + .staged_install(|dir| { + find_platform_manifest(dir).ok_or_else(|| { + fbuild_core::FbuildError::PackageError(format!( + "{} has no platform.json", + dir.display() + )) + })?; + Ok(()) + }) + .await? + } else if platform_base.is_cached() { + platform_base.install_path() + } else { + return Ok(None); + }; let manifest_path = find_platform_manifest(&installed).ok_or_else(|| { fbuild_core::FbuildError::PackageError(format!( "{} has no platform.json", @@ -239,15 +306,25 @@ async fn resolve_registry_overrides_with_client( let Some(registry) = requirement.spec.registry() else { continue; }; - let payload = client - .resolve_cached(registry, requirement.kind, host, cache_root, true) + let payload = access + .client + .resolve_cached( + registry, + requirement.kind, + host, + access.cache_root, + access.fetch, + ) .await - .map_err(package_error)? - .ok_or_else(|| { - fbuild_core::FbuildError::PackageError(format!( - "PlatformIO package `{name}` unavailable" - )) - })?; + .map_err(package_error)?; + let Some(payload) = payload else { + if !access.fetch { + return Ok(None); + } + return Err(package_error(format!( + "PlatformIO package `{name}` unavailable" + ))); + }; tracing::info!( "resolved PlatformIO package {}@{}: {} (sha256 {})", payload.name, @@ -264,7 +341,7 @@ async fn resolve_registry_overrides_with_client( }, ); } - Ok(overrides) + Ok(Some(overrides)) } fn requirements_with_platform_defaults( @@ -804,4 +881,29 @@ mod tests { .contains("framework-arduino-unknown") ); } + + #[tokio::test] + async fn offline_registry_resolution_does_not_contact_uncached_platform() { + let temp = tempfile::tempdir().unwrap(); + let client = fbuild_packages::platformio_registry::RegistryClient::new( + "http://127.0.0.1:1/v3", + reqwest::Client::new(), + ); + let env = env(&[("platform", "platformio/espressif8266@4.0.1")]); + let result = resolve_registry_overrides_with_client_fetch( + temp.path(), + &env, + "espressif8266", + &["framework-arduinoespressif8266", "toolchain-xtensa"], + &[], + RegistryResolutionAccess { + client: &client, + cache_root: &temp.path().join("uncached-platform-metadata"), + fetch: false, + }, + ) + .await + .unwrap(); + assert!(result.is_none()); + } } diff --git a/crates/fbuild-build-engine/src/pipeline/context.rs b/crates/fbuild-build-engine/src/pipeline/context.rs index 610707f72..40889f781 100644 --- a/crates/fbuild-build-engine/src/pipeline/context.rs +++ b/crates/fbuild-build-engine/src/pipeline/context.rs @@ -171,6 +171,9 @@ impl BuildContext { Some(fbuild_core::Platform::Espressif32) => { Some(fbuild_core::Platform::Espressif32) } + Some(fbuild_core::Platform::Espressif8266) => { + Some(fbuild_core::Platform::Espressif8266) + } Some(fbuild_core::Platform::Teensy) => Some(fbuild_core::Platform::Teensy), Some(fbuild_core::Platform::Ststm32) => Some(fbuild_core::Platform::Ststm32), Some(fbuild_core::Platform::NordicNrf52) => { @@ -194,6 +197,9 @@ impl BuildContext { }; let resolved_packages: &[&str] = match resolved_platform { Some(fbuild_core::Platform::Espressif32) => &["framework-arduinoespressif32"], + Some(fbuild_core::Platform::Espressif8266) => { + &["framework-arduinoespressif8266", "toolchain-xtensa"] + } Some(fbuild_core::Platform::Teensy) => { &["framework-arduinoteensy", "toolchain-gccarmnoneeabi-teensy"] } diff --git a/crates/fbuild-build-esp/src/esp8266/mod.rs b/crates/fbuild-build-esp/src/esp8266/mod.rs index 4659c64af..9de0167ef 100644 --- a/crates/fbuild-build-esp/src/esp8266/mod.rs +++ b/crates/fbuild-build-esp/src/esp8266/mod.rs @@ -1,4 +1,4 @@ -//! ESP8266 platform build support (NodeMCU, Wemos D1, etc.) +//! ESP8266 platform build support (NodeMCU, Wemos D1, etc.) pub mod esp8266_compiler; pub mod esp8266_linker; @@ -23,9 +23,32 @@ impl crate::PlatformSupport for Esp8266PlatformSupport { inputs: &crate::provision::ProvisionInputs<'_>, mode: crate::provision::ProvisionMode, ) -> fbuild_core::Result> { - use crate::provision::{PackageKind, provision_package}; - let (toolchain, framework) = - orchestrator::esp8266_packages(inputs.project_dir, Some(inputs.env_config)); + use crate::provision::{ + PackageKind, ProvisionStatus, ProvisionedPackage, provision_package, + }; + let selected = if mode.fetches() { + Some(orchestrator::esp8266_packages(inputs.project_dir, Some(inputs.env_config)).await?) + } else { + orchestrator::esp8266_packages_offline(inputs.project_dir, Some(inputs.env_config)) + .await? + }; + let Some((toolchain, framework)) = selected else { + // Metadata or the selected platform manifest is not cached. + // These modes cannot fetch it, so report uncertainty as missing + // instead of silently reporting the adapter's default packages. + return Ok(vec![ + ProvisionedPackage::new( + PackageKind::Toolchain, + "toolchain-xtensa", + ProvisionStatus::WouldFetch, + ), + ProvisionedPackage::new( + PackageKind::Framework, + "framework-arduinoespressif8266", + ProvisionStatus::WouldFetch, + ), + ]); + }; Ok(vec![ provision_package(PackageKind::Toolchain, &toolchain, mode).await, provision_package(PackageKind::Framework, &framework, mode).await, diff --git a/crates/fbuild-build-esp/src/esp8266/orchestrator.rs b/crates/fbuild-build-esp/src/esp8266/orchestrator.rs index d59cefebb..f240369c0 100644 --- a/crates/fbuild-build-esp/src/esp8266/orchestrator.rs +++ b/crates/fbuild-build-esp/src/esp8266/orchestrator.rs @@ -39,21 +39,101 @@ fn profile_label(profile: fbuild_core::BuildProfile) -> &'static str { } } -/// The ESP8266 toolchain and Arduino framework for an env, honoring the -/// `framework-arduinoespressif8266` `platform_packages` override -/// (FastLED/fbuild#664, #681). Shared by the build and `fbuild install`, so -/// both provision the same packages (FastLED/fbuild#1433). -pub(crate) fn esp8266_packages( +/// Resolve the selected PlatformIO platform's exact framework and toolchain +/// payloads before constructing either package. Shared by build and install. +pub(crate) async fn esp8266_packages( project_dir: &Path, env_config: Option<&HashMap>, +) -> Result<( + fbuild_packages::toolchain::Esp8266Toolchain, + fbuild_packages::library::Esp8266Framework, +)> { + esp8266_packages_with_fetch(project_dir, env_config, true) + .await? + .ok_or_else(|| { + fbuild_core::FbuildError::PackageError("ESP8266 packages unavailable".into()) + }) +} + +pub(crate) async fn esp8266_packages_offline( + project_dir: &Path, + env_config: Option<&HashMap>, +) -> Result< + Option<( + fbuild_packages::toolchain::Esp8266Toolchain, + fbuild_packages::library::Esp8266Framework, + )>, +> { + esp8266_packages_with_fetch(project_dir, env_config, false).await +} + +async fn esp8266_packages_with_fetch( + project_dir: &Path, + env_config: Option<&HashMap>, + fetch: bool, +) -> Result< + Option<( + fbuild_packages::toolchain::Esp8266Toolchain, + fbuild_packages::library::Esp8266Framework, + )>, +> { + let registry_overrides = if let Some(env) = env_config { + if fetch { + Some( + crate::package_override::resolve_registry_overrides( + project_dir, + env, + "espressif8266", + &["framework-arduinoespressif8266", "toolchain-xtensa"], + &[], + ) + .await?, + ) + } else { + crate::package_override::resolve_registry_overrides_offline( + project_dir, + env, + "espressif8266", + &["framework-arduinoespressif8266", "toolchain-xtensa"], + &[], + ) + .await? + } + } else { + Some(HashMap::new()) + }; + Ok(registry_overrides.map(|registry_overrides| { + esp8266_packages_from_resolved(project_dir, env_config, ®istry_overrides) + })) +} + +fn esp8266_packages_from_resolved( + project_dir: &Path, + env_config: Option<&HashMap>, + registry_overrides: &HashMap, ) -> ( fbuild_packages::toolchain::Esp8266Toolchain, fbuild_packages::library::Esp8266Framework, ) { - let toolchain = fbuild_packages::toolchain::Esp8266Toolchain::new(project_dir); - let override_pin = env_config.and_then(|env| { - crate::package_override::resolve_override(env, "framework-arduinoespressif8266") - }); + let toolchain_pin = registry_overrides + .get("toolchain-xtensa") + .cloned() + .or_else(|| { + env_config + .and_then(|env| crate::package_override::resolve_override(env, "toolchain-xtensa")) + }); + let toolchain = match toolchain_pin { + Some(pin) => fbuild_packages::toolchain::Esp8266Toolchain::with_override(project_dir, pin), + None => fbuild_packages::toolchain::Esp8266Toolchain::new(project_dir), + }; + let override_pin = registry_overrides + .get("framework-arduinoespressif8266") + .cloned() + .or_else(|| { + env_config.and_then(|env| { + crate::package_override::resolve_override(env, "framework-arduinoespressif8266") + }) + }); let framework = match override_pin { Some(o) => fbuild_packages::library::Esp8266Framework::with_override(project_dir, o), None => fbuild_packages::library::Esp8266Framework::new(project_dir), @@ -61,6 +141,38 @@ pub(crate) fn esp8266_packages( (toolchain, framework) } +/// Read the registry identity already resolved by `esp8266_packages` for +/// build output and the fast-path fingerprint. This is cache-only. +async fn selected_platform_identity( + project_dir: &Path, + requested: &str, +) -> Result> { + use fbuild_core::platformio_package::{PackageKind, host_system, parse_package_spec}; + + let spec = parse_package_spec(requested) + .map_err(|error| fbuild_core::FbuildError::PackageError(error.to_string()))?; + let Some(registry) = spec + .registry() + .filter(|registry| registry.requirement.is_some()) + else { + return Ok(None); + }; + let host = host_system(fbuild_core::platform::host::current()).ok_or_else(|| { + fbuild_core::FbuildError::PackageError("unsupported PlatformIO host".into()) + })?; + let cache_root = fbuild_packages::Cache::new(project_dir).platforms_dir(); + fbuild_packages::platformio_registry::RegistryClient::default() + .resolve_cached(registry, PackageKind::Platform, host, &cache_root, false) + .await + .map_err(|error| fbuild_core::FbuildError::PackageError(error.to_string()))? + .ok_or_else(|| { + fbuild_core::FbuildError::PackageError(format!( + "selected ESP8266 platform `{requested}` missing from registry cache" + )) + }) + .map(Some) +} + #[async_trait::async_trait] impl BuildOrchestrator for Esp8266Orchestrator { fn platform(&self) -> Platform { @@ -82,7 +194,38 @@ impl BuildOrchestrator for Esp8266Orchestrator { let (toolchain, framework) = esp8266_packages( ¶ms.project_dir, ctx.config.get_env_config(¶ms.env_name).ok(), - ); + ) + .await?; + let toolchain_info = fbuild_packages::Package::get_info(&toolchain); + let framework_info = fbuild_packages::Package::get_info(&framework); + let requested_platform = ctx + .config + .get_env_config(¶ms.env_name)? + .get("platform") + .cloned() + .unwrap_or_else(|| "espressif8266".to_string()); + let selected_platform = + selected_platform_identity(¶ms.project_dir, &requested_platform).await?; + ctx.build_log + .push(format!("ESP8266 requested platform: {requested_platform}")); + if let Some(selected) = &selected_platform { + ctx.build_log.push(format!( + "ESP8266 resolved platform: {}/{}@{}: {} (sha256 {})", + selected.owner, selected.name, selected.version, selected.url, selected.sha256, + )); + } + ctx.build_log.push(format!( + "ESP8266 toolchain-xtensa@{}: {} (sha256 {})", + toolchain_info.version, + toolchain_info.url, + toolchain_info.checksum.as_deref().unwrap_or("unverified"), + )); + ctx.build_log.push(format!( + "ESP8266 framework-arduinoespressif8266@{}: {} (sha256 {})", + framework_info.version, + framework_info.url, + framework_info.checksum.as_deref().unwrap_or("unverified"), + )); let _toolchain_dir = fbuild_packages::Package::ensure_installed(&toolchain).await?; tracing::info!("ESP8266 toolchain ready"); @@ -148,11 +291,42 @@ impl BuildOrchestrator for Esp8266Orchestrator { crate::eh_frame_policy::EhFramePolicy::Preserve => "preserve".to_string(), }), extra: Some(std::collections::BTreeMap::from([ + ("requested_platform".to_string(), requested_platform.clone()), + ( + "resolved_platform".to_string(), + selected_platform + .as_ref() + .map(|payload| { + format!( + "{}/{}@{}#{}", + payload.owner, payload.name, payload.version, payload.sha256 + ) + }) + .unwrap_or_default(), + ), ( "flash_mode".to_string(), ctx.board.flash_mode.clone().unwrap_or_default(), ), ("flash_freq".to_string(), flash_freq.clone()), + ( + "toolchain_package".to_string(), + format!( + "{}@{}#{}", + toolchain_info.url, + toolchain_info.version, + toolchain_info.checksum.as_deref().unwrap_or("unverified") + ), + ), + ( + "framework_package".to_string(), + format!( + "{}@{}#{}", + framework_info.url, + framework_info.version, + framework_info.checksum.as_deref().unwrap_or("unverified") + ), + ), ])), }, &ctx, @@ -531,6 +705,205 @@ pub fn is_esp8266_project(project_dir: &Path, env_name: &str) -> bool { #[cfg(test)] mod tests { use super::*; + use fbuild_packages::Package as _; + + #[test] + fn explicit_toolchain_registry_pin_does_not_use_fixed_github_toolchain() { + let temp = tempfile::tempdir().unwrap(); + let env = HashMap::from([ + ("platform".to_string(), "espressif8266".to_string()), + ( + "platform_packages".to_string(), + "platformio/toolchain-xtensa@2.100300.220621".to_string(), + ), + ]); + let registry_overrides = HashMap::from([( + "toolchain-xtensa".to_string(), + fbuild_config::PackageOverride { + url: "https://dl.registry.platformio.org/download/platformio/tool/toolchain-xtensa/2.100300.220621/toolchain-xtensa-linux_x86_64-2.100300.220621.tar.gz".to_string(), + version: "2.100300.220621".to_string(), + checksum: Some("a3d51bebcfaa2f5cca154956fee3e9270b6d0e9c5d51de6034a86aaa606ea8a5".to_string()), + }, + )]); + let (toolchain, _) = + esp8266_packages_from_resolved(temp.path(), Some(&env), ®istry_overrides); + assert_eq!(toolchain.get_info().version, "2.100300.220621"); + assert!( + toolchain + .get_info() + .url + .contains("dl.registry.platformio.org") + ); + assert_eq!( + toolchain.get_info().checksum, + registry_overrides["toolchain-xtensa"].checksum + ); + } + + #[test] + fn framework_url_override_precedes_selected_platform_manifest() { + let temp = tempfile::tempdir().unwrap(); + let url = "https://example.test/esp8266-framework.tar.gz"; + let env = HashMap::from([ + ("platform".to_string(), "espressif8266@4.0.1".to_string()), + ( + "platform_packages".to_string(), + format!("framework-arduinoespressif8266@{url}"), + ), + ]); + let registry_overrides = HashMap::from([( + "toolchain-xtensa".to_string(), + fbuild_config::PackageOverride::new( + "https://example.test/toolchain.tar.gz", + "2.100300.220621", + ), + )]); + let (toolchain, framework) = + esp8266_packages_from_resolved(temp.path(), Some(&env), ®istry_overrides); + assert_eq!(framework.get_info().url, url); + assert_eq!(toolchain.get_info().version, "2.100300.220621"); + } + + #[tokio::test] + async fn unpinned_platform_preserves_framework_archive_override() { + let temp = tempfile::tempdir().unwrap(); + let url = "https://example.test/esp8266-framework.tar.gz"; + let env = HashMap::from([ + ("platform".to_string(), "espressif8266".to_string()), + ( + "platform_packages".to_string(), + format!("framework-arduinoespressif8266@{url}"), + ), + ]); + let (_, framework) = esp8266_packages(temp.path(), Some(&env)).await.unwrap(); + assert_eq!(framework.get_info().url, url); + } + + #[tokio::test] + async fn unrelated_registry_platform_fails_before_package_install() { + let temp = tempfile::tempdir().unwrap(); + let env = HashMap::from([("platform".to_string(), "unknown8266@4.0.1".to_string())]); + let error = esp8266_packages(temp.path(), Some(&env)) + .await + .err() + .expect("wrong platform must fail"); + assert!( + error + .to_string() + .contains("expected PlatformIO platform `espressif8266`") + ); + } + + #[tokio::test] + async fn unsupported_registry_package_fails_before_download() { + let temp = tempfile::tempdir().unwrap(); + let env = HashMap::from([ + ("platform".to_string(), "espressif8266".to_string()), + ( + "platform_packages".to_string(), + "platformio/tool-esptoolpy@1.30000.201119".to_string(), + ), + ]); + let error = esp8266_packages(temp.path(), Some(&env)) + .await + .err() + .expect("unsupported package must fail"); + assert!(error.to_string().contains("tool-esptoolpy")); + assert!(error.to_string().contains("not supported")); + } + + #[tokio::test(flavor = "multi_thread")] + #[ignore = "downloads PlatformIO ESP8266 4.0.1 platform, GCC, and Arduino framework"] + async fn pinned_esp8266_401_builds_with_selected_registry_packages() { + let backend = crate::compile_backend::CompileBackend::start() + .await + .expect("compile backend starts"); + crate::compile_backend::install_global(backend); + let project = tempfile::tempdir().unwrap(); + std::fs::create_dir_all(project.path().join("src")).unwrap(); + std::fs::write( + project.path().join("platformio.ini"), + "[env:esp8266]\nplatform = espressif8266@4.0.1\nboard = nodemcuv2\nframework = arduino\n", + ) + .unwrap(); + std::fs::copy( + concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../tests/platform/esp8266/src/main.ino" + ), + project.path().join("src/main.ino"), + ) + .unwrap(); + let build_dir = fbuild_paths::BuildLayout::new( + project.path().to_path_buf(), + "esp8266".into(), + fbuild_core::BuildProfile::Release, + ) + .resolve(); + let params = BuildParams { + project_dir: project.path().to_path_buf(), + env_name: "esp8266".into(), + clean_all: false, + clean_only: false, + clean: false, + profile: fbuild_core::BuildProfile::Release, + build_dir, + verbose: false, + jobs: Some(2), + generate_compiledb: false, + compiledb_only: false, + log_sender: None, + symbol_analysis: false, + symbol_analysis_path: None, + no_timestamp: true, + src_dir: None, + pio_env: Default::default(), + extra_build_flags: Vec::new(), + watch_set_cache: None, + bloat_analysis: false, + caller_path: None, + }; + let built = Esp8266Orchestrator.build(¶ms).await.unwrap(); + assert!(built.success); + assert!(built.elf_path.as_ref().is_some_and(|path| path.is_file())); + let config = + fbuild_config::PlatformIOConfig::from_path(&project.path().join("platformio.ini")) + .unwrap(); + let env = config.get_env_config("esp8266").unwrap(); + let (cached_toolchain, cached_framework) = + esp8266_packages_offline(project.path(), Some(env)) + .await + .unwrap() + .expect("installed platform and payload metadata resolve offline"); + assert_eq!( + fbuild_packages::Package::get_info(&cached_toolchain).version, + "2.100300.220621" + ); + assert_eq!( + fbuild_packages::Package::get_info(&cached_framework).version, + "3.30002.0" + ); + let (_, framework) = esp8266_packages(project.path(), Some(env)).await.unwrap(); + let core_version = + std::fs::read_to_string(framework.get_core_dir("esp8266").join("core_version.h")) + .unwrap(); + assert!(core_version.contains("ARDUINO_ESP8266_RELEASE \"3.0.2\"")); + let log = built.build_log.into_lines().join("\n"); + assert!( + log.contains("ESP8266 requested platform: espressif8266@4.0.1"), + "{log}" + ); + assert!( + log.contains("ESP8266 resolved platform: platformio/espressif8266@4.0.1"), + "{log}" + ); + assert!( + log.contains("framework-arduinoespressif8266@3.30002.0"), + "{log}" + ); + assert!(log.contains("toolchain-xtensa@2.100300.220621"), "{log}"); + assert!(!log.contains("version pin `4.0.1` is ignored"), "{log}"); + } #[test] fn test_esp8266_orchestrator_platform() { diff --git a/crates/fbuild-config/src/platform_packages.rs b/crates/fbuild-config/src/platform_packages.rs index 6b92bdb56..4f3a3ab6c 100644 --- a/crates/fbuild-config/src/platform_packages.rs +++ b/crates/fbuild-config/src/platform_packages.rs @@ -500,6 +500,23 @@ mod tests { assert!(warnings[0].contains("platform_packages")); } + #[test] + fn resolved_esp8266_platform_and_packages_are_not_reported_as_ignored() { + let config = env(&[ + ("platform", "espressif8266@4.0.1"), + ( + "platform_packages", + "platformio/framework-arduinoespressif8266@3.30002.0\nplatformio/toolchain-xtensa@2.100300.220621", + ), + ]); + let warnings = ignored_version_pins_with_resolved_packages( + &config, + Some(fbuild_core::Platform::Espressif8266), + &["framework-arduinoespressif8266", "toolchain-xtensa"], + ); + assert!(warnings.is_empty(), "{warnings:?}"); + } + #[test] fn resolved_framework_pin_does_not_warn_but_unhandled_package_still_does() { let config = env(&[ diff --git a/crates/fbuild-core/tests/platformio_esp8266_resolution.rs b/crates/fbuild-core/tests/platformio_esp8266_resolution.rs new file mode 100644 index 000000000..e3ed81257 --- /dev/null +++ b/crates/fbuild-core/tests/platformio_esp8266_resolution.rs @@ -0,0 +1,145 @@ +//! Offline ESP8266 registry fixtures from platformio/espressif8266@4.0.1. + +use fbuild_core::platformio_package::{ + PackageKind, parse_package_spec, require_platform_package, resolve_platform_requirements, + resolve_registry_json, +}; + +const PLATFORM_METADATA: &str = r#"{"name":"espressif8266","owner":{"username":"platformio"},"versions":[{"name":"4.0.1","files":[{"system":"*","download_url":"https://dl.registry.platformio.org/download/platformio/platform/espressif8266/4.0.1/espressif8266-4.0.1.tar.gz","checksum":{"sha256":"a5a0fbbab19d1c993edd95b782a6c29e70b585c0140d93009b4e0bc8347df15c"}}]}]}"#; +const MANIFEST: &str = r#"{ + "version":"4.0.1", + "packages":{ + "toolchain-xtensa":{"type":"toolchain","owner":"platformio","version":"~2.100300.0","optionalVersions":["~1.40802.0"]}, + "framework-arduinoespressif8266":{"type":"framework","optional":true,"owner":"platformio","version":"~3.30002.0"}, + "framework-esp8266-rtos-sdk":{"type":"framework","optional":true,"owner":"platformio","version":">=1.5.0-beta"}, + "framework-esp8266-nonos-sdk":{"type":"framework","optional":true,"owner":"platformio","version":">=2.1.0"}, + "tool-esptool":{"type":"uploader","owner":"platformio","version":"<2"}, + "tool-esptoolpy":{"type":"uploader","owner":"platformio","version":"~1.30000.0"}, + "tool-mkspiffs":{"type":"uploader","optional":true,"owner":"platformio","version":"~1.200.0"}, + "tool-mklittlefs":{"type":"uploader","optional":true,"owner":"platformio","version":"~1.203.0"} + } +}"#; +const FRAMEWORK_METADATA: &str = r#"{"name":"framework-arduinoespressif8266","owner":{"username":"platformio"},"versions":[{"name":"3.30002.0","files":[{"system":"*","download_url":"https://dl.registry.platformio.org/download/platformio/tool/framework-arduinoespressif8266/3.30002.0/framework-arduinoespressif8266-3.30002.0.tar.gz","checksum":{"sha256":"ba4bf3467a4b09d32d73ea06fad0864dac3f84523bb02c27a3a97b66d593fa65"}}]}]}"#; +const TOOLCHAIN_METADATA: &str = r#"{"name":"toolchain-xtensa","owner":{"username":"platformio"},"versions":[{"name":"2.100300.220621","files":[{"system":["linux_x86_64"],"download_url":"https://dl.registry.platformio.org/download/platformio/tool/toolchain-xtensa/2.100300.220621/toolchain-xtensa-linux_x86_64-2.100300.220621.tar.gz","checksum":{"sha256":"a3d51bebcfaa2f5cca154956fee3e9270b6d0e9c5d51de6034a86aaa606ea8a5"}}]}]}"#; + +#[test] +fn espressif8266_aliases_resolve_exact_platform_and_packages() { + let mut platform_identity = None; + for alias in [ + "espressif8266@4.0.1", + "platformio/espressif8266@4.0.1", + "platform/platformio/espressif8266@4.0.1", + ] { + let spec = parse_package_spec(alias).unwrap(); + let payload = resolve_registry_json( + spec.registry().unwrap(), + PackageKind::Platform, + "linux_x86_64", + PLATFORM_METADATA, + ) + .unwrap(); + assert_eq!(payload.version, "4.0.1"); + assert_eq!( + payload.sha256, + "a5a0fbbab19d1c993edd95b782a6c29e70b585c0140d93009b4e0bc8347df15c" + ); + assert_eq!( + payload.url, + "https://dl.registry.platformio.org/download/platformio/platform/espressif8266/4.0.1/espressif8266-4.0.1.tar.gz" + ); + assert_eq!( + platform_identity.get_or_insert_with(|| payload.cache_identity()), + &payload.cache_identity() + ); + } + + let requirements = resolve_platform_requirements(MANIFEST, &[]).unwrap(); + let framework = + require_platform_package(&requirements, "framework-arduinoespressif8266").unwrap(); + assert_eq!(framework.kind, PackageKind::Framework); + let framework_payload = resolve_registry_json( + framework.spec.registry().unwrap(), + framework.kind, + "linux_x86_64", + FRAMEWORK_METADATA, + ) + .unwrap(); + assert_eq!(framework_payload.version, "3.30002.0"); + assert_eq!( + framework_payload.sha256, + "ba4bf3467a4b09d32d73ea06fad0864dac3f84523bb02c27a3a97b66d593fa65" + ); + let toolchain = require_platform_package(&requirements, "toolchain-xtensa").unwrap(); + assert_eq!(toolchain.kind, PackageKind::Tool); + let toolchain_payload = resolve_registry_json( + toolchain.spec.registry().unwrap(), + toolchain.kind, + "linux_x86_64", + TOOLCHAIN_METADATA, + ) + .unwrap(); + assert_eq!(toolchain_payload.version, "2.100300.220621"); + assert_eq!( + toolchain_payload.sha256, + "a3d51bebcfaa2f5cca154956fee3e9270b6d0e9c5d51de6034a86aaa606ea8a5" + ); + assert_ne!( + framework_payload.cache_identity(), + toolchain_payload.cache_identity() + ); + assert_ne!( + platform_identity.as_ref().unwrap(), + &framework_payload.cache_identity() + ); + assert_ne!( + platform_identity.unwrap(), + toolchain_payload.cache_identity() + ); + assert!( + resolve_registry_json( + toolchain.spec.registry().unwrap(), + toolchain.kind, + "freebsd_x86_64", + TOOLCHAIN_METADATA + ) + .is_err() + ); +} + +#[test] +fn unavailable_esp8266_versions_and_host_do_not_fall_back() { + let unavailable_platform = parse_package_spec("espressif8266@4.0.2").unwrap(); + assert!( + resolve_registry_json( + unavailable_platform.registry().unwrap(), + PackageKind::Platform, + "linux_x86_64", + PLATFORM_METADATA + ) + .is_err() + ); + let unavailable_toolchain = + parse_package_spec("platformio/toolchain-xtensa@2.100300.220622").unwrap(); + assert!( + resolve_registry_json( + unavailable_toolchain.registry().unwrap(), + PackageKind::Tool, + "linux_x86_64", + TOOLCHAIN_METADATA + ) + .is_err() + ); +} + +#[test] +fn explicit_framework_pin_precedes_esp8266_manifest_requirement() { + let override_spec = + parse_package_spec("tool/platformio/framework-arduinoespressif8266@3.30002.0").unwrap(); + let requirements = resolve_platform_requirements(MANIFEST, &[override_spec]).unwrap(); + let framework = + require_platform_package(&requirements, "framework-arduinoespressif8266").unwrap(); + assert_eq!( + framework.spec.registry().unwrap().requirement.as_deref(), + Some("3.30002.0") + ); +} diff --git a/crates/fbuild-toolchain/src/toolchain/esp8266.rs b/crates/fbuild-toolchain/src/toolchain/esp8266.rs index 16c06d6d8..77db347a9 100644 --- a/crates/fbuild-toolchain/src/toolchain/esp8266.rs +++ b/crates/fbuild-toolchain/src/toolchain/esp8266.rs @@ -37,6 +37,13 @@ impl Esp8266Toolchain { } } + /// Use the host-specific archive selected by a PlatformIO registry pin. + pub fn with_override(project_dir: &Path, ovr: fbuild_config::PackageOverride) -> Self { + let mut toolchain = Self::new(project_dir); + toolchain.base = toolchain.base.with_override(ovr); + toolchain + } + #[cfg(test)] fn with_cache_root(project_dir: &Path, cache_root: &Path) -> Self { let (url, checksum) = platform_package(); diff --git a/tests/platform/esp8266/platformio.ini b/tests/platform/esp8266/platformio.ini index 3fb6d9753..4ee9fed78 100644 --- a/tests/platform/esp8266/platformio.ini +++ b/tests/platform/esp8266/platformio.ini @@ -7,3 +7,9 @@ platform = espressif8266@4.2.1 board = nodemcuv2 framework = arduino + +# Registry-pin regression fixture (FastLED/fbuild#1496): Arduino core 3.0.2 +# and PlatformIO's matching Xtensa GCC/SDK, not fbuild's 3.1.2 default. +[env:esp8266-401] +extends = env:esp8266 +platform = espressif8266@4.0.1