From b001d0ec1b4ec302574f8940e0b9dfe1ba6b3b6d Mon Sep 17 00:00:00 2001 From: Zach Vorhies Date: Sat, 5 Sep 2026 16:06:21 -0700 Subject: [PATCH 1/2] fix(samd): fetch the core from Adafruit's bundle so submodules come with it Closes #1400. `samd-core` was fetched from GitHub's auto-generated source archive: https://github.com/adafruit/ArduinoCore-samd/archive/refs/tags/1.7.16.tar.gz Those archives omit submodules by design, and tag 1.7.16 declares two under `libraries/` -- `Adafruit_TinyUSB_Arduino` and `Adafruit_ZeroDMA`. Until #1401 that was latent: FastLED's SAMD builds compile sketches that never include either library, so they were green for months. #1401 added the unpack-time submodule check, which fires on the *package* rather than on use, so every SAMD build began failing before a compiler ran: build error: package error: samd-core unpacked without its submodule contents. These directories are declared in .gitmodules and came out empty: - libraries/Adafruit_TinyUSB_Arduino - libraries/Adafruit_ZeroDMA That took out metro_m4, samd21, samd21_zero, samd51j and samd51p downstream in FastLED the moment it pinned 2.5.22. #1400 left open the question of whether Adafruit's board-index bundle actually carries the submodule contents, since Adafruit publishes no release asset for 1.7.16. It does. The bundle referenced by `package_adafruit_index.json` contains 368 files under `libraries/Adafruit_TinyUSB_Arduino/` (including `Adafruit_TinyUSB.h` and `tusb.h`) and 25 under `libraries/Adafruit_ZeroDMA/`, so this is a real fix rather than a way to quiet the check. It also carries no `.gitmodules`, which is what #1401 already treats as clean -- it is a prepared bundle, not a git archive. The bundle is served from a GitHub Pages site rather than an immutable release asset, so the SHA-256 from the package index is now pinned and verified on download; the old URL passed `None`. `find_core_root` needed no change -- it scans for any subdirectory containing `cores/`, so the top-level rename from `ArduinoCore-samd-1.7.16/` to `adafruit-samd-1.7.16/` is handled generically. The doc comment and its test are updated to match, and `.tar.bz2` was already routed to `extract_tar_bz2` by `extractor::extract`, which dispatches on the filename that `download_file_with_progress` derives from the URL. Verified end to end: `fbuild build tests/platform/samd21 -e samd21` succeeds (flash 11420 bytes, ram 3828 bytes), unpacking to `adafruit-samd-1.7.16/cores/arduino/` with the submodule check passing. Two tests pin the invariant so a future edit cannot quietly reintroduce #1400: one rejects a `/archive/refs/` URL form, one requires the checksum to stay pinned. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01MLhWkMfzLjrnLTDMBE6Fj9 --- .../fbuild-library/src/library/samd_core.rs | 47 ++++++++++++++++--- uv.lock | 2 +- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/crates/fbuild-library/src/library/samd_core.rs b/crates/fbuild-library/src/library/samd_core.rs index f0b1230f1..d747a3fa3 100644 --- a/crates/fbuild-library/src/library/samd_core.rs +++ b/crates/fbuild-library/src/library/samd_core.rs @@ -1,6 +1,7 @@ //! Adafruit SAMD core (ArduinoCore-samd) framework package. //! -//! Downloads and manages the Adafruit SAMD core for SAMD21/SAMD51 boards from GitHub. +//! Downloads and manages the Adafruit SAMD core for SAMD21/SAMD51 boards from +//! Adafruit's Arduino board index. //! Provides paths to: `cores/arduino`, `variants/`, and `libraries/`. use std::path::{Path, PathBuf}; @@ -8,8 +9,17 @@ use std::path::{Path, PathBuf}; use crate::{CacheSubdir, Framework, PackageBase, PackageInfo}; const SAMD_CORE_VERSION: &str = "1.7.16"; +// Adafruit's board-index bundle, not GitHub's auto-generated source archive. +// The GitHub archive omits the two submodules the tag declares +// (`libraries/Adafruit_TinyUSB_Arduino`, `libraries/Adafruit_ZeroDMA`), so +// since the unpack-time submodule check (#1401) every SAMD build failed with +// "samd-core unpacked without its submodule contents" (#1400). Adafruit +// publishes no release asset for 1.7.16, but the archive its Arduino package +// index points at is prepared with both trees populated. URL and SHA-256 are +// taken verbatim from `package_adafruit_index.json`. const SAMD_CORE_URL: &str = - "https://github.com/adafruit/ArduinoCore-samd/archive/refs/tags/1.7.16.tar.gz"; + "https://adafruit.github.io/arduino-board-index/boards/adafruit-samd-1.7.16.tar.bz2"; +const SAMD_CORE_SHA256: &str = "56a099437b0fc6d160922e34a49147a05600d028d3c780c2f575c4d50106f9e0"; /// Adafruit SAMD core framework manager. pub struct SamdCores { @@ -25,7 +35,7 @@ impl SamdCores { SAMD_CORE_VERSION, SAMD_CORE_URL, SAMD_CORE_URL, - None, + Some(SAMD_CORE_SHA256), CacheSubdir::Platforms, project_dir, ), @@ -44,7 +54,7 @@ impl SamdCores { SAMD_CORE_VERSION, SAMD_CORE_URL, SAMD_CORE_URL, - None, + Some(SAMD_CORE_SHA256), CacheSubdir::Platforms, project_dir, ) @@ -61,7 +71,7 @@ impl SamdCores { SAMD_CORE_VERSION, SAMD_CORE_URL, SAMD_CORE_URL, - None, + Some(SAMD_CORE_SHA256), CacheSubdir::Platforms, project_dir, cache_root, @@ -172,7 +182,7 @@ impl Framework for SamdCores { /// Find the actual core root inside an extracted archive. /// -/// GitHub archives extract as `ArduinoCore-samd-1.7.16/` with the core inside. +/// The bundle extracts as `adafruit-samd-1.7.16/` with the core inside. fn find_core_root(install_dir: &Path) -> PathBuf { if install_dir.join("cores").exists() { return install_dir.to_path_buf(); @@ -243,6 +253,29 @@ mod tests { assert!(!core.is_installed()); } + /// The core must not come from a GitHub auto-generated source archive. + /// + /// Those omit submodules by design, and tag 1.7.16 declares two under + /// `libraries/` (`Adafruit_TinyUSB_Arduino`, `Adafruit_ZeroDMA`). Going + /// back to that URL form reinstates #1400: the unpack-time submodule + /// check added in #1401 fails every SAMD build before a compiler runs. + #[test] + fn test_core_url_is_not_a_github_source_archive() { + assert!( + !SAMD_CORE_URL.contains("/archive/refs/"), + "samd-core must use Adafruit's prepared bundle, not a GitHub \ + source archive (see #1400); got {SAMD_CORE_URL}" + ); + } + + /// A pinned bundle is only trustworthy with a checksum, and the bundle is + /// served from a GitHub Pages site rather than an immutable release asset. + #[test] + fn test_core_checksum_is_pinned() { + assert_eq!(SAMD_CORE_SHA256.len(), 64); + assert!(SAMD_CORE_SHA256.chars().all(|c| c.is_ascii_hexdigit())); + } + #[test] fn test_find_core_root_direct() { let tmp = tempfile::TempDir::new().unwrap(); @@ -253,7 +286,7 @@ mod tests { #[test] fn test_find_core_root_nested() { let tmp = tempfile::TempDir::new().unwrap(); - let nested = tmp.path().join("ArduinoCore-samd-1.7.16"); + let nested = tmp.path().join("adafruit-samd-1.7.16"); std::fs::create_dir_all(nested.join("cores/arduino")).unwrap(); assert_eq!(find_core_root(tmp.path()), nested); } diff --git a/uv.lock b/uv.lock index 0d8c2e54f..9b96c7d0d 100644 --- a/uv.lock +++ b/uv.lock @@ -4,7 +4,7 @@ requires-python = ">=3.10" [[package]] name = "fbuild" -version = "2.5.21" +version = "2.5.22" source = { editable = "." } [package.dev-dependencies] From 69c127ca5c5c4a3871286f0a57ecb874d0d9fb8c Mon Sep 17 00:00:00 2001 From: Zach Vorhies Date: Sat, 5 Sep 2026 17:36:02 -0700 Subject: [PATCH 2/2] test(samd): pin the bundle URL and checksum exactly, widen the shape guard CodeRabbit caught a real hole: the shape check rejected only `/archive/refs/`, so `github.com///archive/.tar.gz` would have passed while omitting submodules just the same. That form is in live use -- `ch32v-core` fetches exactly that way -- so this was not hypothetical. Keep both checks rather than replacing one with the other, because they fail on different mistakes: - the shape guard now rejects any `github.com` URL containing `/archive/`, and survives a deliberate version bump, which is when the wrong form is most likely to come back. Release-asset URLs still pass, as esp8266's does. - exact equality on both the URL and the SHA-256 makes moving either one a deliberate edit that shows up in review. The old checksum test only checked 64 hex characters, so a wrong digest passed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01MLhWkMfzLjrnLTDMBE6Fj9 --- .../fbuild-library/src/library/samd_core.rs | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/crates/fbuild-library/src/library/samd_core.rs b/crates/fbuild-library/src/library/samd_core.rs index d747a3fa3..d51eef2f3 100644 --- a/crates/fbuild-library/src/library/samd_core.rs +++ b/crates/fbuild-library/src/library/samd_core.rs @@ -259,21 +259,39 @@ mod tests { /// `libraries/` (`Adafruit_TinyUSB_Arduino`, `Adafruit_ZeroDMA`). Going /// back to that URL form reinstates #1400: the unpack-time submodule /// check added in #1401 fails every SAMD build before a compiler runs. + /// + /// This is a shape check rather than an equality check on purpose: it + /// stays meaningful when the pin below is deliberately moved to a new + /// version, which is the moment the wrong URL form is most likely to be + /// reintroduced. Both auto-generated forms are rejected -- + /// `/archive/refs/tags/` and `/archive/` -- because both omit + /// submodules, and `ch32v-core` shows the second form is in live use. #[test] fn test_core_url_is_not_a_github_source_archive() { assert!( - !SAMD_CORE_URL.contains("/archive/refs/"), + !(SAMD_CORE_URL.starts_with("https://github.com/") + && SAMD_CORE_URL.contains("/archive/")), "samd-core must use Adafruit's prepared bundle, not a GitHub \ source archive (see #1400); got {SAMD_CORE_URL}" ); } - /// A pinned bundle is only trustworthy with a checksum, and the bundle is - /// served from a GitHub Pages site rather than an immutable release asset. + /// Both constants are pinned exactly, so moving either one has to be a + /// deliberate edit that shows up in review. + /// + /// The bundle is served from a GitHub Pages site rather than an immutable + /// release asset, so an unnoticed change to the URL without a matching + /// checksum -- or the reverse -- is the failure worth catching. #[test] - fn test_core_checksum_is_pinned() { - assert_eq!(SAMD_CORE_SHA256.len(), 64); - assert!(SAMD_CORE_SHA256.chars().all(|c| c.is_ascii_hexdigit())); + fn test_core_url_and_checksum_are_pinned() { + assert_eq!( + SAMD_CORE_URL, + "https://adafruit.github.io/arduino-board-index/boards/adafruit-samd-1.7.16.tar.bz2" + ); + assert_eq!( + SAMD_CORE_SHA256, + "56a099437b0fc6d160922e34a49147a05600d028d3c780c2f575c4d50106f9e0" + ); } #[test]