Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 199 additions & 0 deletions crates/fbuild-build-arm/src/rp2040/bluetooth_libs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
//! Arduino-Pico's Bluetooth-only framework libraries.
//!
//! Arduino-Pico marks every library that needs its BTstack configuration with
//! `#include <_needsbt.h>`, a core header that does
//! `static_assert(ENABLE_CLASSIC, "This library needs Bluetooth enabled...")`.
//! `ENABLE_CLASSIC` comes from the board's `ipbtstack` menu (`-DENABLE_CLASSIC=1`
//! on the Bluetooth entries only), so on a build without Bluetooth such a
//! library cannot compile at all.
//!
//! The library finder over-selects on purpose: a guard on a macro the project
//! derives in a header (`#if FL_BLE_AVAILABLE`) is undecidable to the scanner,
//! so every arm is scanned. Since FastLED/fbuild#1473 seeds every translation
//! unit a local library compiles, FastLED's unity TUs reach its RP2350W BLE
//! transport and its guarded `#include <BTstackLib.h>`, and a plain Pico 2
//! build picked `BTstackLib` and died on that static assertion. Neither
//! arduino-cli (which preprocesses) nor PlatformIO's `chain` LDF (which does
//! not scan every library TU) selects it there.
//!
//! An inferred selection of a library the framework itself declares
//! unbuildable in this configuration can never be right, so those libraries
//! are not candidates unless Bluetooth is enabled. A `lib_deps` declaration
//! still selects one, and the framework's own error explains the missing menu
//! setting.

use std::collections::HashMap;
use std::path::Path;

use fbuild_packages::library::FrameworkLibrary;

/// Drop framework libraries that require Arduino-Pico Bluetooth when the
/// build does not enable it, keeping any library `lib_deps` names.
pub(crate) fn exclude_bluetooth_libraries_when_disabled(
libraries: Vec<FrameworkLibrary>,
defines: &HashMap<String, String>,
declared: &[String],
) -> Vec<FrameworkLibrary> {
if bluetooth_enabled(defines) {
return libraries;
}
let declared: Vec<String> = 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_bluetooth(library) {
return true;
}
tracing::info!(
library = %library.name,
"skipping framework library: it includes <_needsbt.h> and the build does not \
enable Bluetooth (ENABLE_CLASSIC); select a Bluetooth `ipbtstack` menu or name \
it in lib_deps"
);
false
})
.collect()
}

/// `_needsbt.h` asserts `ENABLE_CLASSIC`, so that is the switch that decides.
fn bluetooth_enabled(defines: &HashMap<String, String>) -> bool {
defines
.get("ENABLE_CLASSIC")
.map(|value| !matches!(value.trim(), "" | "0"))
.unwrap_or(false)
}

/// Whether any public header of `library` includes `_needsbt.h`.
fn needs_bluetooth(library: &FrameworkLibrary) -> bool {
library
.include_dirs
.iter()
.filter_map(|dir| std::fs::read_dir(dir).ok())
.flat_map(|entries| entries.flatten())
.map(|entry| entry.path())
.filter(|path| is_header(path))
.filter_map(|path| std::fs::read_to_string(path).ok())
.any(|text| text.lines().any(includes_needsbt))
}

fn is_header(path: &Path) -> bool {
path.is_file()
&& matches!(
path.extension().and_then(|ext| ext.to_str()),
Some("h" | "hh" | "hpp" | "hxx")
)
}

fn includes_needsbt(line: &str) -> bool {
let Some(directive) = line.trim_start().strip_prefix('#') else {
return false;
};
let Some(target) = directive.trim_start().strip_prefix("include") else {
return false;
};
let target = target.trim();
target == "<_needsbt.h>" || target == "\"_needsbt.h\""
}

#[cfg(test)]
mod tests {
use super::*;

fn library(root: &Path, name: &str, header: &str) -> FrameworkLibrary {
let dir = root.join("libraries").join(name);
let src = dir.join("src");
std::fs::create_dir_all(&src).unwrap();
std::fs::write(src.join(format!("{name}.h")), header).unwrap();
let source = src.join(format!("{name}.cpp"));
std::fs::write(&source, format!("#include \"{name}.h\"\n")).unwrap();
FrameworkLibrary {
name: name.to_string(),
dir,
include_dirs: vec![src],
source_files: vec![source],
}
}

/// The real shapes from Arduino-Pico 5.7.0: `BTstackLib.h` includes the
/// core's `_needsbt.h`; `SPI.h` does not.
fn fixture(root: &Path) -> Vec<FrameworkLibrary> {
vec![
library(
root,
"BTstackLib",
"#pragma once\n#include <Arduino.h>\n#include <_needsbt.h>\n",
),
library(root, "SPI", "#pragma once\n#include <Arduino.h>\n"),
]
}

fn names(libraries: &[FrameworkLibrary]) -> Vec<&str> {
libraries.iter().map(|lib| lib.name.as_str()).collect()
}

fn defines(pairs: &[(&str, &str)]) -> HashMap<String, String> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}

#[test]
fn drops_bluetooth_library_when_bluetooth_is_disabled() {
let tmp = tempfile::TempDir::new().unwrap();
let kept = exclude_bluetooth_libraries_when_disabled(
fixture(tmp.path()),
&defines(&[("LWIP_IPV4", "1")]),
&[],
);
assert_eq!(names(&kept), ["SPI"]);
}

#[test]
fn drops_bluetooth_library_when_enable_classic_is_zero() {
let tmp = tempfile::TempDir::new().unwrap();
let kept = exclude_bluetooth_libraries_when_disabled(
fixture(tmp.path()),
&defines(&[("ENABLE_CLASSIC", "0")]),
&[],
);
assert_eq!(names(&kept), ["SPI"]);
}

#[test]
fn keeps_bluetooth_library_when_the_menu_enables_bluetooth() {
let tmp = tempfile::TempDir::new().unwrap();
let kept = exclude_bluetooth_libraries_when_disabled(
fixture(tmp.path()),
&defines(&[("ENABLE_CLASSIC", "1"), ("ENABLE_BLE", "1")]),
&[],
);
assert_eq!(names(&kept), ["BTstackLib", "SPI"]);
}

#[test]
fn keeps_declared_bluetooth_library_even_when_disabled() {
let tmp = tempfile::TempDir::new().unwrap();
let kept = exclude_bluetooth_libraries_when_disabled(
fixture(tmp.path()),
&HashMap::new(),
&["btstacklib@^1.0".to_string()],
);
assert_eq!(names(&kept), ["BTstackLib", "SPI"]);
}

#[test]
fn a_commented_out_include_does_not_mark_a_library() {
let tmp = tempfile::TempDir::new().unwrap();
let libs = vec![library(
tmp.path(),
"Quiet",
"#pragma once\n// #include <_needsbt.h>\n",
)];
let kept = exclude_bluetooth_libraries_when_disabled(libs, &HashMap::new(), &[]);
assert_eq!(names(&kept), ["Quiet"]);
}
}
1 change: 1 addition & 0 deletions crates/fbuild-build-arm/src/rp2040/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! RP2040/RP2350 platform build support (Raspberry Pi Pico, etc.)

mod bluetooth_libs;
pub mod mcu_config;
pub mod orchestrator;
mod uf2;
Expand Down
12 changes: 11 additions & 1 deletion crates/fbuild-build-arm/src/rp2040/orchestrator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -298,12 +298,22 @@ impl BuildOrchestrator for Rp2040Orchestrator {
// Arduino-Pico ships framework libraries (WiFi, SPI, Wire, ...) under
// `libraries/`. Mirror PlatformIO's LDF so their headers are visible
// and only the sources required by the project are compiled.
let framework_libs = framework.get_framework_libraries();
let framework_info = fbuild_packages::Package::get_info(&framework);
let declared_deps = ctx
.config
.get_lib_deps(&params.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`.
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(
framework.get_framework_libraries(),
&bluetooth_defines,
&declared_deps,
)
};
warn_if_lib_ldf_mode_unsupported(
ctx.config
.get_lib_ldf_mode(&params.env_name)
Expand Down
2 changes: 1 addition & 1 deletion crates/fbuild-library-select/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ pub fn resolve_with_stats_active_declared(
/// Entries that are URLs or local paths (`https://…`, `file://…`, `./vendor`)
/// name something that has to be *fetched*, not a framework library that is
/// already on disk, so they never match and are left to the installer path.
fn declared_dep_name(entry: &str) -> Option<String> {
pub fn declared_dep_name(entry: &str) -> Option<String> {
let entry = entry.trim();
if entry.is_empty() || entry.contains("://") || entry.starts_with('.') || entry.starts_with('/')
{
Expand Down
12 changes: 12 additions & 0 deletions docs/architecture/library-selection.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,18 @@ Two separate comparisons with PlatformIO, since it is easy to conflate them:
The `#if 0` hint works under `chain` only as a side effect of it evaluating
nothing. `chain+` does not honor it. fbuild honors it deliberately.

The undecidable row over-selects, which is harmless for a library that
compiles in any configuration. It is fatal for one the framework itself
declares unbuildable here. Arduino-Pico marks its Bluetooth libraries
(`BTstackLib`, `SerialBT`, `BLE`, ...) with `#include <_needsbt.h>`, which
`static_assert`s `ENABLE_CLASSIC`. FastLED's RP2350W BLE transport includes
`<BTstackLib.h>` behind `#if FL_BLE_AVAILABLE`, a header-derived guard, and
once #1473 seeded FastLED's unity TUs a plain Pico 2 build selected
`BTstackLib` and failed. The RP2040 orchestrator therefore drops those
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`).

## Why two-pass (not fixed-point)

PlatformIO `chain` mode runs BFS from project sources, then ONE
Expand Down
Loading