diff --git a/crates/fbuild-build-esp/src/esp32/orchestrator/packages_platform.rs b/crates/fbuild-build-esp/src/esp32/orchestrator/packages_platform.rs index 9848b5851..861bf37b1 100644 --- a/crates/fbuild-build-esp/src/esp32/orchestrator/packages_platform.rs +++ b/crates/fbuild-build-esp/src/esp32/orchestrator/packages_platform.rs @@ -116,12 +116,65 @@ pub(super) async fn pioarduino_platform( ), )); } - warn_unhonored_platform_pin(env_config); + if let Some(source) = platform_repository_source(env_config)? { + // A `.git#` pin is resolved to an immutable commit and installed + // under that commit's archive URL, so two refs can never share a + // cached payload, and a ref is never replaced by stable + // (FastLED/fbuild#1662). The commit lock is reused by later builds. + if !fetch { + return Ok(None); + } + let cache_root = fbuild_packages::Cache::new(project_dir).platforms_dir(); + let resolved = fbuild_packages::platformio_repository::resolve_cached_source( + &source, + &cache_root, + false, + ) + .await + .map_err(|error| { + fbuild_core::FbuildError::PackageError(format!( + "cannot resolve ESP32 platform `{}`: {error}", + requested_platform(env_config) + )) + })?; + tracing::info!( + "resolved requested ESP32 platform {} to {}", + requested_platform(env_config), + resolved.archive.url + ); + return Ok(Some( + fbuild_packages::library::Esp32Platform::with_override(project_dir, resolved.archive), + )); + } + reject_unhonored_platform_pin(env_config)?; Ok(Some(fbuild_packages::library::Esp32Platform::new( project_dir, ))) } +fn requested_platform(env_config: Option<&HashMap>) -> &str { + env_config + .and_then(|env| env.get("platform")) + .map_or("", |value| value.trim()) +} + +/// A `platform = [#ref]` pin. Archive URLs are handled by +/// `resolve_platform_override`; registry names by [`platform_registry_spec`]. +fn platform_repository_source( + env_config: Option<&HashMap>, +) -> Result> { + let platform = requested_platform(env_config); + if !platform.contains("://") && !platform.starts_with("git@") { + return Ok(None); + } + match parse_package_spec(platform) { + Ok(spec) if matches!(spec.source, PackageSource::Repository { .. }) => { + Ok(Some(spec.source)) + } + _ => Ok(None), + } +} + /// The Arduino framework package. Override precedence (FastLED/fbuild#672): /// 1. `platform_packages = framework-arduinoespressif32@#` wins /// outright — consumer-supplied URL replaces the platform.json-derived @@ -256,18 +309,20 @@ pub(super) async fn ensure_sdk_libs( /// Name an unsupported non-archive URL instead of dropping it silently /// (FastLED/fbuild#1407). Registry aliases are resolved above; other URLs /// still fall back to the pioarduino stable platform. -fn warn_unhonored_platform_pin(env_config: Option<&HashMap>) { - let Some(value) = env_config.and_then(|env| env.get("platform")) else { - return; - }; - let value = value.trim(); +/// A URL pin that is neither a release archive nor a repository cannot be +/// honored. Building pioarduino stable instead would report a green build of +/// an SDK the project did not ask for (FastLED/fbuild#1662). +fn reject_unhonored_platform_pin(env_config: Option<&HashMap>) -> Result<()> { + let value = requested_platform(env_config); if value.contains("://") { - tracing::warn!( - "platform pin `{value}` is not a downloadable archive URL; building with the \ - pioarduino stable platform instead. Pin a release with `platform = \ - https://github.com/pioarduino/platform-espressif32/releases/download//platform-espressif32.zip`" - ); + return Err(fbuild_core::FbuildError::PackageError(format!( + "ESP32 platform pin `{value}` is neither a release archive nor a repository URL. \ + Pin a release with `platform = \ + https://github.com/pioarduino/platform-espressif32/releases/download//platform-espressif32.zip` \ + or a repository with `platform = https://github.com//.git#`" + ))); } + Ok(()) } /// Provision the managed `tool-esptoolpy` package (the tasmota PyInstaller @@ -382,6 +437,77 @@ pub(super) async fn esptool_package( mod registry_framework_override_tests { use super::*; + fn platform_env(platform: &str) -> HashMap { + HashMap::from([("platform".into(), platform.into())]) + } + + #[test] + fn repository_ref_pin_is_a_repository_source() { + let env = platform_env("https://github.com/pioarduino/platform-espressif32.git#prep_IDF6"); + let Some(PackageSource::Repository { url, reference }) = + platform_repository_source(Some(&env)).unwrap() + else { + panic!("expected a repository source"); + }; + assert_eq!( + url, + "https://github.com/pioarduino/platform-espressif32.git" + ); + assert_eq!(reference.as_deref(), Some("prep_IDF6")); + } + + #[test] + fn archive_and_registry_pins_are_not_repository_sources() { + for platform in [ + "https://github.com/pioarduino/platform-espressif32/releases/download/55.03.311/platform-espressif32.zip", + "espressif32@6.9.0", + "espressif32", + ] { + assert!( + platform_repository_source(Some(&platform_env(platform))) + .unwrap() + .is_none(), + "{platform}" + ); + } + } + + #[test] + fn unresolvable_url_pin_is_an_error_not_a_stable_fallback() { + let env = platform_env("https://example.test/not-a-platform"); + assert!(reject_unhonored_platform_pin(Some(&env)).is_err()); + assert!(reject_unhonored_platform_pin(Some(&platform_env("espressif32"))).is_ok()); + assert!(reject_unhonored_platform_pin(None).is_ok()); + } + + #[tokio::test] + async fn cached_commit_lock_selects_that_commits_archive_without_network() { + let tmp = tempfile::TempDir::new().unwrap(); + let env = platform_env("https://github.com/pioarduino/platform-espressif32.git#prep_IDF6"); + let source = platform_repository_source(Some(&env)).unwrap().unwrap(); + let commit = "0123456789abcdef0123456789abcdef01234567"; + let request = serde_json::to_vec(&source).unwrap(); + let key = format!("{:x}", Sha256::digest(request)); + let cache_root = fbuild_packages::Cache::new(tmp.path()).platforms_dir(); + let locks = cache_root.join("platformio-source-locks"); + std::fs::create_dir_all(&locks).unwrap(); + std::fs::write( + locks.join(format!("{key}.json")), + serde_json::to_vec(&fbuild_core::platformio_package::PackageLock::Repository { + url: "https://github.com/pioarduino/platform-espressif32.git".into(), + commit: commit.into(), + }) + .unwrap(), + ) + .unwrap(); + let platform = pioarduino_platform(tmp.path(), Some(&env), true) + .await + .unwrap() + .unwrap(); + let url = fbuild_packages::Package::get_info(&platform).url; + assert!(url.contains(commit), "{url}"); + } + #[test] fn framework_registry_pin_selects_registry_platform_for_unversioned_alias() { let env = HashMap::from([