From 1993e5a57ff618693fe74fa4d62c976334f5bdc4 Mon Sep 17 00:00:00 2001 From: Zach Vorhies Date: Wed, 21 Oct 2026 21:50:29 -0700 Subject: [PATCH 1/5] fix(rp2040): repair Pico boot2 definitions and board mapping (#1670) --- .github/workflows/check-windows.yml | 7 ++ .../src/rp2040/configs/README.md | 3 + .../src/rp2040/configs/rp2040.json | 4 +- .../src/rp2040/configs/rp2350.json | 4 +- .../fbuild-build-arm/src/rp2040/mcu_config.rs | 9 +++ .../src/rp2040/orchestrator.rs | 24 +++--- crates/fbuild-build/tests/it/README.md | 4 + crates/fbuild-build/tests/it/main.rs | 1 + .../tests/it/rp2040_acceptance.rs | 74 +++++++++++++++++++ .../assets/boards/json/pico.json | 2 +- crates/fbuild-config/src/board/methods.rs | 15 +--- crates/fbuild-config/src/board/tests.rs | 15 ++++ crates/fbuild-core/src/compiler_flags.rs | 45 +++++++++++ 13 files changed, 180 insertions(+), 27 deletions(-) create mode 100644 crates/fbuild-build/tests/it/rp2040_acceptance.rs diff --git a/.github/workflows/check-windows.yml b/.github/workflows/check-windows.yml index 7efbff76e..f0e4d5724 100644 --- a/.github/workflows/check-windows.yml +++ b/.github/workflows/check-windows.yml @@ -81,6 +81,13 @@ jobs: run: | soldr cargo test -p fbuild-build --test it renesas_acceptance:: -- --ignored --nocapture + - name: Build Pico boot2 and default firmware + shell: bash + env: + FBUILD_DEV_MODE: "1" + run: | + soldr cargo test -p fbuild-build --test it rp2040_acceptance:: -- --ignored --nocapture + - name: Setup uv for legacy source package install uses: astral-sh/setup-uv@v3 diff --git a/crates/fbuild-build-arm/src/rp2040/configs/README.md b/crates/fbuild-build-arm/src/rp2040/configs/README.md index 541bdb952..6a17e1b4d 100644 --- a/crates/fbuild-build-arm/src/rp2040/configs/README.md +++ b/crates/fbuild-build-arm/src/rp2040/configs/README.md @@ -1,5 +1,8 @@ # RP2040 MCU Configs +USB device macros `USBD_VID` and `USBD_PID` alias the registry-supplied +`USB_VID` and `USB_PID`; these configs contain no board USB identity values. + JSON configuration files for Raspberry Pi RP2040/RP2350 MCU families. - `rp2040.json` - RP2040 (Cortex-M0+, no FPU) diff --git a/crates/fbuild-build-arm/src/rp2040/configs/rp2040.json b/crates/fbuild-build-arm/src/rp2040/configs/rp2040.json index 5674eaaa4..e53ee1674 100644 --- a/crates/fbuild-build-arm/src/rp2040/configs/rp2040.json +++ b/crates/fbuild-build-arm/src/rp2040/configs/rp2040.json @@ -56,6 +56,8 @@ }, "defines": [ - ["ARDUINO", "10808"] + ["ARDUINO", "10808"], + ["USBD_VID", "USB_VID"], + ["USBD_PID", "USB_PID"] ] } diff --git a/crates/fbuild-build-arm/src/rp2040/configs/rp2350.json b/crates/fbuild-build-arm/src/rp2040/configs/rp2350.json index 15541eb23..31135039b 100644 --- a/crates/fbuild-build-arm/src/rp2040/configs/rp2350.json +++ b/crates/fbuild-build-arm/src/rp2040/configs/rp2350.json @@ -60,6 +60,8 @@ }, "defines": [ - ["ARDUINO", "10808"] + ["ARDUINO", "10808"], + ["USBD_VID", "USB_VID"], + ["USBD_PID", "USB_PID"] ] } diff --git a/crates/fbuild-build-arm/src/rp2040/mcu_config.rs b/crates/fbuild-build-arm/src/rp2040/mcu_config.rs index 9caa7436a..3e5ac2e5b 100644 --- a/crates/fbuild-build-arm/src/rp2040/mcu_config.rs +++ b/crates/fbuild-build-arm/src/rp2040/mcu_config.rs @@ -34,6 +34,15 @@ pub fn get_rp2040_config_for_mcu(mcu: &str) -> Result { mod tests { use super::*; + #[test] + fn usb_device_defines_alias_the_registry_compile_identity() { + for mcu in ["rp2040", "rp2350"] { + let defines = get_rp2040_config_for_mcu(mcu).unwrap().defines_map(); + assert_eq!(defines.get("USBD_VID").map(String::as_str), Some("USB_VID")); + assert_eq!(defines.get("USBD_PID").map(String::as_str), Some("USB_PID")); + } + } + #[test] fn test_load_rp2040_config() { let config = get_rp2040_config_for_mcu("rp2040").unwrap(); diff --git a/crates/fbuild-build-arm/src/rp2040/orchestrator.rs b/crates/fbuild-build-arm/src/rp2040/orchestrator.rs index 9bcead3e5..f658713e6 100644 --- a/crates/fbuild-build-arm/src/rp2040/orchestrator.rs +++ b/crates/fbuild-build-arm/src/rp2040/orchestrator.rs @@ -55,6 +55,7 @@ struct Rp2040FingerprintMetadata { board_extra_flags: Option, board_upload_protocol: Option, board_menu_overrides: BTreeMap, + board_defines: BTreeMap, platform: String, max_flash: Option, max_ram: Option, @@ -199,7 +200,14 @@ impl BuildOrchestrator for Rp2040Orchestrator { &framework.get_boards_txt(), &board_id, &board_menu_overrides, - ); + ) + .or_else(|| { + crate::arduino_props::load_board_props_with_menu_overrides( + &framework.get_boards_txt(), + ctx.board.registry_board_id(), + &board_menu_overrides, + ) + }); let build_dir = ctx.build_dir.clone(); let metadata_hash = stable_hash_with_build_config( &Rp2040FingerprintMetadata { @@ -217,6 +225,7 @@ impl BuildOrchestrator for Rp2040Orchestrator { board_extra_flags: ctx.board.extra_flags.clone(), board_upload_protocol: ctx.board.upload_protocol.clone(), board_menu_overrides: board_menu_overrides.into_iter().collect(), + board_defines: ctx.board.get_defines().into_iter().collect(), platform: "raspberrypi".to_string(), max_flash: ctx.board.max_flash, max_ram: ctx.board.max_ram, @@ -702,8 +711,6 @@ fn apply_rp_board_props( return; }; for key in [ - "usbvid", - "usbpid", "usbpwr", "usbstack_flags", "variantdefines", @@ -800,15 +807,7 @@ fn add_rp_board_includes( } fn apply_define_flags(flags: &[String], defines: &mut HashMap) { - for flag in flags { - if let Some(def) = flag.strip_prefix("-D") { - if let Some((key, val)) = def.split_once('=') { - defines.insert(key.to_string(), val.trim().to_string()); - } else { - defines.insert(def.trim().to_string(), "1".to_string()); - } - } - } + defines.extend(fbuild_core::compiler_flags::parse_define_flags(flags)); } fn rp_family(mcu: &str) -> &'static str { @@ -1096,6 +1095,7 @@ mod tests { board_extra_flags: None, board_upload_protocol: None, board_menu_overrides: BTreeMap::new(), + board_defines: BTreeMap::new(), platform: "raspberrypi".into(), max_flash: None, max_ram: None, diff --git a/crates/fbuild-build/tests/it/README.md b/crates/fbuild-build/tests/it/README.md index bdc39f74d..0752dd483 100644 --- a/crates/fbuild-build/tests/it/README.md +++ b/crates/fbuild-build/tests/it/README.md @@ -13,3 +13,7 @@ interrupt handler (including an LTO build), plus ClearCore vendor syscall linkag `renesas_acceptance` builds UNO R4 firmware using `String` and `IPAddress` and verifies that the supported default core supplied both source files. Run with `soldr cargo test -p fbuild-build --test it renesas_acceptance:: -- --ignored`. + +`rp2040_acceptance` builds a real default `pico` project, including boot2 +assembly and USB string definitions. Run it with +`soldr cargo test -p fbuild-build --test it rp2040_acceptance:: -- --ignored`. diff --git a/crates/fbuild-build/tests/it/main.rs b/crates/fbuild-build/tests/it/main.rs index a6423d257..1da480a7f 100644 --- a/crates/fbuild-build/tests/it/main.rs +++ b/crates/fbuild-build/tests/it/main.rs @@ -13,6 +13,7 @@ mod lite_scons_acceptance; mod nxplpc_build_flags; mod nxplpc_core_compile_commands; mod renesas_acceptance; +mod rp2040_acceptance; mod samd_acceptance; mod stm32_acceptance; mod teensy30_acceptance; diff --git a/crates/fbuild-build/tests/it/rp2040_acceptance.rs b/crates/fbuild-build/tests/it/rp2040_acceptance.rs new file mode 100644 index 000000000..995a6a795 --- /dev/null +++ b/crates/fbuild-build/tests/it/rp2040_acceptance.rs @@ -0,0 +1,74 @@ +//! Real default Raspberry Pi Pico build regression for FastLED/fbuild#1670. + +use fbuild_build::{BuildOrchestrator, BuildParams, compile_backend}; +use fbuild_core::BuildProfile; + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +#[ignore = "#1670: downloads Arduino-Pico, matching pqt-gcc and published USB profiles"] +async fn pico_default_build_assembles_boot2_and_preserves_string_defines() { + tokio::task::spawn_blocking(|| { + let registry = tempfile::tempdir().unwrap(); + assert!(fbuild_core::usb::profiles::populate_profiles_from_paths( + ®istry.path().join("_meta.json"), + ®istry.path().join("usb-profiles.json"), + )); + }) + .await + .unwrap(); + let backend = compile_backend::CompileBackend::start().await.unwrap(); + compile_backend::install_global(backend); + let tmp = tempfile::tempdir().unwrap(); + let project = tmp.path(); + std::fs::write( + project.join("platformio.ini"), + "[env:pico]\nplatform = raspberrypi\nboard = pico\nframework = arduino\n", + ) + .unwrap(); + std::fs::create_dir(project.join("src")).unwrap(); + std::fs::write(project.join("src/main.cpp"), r#" +#include +constexpr char manufacturer[] = USB_MANUFACTURER; +constexpr char product[] = USB_PRODUCT; +static_assert(sizeof(manufacturer) > 1 && sizeof(product) > 1, "USB strings survive compiler arguments"); +void setup() { pinMode(LED_BUILTIN, OUTPUT); } +void loop() { digitalWrite(LED_BUILTIN, HIGH); } +"#).unwrap(); + let build_dir = project + .join(fbuild_paths::FBUILD_DIR_NAME) + .join(fbuild_paths::BUILD_DIR_NAME) + .join("pico/release"); + let params = BuildParams { + project_dir: project.to_path_buf(), + env_name: "pico".into(), + build_dir: build_dir.clone(), + clean_all: false, + clean_only: false, + clean: true, + profile: BuildProfile::Release, + verbose: true, + jobs: Some(4), + generate_compiledb: true, + 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 result = tokio::time::timeout( + std::time::Duration::from_secs(900), + fbuild_build::rp2040::Rp2040Orchestrator.build(¶ms), + ) + .await + .expect("Pico build timed out") + .expect("Pico build must succeed"); + assert!(result.success); + assert!(build_dir.join("boot2.o").is_file(), "boot2 assembly output"); + assert!(result.elf_path.as_ref().expect("firmware ELF").is_file()); + assert!(result.size_info.as_ref().unwrap().total_flash > 0); +} diff --git a/crates/fbuild-config/assets/boards/json/pico.json b/crates/fbuild-config/assets/boards/json/pico.json index 0409969d2..36de041a1 100644 --- a/crates/fbuild-config/assets/boards/json/pico.json +++ b/crates/fbuild-config/assets/boards/json/pico.json @@ -4,7 +4,7 @@ "extra_flags": "-D ARDUINO_RASPBERRY_PI_PICO -DARDUINO_ARCH_RP2040", "f_cpu": "133000000L", "mcu": "rp2040", - "variant": "RASPBERRY_PI_PICO" + "variant": "rpipico" }, "debug": { "tools": { diff --git a/crates/fbuild-config/src/board/methods.rs b/crates/fbuild-config/src/board/methods.rs index a9da2cf01..8be828165 100644 --- a/crates/fbuild-config/src/board/methods.rs +++ b/crates/fbuild-config/src/board/methods.rs @@ -297,18 +297,9 @@ impl BoardConfig { // Extra flags if let Some(ref flags) = self.extra_flags { - for flag in fbuild_core::shell_split::split(flags) { - if let Some(define) = flag.strip_prefix("-D") { - if let Some(eq_pos) = define.find('=') { - defines.insert( - define[..eq_pos].to_string(), - define[eq_pos + 1..].to_string(), - ); - } else { - defines.insert(define.to_string(), "1".to_string()); - } - } - } + defines.extend(fbuild_core::compiler_flags::parse_define_flags( + &fbuild_core::shell_split::split(flags), + )); } defines diff --git a/crates/fbuild-config/src/board/tests.rs b/crates/fbuild-config/src/board/tests.rs index 98bff4239..11969d97b 100644 --- a/crates/fbuild-config/src/board/tests.rs +++ b/crates/fbuild-config/src/board/tests.rs @@ -281,6 +281,21 @@ fn test_get_defines_with_extra_flags() { assert_eq!(defines.get("VALUE"), Some(&"42".to_string())); } +#[test] +fn pico_spaced_board_define_does_not_create_an_empty_macro() { + let board = BoardConfig::from_board_id("pico", &HashMap::new()).unwrap(); + assert_eq!(board.variant, "rpipico"); + let defines = board.get_defines(); + assert_eq!( + defines.get("ARDUINO_RASPBERRY_PI_PICO").map(String::as_str), + Some("1") + ); + assert!( + !defines.contains_key(""), + "a bare -D corrupts boot2 assembly" + ); +} + #[test] fn test_get_include_paths() { let config = BoardConfig::from_board_id("uno", &HashMap::new()).unwrap(); diff --git a/crates/fbuild-core/src/compiler_flags.rs b/crates/fbuild-core/src/compiler_flags.rs index ebefc5e9c..ecbd38e99 100644 --- a/crates/fbuild-core/src/compiler_flags.rs +++ b/crates/fbuild-core/src/compiler_flags.rs @@ -49,10 +49,55 @@ pub fn prepare_flags_for_exec(flags: Vec) -> Vec { .collect() } +/// Extract GCC definitions from tokenized flags, accepting both `-DNAME` +/// and `-D NAME`. Later definitions override earlier ones. Replacement text +/// (including quotes and additional `=` characters) is preserved verbatim. +pub fn parse_define_flags(flags: &[String]) -> std::collections::HashMap { + let mut defines = std::collections::HashMap::new(); + let mut flags = flags.iter(); + while let Some(flag) = flags.next() { + let Some(raw) = flag.strip_prefix("-D") else { + continue; + }; + let definition = if raw.is_empty() { + flags.next().map(String::as_str).unwrap_or_default() + } else { + raw + }; + let (name, value) = definition.split_once('=').unwrap_or((definition, "1")); + defines.insert(name.to_string(), value.to_string()); + } + defines +} + #[cfg(test)] mod tests { use super::*; + #[test] + fn separated_and_joined_define_flags_preserve_replacements() { + let flags = [ + "-D", + "BOARD", + "-DVALUE=old", + "-D", + "VALUE=new=tail", + "-DUSB_NAME=\"My Board\"", + "-I/include", + "-DEMPTY=", + ] + .map(str::to_string); + let defines = parse_define_flags(&flags); + assert_eq!(defines.get("BOARD").map(String::as_str), Some("1")); + assert_eq!(defines.get("VALUE").map(String::as_str), Some("new=tail")); + assert_eq!( + defines.get("USB_NAME").map(String::as_str), + Some("\"My Board\"") + ); + assert_eq!(defines.get("EMPTY").map(String::as_str), Some("")); + assert_eq!(defines.len(), 4); + } + #[test] fn strips_escaped_quotes_in_defines() { let flags = vec![ From 2b91815133fd35d0493f5a36246ecf04cad6ecc8 Mon Sep 17 00:00:00 2001 From: Zach Vorhies Date: Wed, 21 Oct 2026 22:44:33 -0700 Subject: [PATCH 2/5] fix(rp2040): resolve variants from the selected framework --- crates/fbuild-build-arm/src/rp2040/README.md | 3 + .../src/rp2040/board_props.rs | 93 +++++++++++++++++++ crates/fbuild-build-arm/src/rp2040/mod.rs | 1 + .../src/rp2040/orchestrator.rs | 44 +-------- .../assets/boards/json/pico.json | 2 +- crates/fbuild-config/src/board/tests.rs | 2 +- 6 files changed, 102 insertions(+), 43 deletions(-) create mode 100644 crates/fbuild-build-arm/src/rp2040/board_props.rs diff --git a/crates/fbuild-build-arm/src/rp2040/README.md b/crates/fbuild-build-arm/src/rp2040/README.md index 29c81690b..40a322eb2 100644 --- a/crates/fbuild-build-arm/src/rp2040/README.md +++ b/crates/fbuild-build-arm/src/rp2040/README.md @@ -4,3 +4,6 @@ RP2040/RP2350 platform build support (Raspberry Pi Pico). Uses the generic ARM compiler/linker from `generic_arm/` with RP2040-specific MCU configurations and the RP2040 Arduino cores framework. + +`board_props.rs` applies Arduino-Pico board properties and resolves the variant +from the selected framework while preserving explicit project overrides. diff --git a/crates/fbuild-build-arm/src/rp2040/board_props.rs b/crates/fbuild-build-arm/src/rp2040/board_props.rs new file mode 100644 index 000000000..748f0d32f --- /dev/null +++ b/crates/fbuild-build-arm/src/rp2040/board_props.rs @@ -0,0 +1,93 @@ +//! Arduino-Pico board properties and framework-specific variant selection. + +use std::collections::HashMap; +use std::path::Path; + +/// User variant overrides take precedence over framework metadata. The board +/// manifest may describe another core (e.g. Mbed), so use the selected Arduino +/// framework's variant when it is available. +pub(super) fn resolve_variant( + declared: &str, + props: &Option>, + overrides: &HashMap, +) -> String { + if overrides.contains_key("variant") { + return declared.to_owned(); + } + props + .as_ref() + .and_then(|props| props.get("variant")) + .filter(|variant| !variant.is_empty()) + .map_or_else(|| declared.to_owned(), Clone::clone) +} + +pub(super) fn apply_rp_board_props( + board_props: &Option>, + framework_dir: &Path, + defines: &mut HashMap, +) { + let Some(props) = board_props.as_ref() else { + return; + }; + for key in [ + "usbpwr", + "usbstack_flags", + "variantdefines", + "led", + "libpicowdefs", + "wificc", + "sdfatdefines", + ] { + if let Some(flags) = props.get(key) { + let expanded = + flags.replace("{runtime.platform.path}", &framework_dir.to_string_lossy()); + let tokens = fbuild_core::shell_split::split(&expanded); + defines.extend(fbuild_core::compiler_flags::parse_define_flags(&tokens)); + } + } + if let Some(value) = props.get("usb_manufacturer") { + defines.insert("USB_MANUFACTURER".to_string(), value.clone()); + } + if let Some(value) = props.get("usb_product") { + defines.insert("USB_PRODUCT".to_string(), value.clone()); + } + for (property, define) in [ + ("flash_total", "PICO_FLASH_SIZE_BYTES"), + ("fs_start", "FS_START"), + ("fs_end", "FS_END"), + ] { + if let Some(value) = props.get(property) { + defines.insert(define.to_string(), value.clone()); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn resolves_framework_variant_and_preserves_explicit_overrides() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("boards.txt"); + std::fs::write( + &path, + "fixture.name=Test board\nfixture.build.variant=framework_variant\n", + ) + .unwrap(); + let props = crate::arduino_props::load_board_props_with_default_menus(&path, "fixture"); + assert_eq!( + resolve_variant("manifest_variant", &props, &HashMap::new()), + "framework_variant" + ); + let overrides = HashMap::from([("variant".to_owned(), "custom_variant".to_owned())]); + assert_eq!( + resolve_variant("custom_variant", &props, &overrides), + "custom_variant" + ); + assert_eq!( + resolve_variant("manifest_variant", &None, &HashMap::new()), + "manifest_variant" + ); + } +} diff --git a/crates/fbuild-build-arm/src/rp2040/mod.rs b/crates/fbuild-build-arm/src/rp2040/mod.rs index c8e31cb80..3b0bda406 100644 --- a/crates/fbuild-build-arm/src/rp2040/mod.rs +++ b/crates/fbuild-build-arm/src/rp2040/mod.rs @@ -1,6 +1,7 @@ //! RP2040/RP2350 platform build support (Raspberry Pi Pico, etc.) mod bluetooth_libs; +mod board_props; pub mod mcu_config; pub mod orchestrator; mod uf2; diff --git a/crates/fbuild-build-arm/src/rp2040/orchestrator.rs b/crates/fbuild-build-arm/src/rp2040/orchestrator.rs index f658713e6..5129a48e3 100644 --- a/crates/fbuild-build-arm/src/rp2040/orchestrator.rs +++ b/crates/fbuild-build-arm/src/rp2040/orchestrator.rs @@ -12,6 +12,7 @@ //! 9. Link (with linker script from variant dir) //! 10. Convert to binary + report size +use super::board_props::{apply_rp_board_props, resolve_variant}; use std::collections::{BTreeMap, HashMap}; use std::path::{Path, PathBuf}; use std::time::Instant; @@ -208,6 +209,8 @@ impl BuildOrchestrator for Rp2040Orchestrator { &board_menu_overrides, ) }); + ctx.board.variant = + resolve_variant(&ctx.board.variant, &board_props, &board_menu_overrides); let build_dir = ctx.build_dir.clone(); let metadata_hash = stable_hash_with_build_config( &Rp2040FingerprintMetadata { @@ -702,47 +705,6 @@ fn add_define_file(define_file: &Path, defines: &mut HashMap) { apply_define_flags(&flags, defines); } -fn apply_rp_board_props( - board_props: &Option>, - framework_dir: &Path, - defines: &mut HashMap, -) { - let Some(props) = board_props.as_ref() else { - return; - }; - for key in [ - "usbpwr", - "usbstack_flags", - "variantdefines", - "led", - "libpicowdefs", - "wificc", - "sdfatdefines", - ] { - if let Some(flags) = props.get(key) { - let expanded = - flags.replace("{runtime.platform.path}", &framework_dir.to_string_lossy()); - let tokens = fbuild_core::shell_split::split(&expanded); - apply_define_flags(&tokens, defines); - } - } - if let Some(value) = props.get("usb_manufacturer") { - defines.insert("USB_MANUFACTURER".to_string(), value.clone()); - } - if let Some(value) = props.get("usb_product") { - defines.insert("USB_PRODUCT".to_string(), value.clone()); - } - for (property, define) in [ - ("flash_total", "PICO_FLASH_SIZE_BYTES"), - ("fs_start", "FS_START"), - ("fs_end", "FS_END"), - ] { - if let Some(value) = props.get(property) { - defines.insert(define.to_string(), value.clone()); - } - } -} - /// Apply Arduino-Pico's framework compiler definitions, with selected board /// properties overriding platform defaults. SdFat's public-copy setting lives /// in platform.txt, not boards.txt, and is required to compile bundled SDFS. diff --git a/crates/fbuild-config/assets/boards/json/pico.json b/crates/fbuild-config/assets/boards/json/pico.json index 36de041a1..0409969d2 100644 --- a/crates/fbuild-config/assets/boards/json/pico.json +++ b/crates/fbuild-config/assets/boards/json/pico.json @@ -4,7 +4,7 @@ "extra_flags": "-D ARDUINO_RASPBERRY_PI_PICO -DARDUINO_ARCH_RP2040", "f_cpu": "133000000L", "mcu": "rp2040", - "variant": "rpipico" + "variant": "RASPBERRY_PI_PICO" }, "debug": { "tools": { diff --git a/crates/fbuild-config/src/board/tests.rs b/crates/fbuild-config/src/board/tests.rs index 11969d97b..f8242e79f 100644 --- a/crates/fbuild-config/src/board/tests.rs +++ b/crates/fbuild-config/src/board/tests.rs @@ -284,7 +284,7 @@ fn test_get_defines_with_extra_flags() { #[test] fn pico_spaced_board_define_does_not_create_an_empty_macro() { let board = BoardConfig::from_board_id("pico", &HashMap::new()).unwrap(); - assert_eq!(board.variant, "rpipico"); + assert_eq!(board.variant, "RASPBERRY_PI_PICO"); let defines = board.get_defines(); assert_eq!( defines.get("ARDUINO_RASPBERRY_PI_PICO").map(String::as_str), From 103262463889485bc88739aebaa0271bd0e6223a Mon Sep 17 00:00:00 2001 From: Zach Vorhies Date: Wed, 21 Oct 2026 23:02:21 -0700 Subject: [PATCH 3/5] fix(rp2040): preserve board define overrides over MCU defaults --- .../src/rp2040/board_props.rs | 40 +++++++++++++++++++ .../src/rp2040/orchestrator.rs | 22 ++++++---- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/crates/fbuild-build-arm/src/rp2040/board_props.rs b/crates/fbuild-build-arm/src/rp2040/board_props.rs index 748f0d32f..2638d6854 100644 --- a/crates/fbuild-build-arm/src/rp2040/board_props.rs +++ b/crates/fbuild-build-arm/src/rp2040/board_props.rs @@ -3,6 +3,17 @@ use std::collections::HashMap; use std::path::Path; +pub(super) fn rp_board_defines( + board: &fbuild_config::BoardConfig, + mcu: &crate::generic_arm::ArmMcuConfig, +) -> HashMap { + // MCU values are defaults; board/project flags are more specific. In + // particular, USB identity aliases must not replace explicit USBD_* flags. + let mut defines = mcu.defines_map(); + defines.extend(board.get_defines()); + defines +} + /// User variant overrides take precedence over framework metadata. The board /// manifest may describe another core (e.g. Mbed), so use the selected Arduino /// framework's variant when it is available. @@ -66,6 +77,35 @@ pub(super) fn apply_rp_board_props( mod tests { use super::*; + #[test] + fn explicit_usb_aliases_override_rp_mcu_defaults() { + for id in ["pico", "rpipico2"] { + let overrides = HashMap::from([( + "extra_flags".to_owned(), + "-DUSBD_VID=TEST_VENDOR -DUSBD_PID=TEST_PRODUCT".to_owned(), + )]); + let board = fbuild_config::BoardConfig::from_board_id(id, &overrides).unwrap(); + let mcu = + super::super::mcu_config::get_rp2040_config_for_mcu(&board.mcu.to_lowercase()) + .unwrap(); + let defines = rp_board_defines(&board, &mcu); + assert_eq!( + defines.get("USBD_VID").map(String::as_str), + Some("TEST_VENDOR"), + "{id}" + ); + assert_eq!( + defines.get("USBD_PID").map(String::as_str), + Some("TEST_PRODUCT"), + "{id}" + ); + let defaults = fbuild_config::BoardConfig::from_board_id(id, &HashMap::new()).unwrap(); + let defines = rp_board_defines(&defaults, &mcu); + assert_eq!(defines.get("USBD_VID").map(String::as_str), Some("USB_VID")); + assert_eq!(defines.get("USBD_PID").map(String::as_str), Some("USB_PID")); + } + } + #[test] fn resolves_framework_variant_and_preserves_explicit_overrides() { let tmp = tempfile::tempdir().unwrap(); diff --git a/crates/fbuild-build-arm/src/rp2040/orchestrator.rs b/crates/fbuild-build-arm/src/rp2040/orchestrator.rs index 5129a48e3..68b5ff79c 100644 --- a/crates/fbuild-build-arm/src/rp2040/orchestrator.rs +++ b/crates/fbuild-build-arm/src/rp2040/orchestrator.rs @@ -12,7 +12,7 @@ //! 9. Link (with linker script from variant dir) //! 10. Convert to binary + report size -use super::board_props::{apply_rp_board_props, resolve_variant}; +use super::board_props::{apply_rp_board_props, resolve_variant, rp_board_defines}; use std::collections::{BTreeMap, HashMap}; use std::path::{Path, PathBuf}; use std::time::Instant; @@ -36,6 +36,8 @@ use crate::{BuildOrchestrator, BuildParams, BuildResult, SourceScanner}; use super::uf2::convert_elf_to_uf2; +const RP_DEFINE_POLICY: &str = "platform-sdfatdefaults-board-defines-v2"; + /// RP2040 platform build orchestrator. pub struct Rp2040Orchestrator; @@ -215,7 +217,7 @@ impl BuildOrchestrator for Rp2040Orchestrator { let metadata_hash = stable_hash_with_build_config( &Rp2040FingerprintMetadata { version: BUILD_FINGERPRINT_VERSION, - platform_define_policy: "platform-sdfatdefaults-v1", + platform_define_policy: RP_DEFINE_POLICY, framework_library_linkage: "local-and-framework-archive-v2", env_name: params.env_name.clone(), profile: profile_label(params.profile).to_string(), @@ -304,8 +306,7 @@ impl BuildOrchestrator for Rp2040Orchestrator { // 6. Build include dirs + compiler let mcu_config = super::mcu_config::get_rp2040_config_for_mcu(&ctx.board.mcu.to_lowercase())?; - let mut defines = ctx.board.get_defines(); - defines.extend(mcu_config.defines_map()); + let mut defines = rp_board_defines(&ctx.board, &mcu_config); if let Some(max_flash) = ctx.board.max_flash { defines.insert("PICO_FLASH_SIZE_BYTES".to_string(), max_flash.to_string()); } @@ -1023,6 +1024,14 @@ mod tests { assert_policy_invalidates_legacy_firmware_cache("platform_define_policy", None); } + #[test] + fn board_define_precedence_invalidates_previous_firmware_cache() { + assert_policy_invalidates_legacy_firmware_cache( + "platform_define_policy", + Some("platform-sdfatdefaults-v1"), + ); + } + #[test] fn framework_archive_policy_invalidates_legacy_firmware_cache() { assert_policy_invalidates_legacy_firmware_cache("framework_library_linkage", None); @@ -1044,7 +1053,7 @@ mod tests { let metadata = Rp2040FingerprintMetadata { version: BUILD_FINGERPRINT_VERSION, - platform_define_policy: "platform-sdfatdefaults-v1", + platform_define_policy: RP_DEFINE_POLICY, framework_library_linkage: "local-and-framework-archive-v2", env_name: "pico".into(), profile: "release".into(), @@ -1301,8 +1310,7 @@ mod tests { board_id, ); let mcu = super::super::mcu_config::get_rp2040_config_for_mcu(&board.mcu).unwrap(); - let mut defines = board.get_defines(); - defines.extend(mcu.defines_map()); + let mut defines = rp_board_defines(&board, &mcu); add_rp_manifest_defines(&framework, &board.mcu, &mut defines); apply_rp_board_props(&props, &framework, &mut defines); apply_rp_platform_defines(&framework, &props, &mut defines).unwrap(); From be57a42c776538c697e03c06698bdadac3505576 Mon Sep 17 00:00:00 2001 From: Zach Vorhies Date: Wed, 21 Oct 2026 23:21:40 -0700 Subject: [PATCH 4/5] test(rp2040): assert exact Pico USB strings --- crates/fbuild-build/tests/it/rp2040_acceptance.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/fbuild-build/tests/it/rp2040_acceptance.rs b/crates/fbuild-build/tests/it/rp2040_acceptance.rs index 995a6a795..b76b174b4 100644 --- a/crates/fbuild-build/tests/it/rp2040_acceptance.rs +++ b/crates/fbuild-build/tests/it/rp2040_acceptance.rs @@ -27,9 +27,11 @@ async fn pico_default_build_assembles_boot2_and_preserves_string_defines() { std::fs::create_dir(project.join("src")).unwrap(); std::fs::write(project.join("src/main.cpp"), r#" #include +#include constexpr char manufacturer[] = USB_MANUFACTURER; constexpr char product[] = USB_PRODUCT; -static_assert(sizeof(manufacturer) > 1 && sizeof(product) > 1, "USB strings survive compiler arguments"); +static_assert(std::string_view(manufacturer) == "Raspberry Pi" && + std::string_view(product) == "Pico", "Pico USB strings survive compiler arguments"); void setup() { pinMode(LED_BUILTIN, OUTPUT); } void loop() { digitalWrite(LED_BUILTIN, HIGH); } "#).unwrap(); From 619e0fa254a34eda191221518750088dc8198540 Mon Sep 17 00:00:00 2001 From: Zach Vorhies Date: Wed, 21 Oct 2026 23:25:14 -0700 Subject: [PATCH 5/5] style: format Pico acceptance fixture writer --- crates/fbuild-build/tests/it/rp2040_acceptance.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/fbuild-build/tests/it/rp2040_acceptance.rs b/crates/fbuild-build/tests/it/rp2040_acceptance.rs index b76b174b4..9ca595c43 100644 --- a/crates/fbuild-build/tests/it/rp2040_acceptance.rs +++ b/crates/fbuild-build/tests/it/rp2040_acceptance.rs @@ -25,7 +25,9 @@ async fn pico_default_build_assembles_boot2_and_preserves_string_defines() { ) .unwrap(); std::fs::create_dir(project.join("src")).unwrap(); - std::fs::write(project.join("src/main.cpp"), r#" + std::fs::write( + project.join("src/main.cpp"), + r#" #include #include constexpr char manufacturer[] = USB_MANUFACTURER; @@ -34,7 +36,9 @@ static_assert(std::string_view(manufacturer) == "Raspberry Pi" && std::string_view(product) == "Pico", "Pico USB strings survive compiler arguments"); void setup() { pinMode(LED_BUILTIN, OUTPUT); } void loop() { digitalWrite(LED_BUILTIN, HIGH); } -"#).unwrap(); +"#, + ) + .unwrap(); let build_dir = project .join(fbuild_paths::FBUILD_DIR_NAME) .join(fbuild_paths::BUILD_DIR_NAME)