diff --git a/crates/fbuild-build-arm/src/rp2040/mod.rs b/crates/fbuild-build-arm/src/rp2040/mod.rs index 3b0bda406..0e28d5458 100644 --- a/crates/fbuild-build-arm/src/rp2040/mod.rs +++ b/crates/fbuild-build-arm/src/rp2040/mod.rs @@ -1,9 +1,10 @@ -//! RP2040/RP2350 platform build support (Raspberry Pi Pico, etc.) +//! RP2040/RP2350 platform build support (Raspberry Pi Pico, etc.) mod bluetooth_libs; mod board_props; pub mod mcu_config; pub mod orchestrator; +mod tinyusb_libs; mod uf2; pub use orchestrator::Rp2040Orchestrator; diff --git a/crates/fbuild-build-arm/src/rp2040/orchestrator.rs b/crates/fbuild-build-arm/src/rp2040/orchestrator.rs index 68b5ff79c..ecc30ae6f 100644 --- a/crates/fbuild-build-arm/src/rp2040/orchestrator.rs +++ b/crates/fbuild-build-arm/src/rp2040/orchestrator.rs @@ -322,14 +322,20 @@ impl BuildOrchestrator for Rp2040Orchestrator { .config .get_lib_deps(¶ms.env_name) .unwrap_or_default(); - // Bluetooth-only libraries cannot compile unless the menu or the - // user's build_flags enable ENABLE_CLASSIC; see `bluetooth_libs`. + // Bluetooth- and TinyUSB-only libraries cannot compile unless the + // menu or the user's build_flags enable ENABLE_CLASSIC / USE_TINYUSB; + // see `bluetooth_libs` and `tinyusb_libs`. let framework_libs = { - let mut bluetooth_defines = defines.clone(); - apply_define_flags(&ctx.user_flags, &mut bluetooth_defines); - super::bluetooth_libs::exclude_bluetooth_libraries_when_disabled( + let mut effective_defines = defines.clone(); + apply_define_flags(&ctx.user_flags, &mut effective_defines); + let libs = super::bluetooth_libs::exclude_bluetooth_libraries_when_disabled( framework.get_framework_libraries(), - &bluetooth_defines, + &effective_defines, + &declared_deps, + ); + super::tinyusb_libs::exclude_tinyusb_libraries_when_disabled( + libs, + &effective_defines, &declared_deps, ) }; diff --git a/crates/fbuild-build-arm/src/rp2040/tinyusb_libs.rs b/crates/fbuild-build-arm/src/rp2040/tinyusb_libs.rs new file mode 100644 index 000000000..6a9f64107 --- /dev/null +++ b/crates/fbuild-build-arm/src/rp2040/tinyusb_libs.rs @@ -0,0 +1,232 @@ +//! Arduino-Pico's TinyUSB-only framework library. +//! +//! Arduino-Pico bundles `Adafruit_TinyUSB_Arduino`, whose API header refuses +//! to compile unless the board's USB-stack menu selected TinyUSB: +//! +//! ```c +//! #if !defined(USE_TINYUSB) && (defined(TINYUSB_HAS_BUITLTIN_CORE_SUPPORT) && ...) +//! #error TinyUSB is not selected, please select it in "Tools->Menu->USB Stack" +//! #endif +//! ``` +//! +//! The core's own `SPI`, `SD` and `LittleFS` sources include +//! `` behind `#ifdef USE_TINYUSB`. The reconciliation +//! pass re-walks a dependent library's full source set and over-selects on +//! guards, so a FastLED 3.10.8 Pico build in `lib/` selected TinyUSB and +//! died on that `#error` (FastLED/fbuild#1719; a bare `SPI` sketch does +//! not reach it). Like the Bluetooth libraries in +//! [`super::bluetooth_libs`], a library the framework declares unbuildable in +//! this configuration is not a candidate unless `USE_TINYUSB` is defined; a +//! `lib_deps` entry still selects it. + +use std::collections::HashMap; +use std::path::Path; + +use fbuild_packages::library::FrameworkLibrary; + +/// How deep under an include dir to look for the guard. The API header sits +/// one level down (`src/arduino/Adafruit_TinyUSB_API.h`). +const MAX_HEADER_DEPTH: usize = 2; + +/// Drop framework libraries that require TinyUSB when the build does not +/// select it, keeping any library `lib_deps` names. +pub(crate) fn exclude_tinyusb_libraries_when_disabled( + libraries: Vec, + defines: &HashMap, + declared: &[String], +) -> Vec { + // The guard tests `defined(USE_TINYUSB)`, so any value counts. + if defines.contains_key("USE_TINYUSB") { + return libraries; + } + let declared: Vec = declared + .iter() + .filter_map(|entry| fbuild_library_select::declared_dep_name(entry)) + .collect(); + libraries + .into_iter() + .filter(|library| { + if declared.contains(&library.name.to_ascii_lowercase()) || !needs_tinyusb(library) { + return true; + } + tracing::info!( + library = %library.name, + "skipping framework library: it #errors unless USE_TINYUSB is defined and the \ + build does not select TinyUSB; select the TinyUSB USB stack or name it in lib_deps" + ); + false + }) + .collect() +} + +/// Whether any header of `library` errors out when `USE_TINYUSB` is undefined. +fn needs_tinyusb(library: &FrameworkLibrary) -> bool { + library + .include_dirs + .iter() + .any(|dir| dir_has_tinyusb_guard(dir, MAX_HEADER_DEPTH)) +} + +fn dir_has_tinyusb_guard(dir: &Path, depth: usize) -> bool { + let Ok(entries) = std::fs::read_dir(dir) else { + return false; + }; + entries.flatten().map(|entry| entry.path()).any(|path| { + if path.is_dir() { + depth > 0 && dir_has_tinyusb_guard(&path, depth - 1) + } else { + is_header(&path) + && std::fs::read_to_string(&path).is_ok_and(|text| errors_without_tinyusb(&text)) + } + }) +} + +fn is_header(path: &Path) -> bool { + matches!( + path.extension().and_then(|ext| ext.to_str()), + Some("h" | "hh" | "hpp" | "hxx") + ) +} + +/// An `#error` directly inside an `#if`/`#ifndef` whose condition requires +/// `USE_TINYUSB` to be undefined. Backslash-continued conditions are joined. +fn errors_without_tinyusb(text: &str) -> bool { + let mut condition: Option = None; + let mut continuing = false; + for line in text.lines() { + let trimmed = line.trim(); + if continuing { + if let Some(cond) = condition.as_mut() { + cond.push(' '); + cond.push_str(trimmed.trim_end_matches('\\')); + } + continuing = trimmed.ends_with('\\'); + continue; + } + let Some(directive) = trimmed.strip_prefix('#').map(str::trim_start) else { + continue; + }; + if directive.starts_with("if") || directive.starts_with("elif") { + condition = Some(directive.trim_end_matches('\\').to_string()); + continuing = trimmed.ends_with('\\'); + } else if directive.starts_with("error") { + if condition.as_deref().is_some_and(requires_tinyusb_undefined) { + return true; + } + } else if directive.starts_with("else") || directive.starts_with("endif") { + condition = None; + } + } + false +} + +fn requires_tinyusb_undefined(condition: &str) -> bool { + let compact: String = condition.chars().filter(|c| !c.is_whitespace()).collect(); + compact.contains("!defined(USE_TINYUSB)") + || compact.contains("!definedUSE_TINYUSB") + || compact == "ifndefUSE_TINYUSB" +} + +#[cfg(test)] +mod tests { + use super::*; + + fn library(root: &Path, name: &str, files: &[(&str, &str)]) -> FrameworkLibrary { + let dir = root.join("libraries").join(name); + let src = dir.join("src"); + for (rel, body) in files { + let path = src.join(rel); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + std::fs::write(path, body).unwrap(); + } + FrameworkLibrary { + name: name.to_string(), + dir, + include_dirs: vec![src], + source_files: Vec::new(), + } + } + + /// The real shapes from Arduino-Pico 4.5.3: the TinyUSB API header guards + /// itself with a continued `#if`, and `SPI.cpp` includes TinyUSB only + /// under `#ifdef USE_TINYUSB` (its header never mentions it). + fn fixture(root: &Path) -> Vec { + vec![ + library( + root, + "Adafruit_TinyUSB_Arduino", + &[ + ( + "Adafruit_TinyUSB.h", + "#pragma once\n#include \"arduino/Adafruit_TinyUSB_API.h\"\n", + ), + ( + "arduino/Adafruit_TinyUSB_API.h", + "#if !defined(USE_TINYUSB) && (defined(TINYUSB_HAS_BUITLTIN_CORE_SUPPORT) && \\\n !defined(ARDUINO_ARCH_ESP32))\n#error TinyUSB is not selected, please select it in \"Tools->Menu->USB Stack\"\n#endif\n", + ), + ], + ), + library( + root, + "SPI", + &[ + ("SPI.h", "#pragma once\n#include \n"), + ( + "SPI.cpp", + "#ifdef USE_TINYUSB\n#include \n#endif\n", + ), + ], + ), + ] + } + + fn names(libraries: &[FrameworkLibrary]) -> Vec<&str> { + libraries.iter().map(|lib| lib.name.as_str()).collect() + } + + #[test] + fn drops_tinyusb_when_the_usb_stack_is_not_tinyusb() { + let tmp = tempfile::TempDir::new().unwrap(); + let kept = exclude_tinyusb_libraries_when_disabled( + fixture(tmp.path()), + &HashMap::from([("ARDUINO_ARCH_RP2040".to_string(), "1".to_string())]), + &[], + ); + assert_eq!(names(&kept), ["SPI"]); + } + + #[test] + fn keeps_tinyusb_when_use_tinyusb_is_defined() { + let tmp = tempfile::TempDir::new().unwrap(); + let kept = exclude_tinyusb_libraries_when_disabled( + fixture(tmp.path()), + &HashMap::from([("USE_TINYUSB".to_string(), String::new())]), + &[], + ); + assert_eq!(names(&kept), ["Adafruit_TinyUSB_Arduino", "SPI"]); + } + + #[test] + fn keeps_declared_tinyusb_even_when_not_selected() { + let tmp = tempfile::TempDir::new().unwrap(); + let kept = exclude_tinyusb_libraries_when_disabled( + fixture(tmp.path()), + &HashMap::new(), + &["adafruit/Adafruit_TinyUSB_Arduino@^3".to_string()], + ); + assert_eq!(names(&kept), ["Adafruit_TinyUSB_Arduino", "SPI"]); + } + + #[test] + fn an_error_under_a_positive_guard_does_not_mark_a_library() { + assert!(!errors_without_tinyusb( + "#ifdef USE_TINYUSB\n#error no\n#endif\n" + )); + assert!(!errors_without_tinyusb( + "#if !defined(USE_TINYUSB)\n#define X\n#else\n#error no\n#endif\n" + )); + assert!(errors_without_tinyusb( + "#ifndef USE_TINYUSB\n#error yes\n#endif\n" + )); + } +} diff --git a/docs/architecture/library-selection.md b/docs/architecture/library-selection.md index 6f66b7544..e6af5e2f0 100644 --- a/docs/architecture/library-selection.md +++ b/docs/architecture/library-selection.md @@ -155,6 +155,15 @@ libraries from the candidates unless `ENABLE_CLASSIC` is set by the `ipbtstack` menu or `build_flags`; a `lib_deps` entry still selects one (`crates/fbuild-build-arm/src/rp2040/bluetooth_libs.rs`). +The same applies to Arduino-Pico's bundled `Adafruit_TinyUSB_Arduino`, whose +API header `#error`s unless `USE_TINYUSB` (the TinyUSB USB-stack menu) is +defined. The core's `SPI`, `SD` and `LittleFS` sources include it behind +`#ifdef USE_TINYUSB`, and a FastLED 3.10.8 Pico build in `lib/` selected it +through them and failed (FastLED/fbuild#1719). A library with an `#error` under a `!defined(USE_TINYUSB)` +guard is dropped from the candidates unless `USE_TINYUSB` is defined; a +`lib_deps` entry still selects it +(`crates/fbuild-build-arm/src/rp2040/tinyusb_libs.rs`). + ## Why two-pass (not fixed-point) PlatformIO `chain` mode runs BFS from project sources, then ONE