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
22 changes: 7 additions & 15 deletions crates/fbuild-build/src/nxplpc/configs/nxplpc.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,42 +6,34 @@
"common": [
"-mcpu=cortex-m0plus",
"-mthumb",
"-mfloat-abi=soft",
"-fdata-sections",
"-ffunction-sections",
"-fmessage-length=0",
"-fno-exceptions",
"-fomit-frame-pointer",
"-funsigned-char",
"-Wall",
"-Wextra",
"-Wno-unused-parameter",
"-MMD"
],
"c": [
"-std=gnu11"
],
"cxx": [
"-std=gnu++17",
"-std=gnu++11",
"-fno-rtti",
"-fno-use-cxa-atexit",
"-fno-threadsafe-statics"
]
},
"linker_flags": [
"-mcpu=cortex-m0plus",
"-mthumb",
"-mfloat-abi=soft",
"-specs=nano.specs",
"-specs=nosys.specs",
"-Wl,--gc-sections",
"-Wl,--print-memory-usage",
"-nostartfiles"
"-Wl,--gc-sections"
],
"linker_libs": [
"-Wl,--start-group",
"-lgcc",
"-lc_nano",
"-lc",
"-lm",
"-lgcc",
"-lnosys",
"-Wl,--end-group"
],
Expand All @@ -51,8 +43,8 @@
},
"profiles": {
"release": {
"compile_flags": ["-Os", "-flto", "-fno-fat-lto-objects"],
"link_flags": ["-flto", "-fuse-linker-plugin"]
"compile_flags": ["-Os"],
"link_flags": []
},
"quick": {
"compile_flags": ["-Os"],
Expand Down
27 changes: 23 additions & 4 deletions crates/fbuild-build/src/nxplpc/mcu_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,16 +176,35 @@ mod tests {
assert!(config.compiler_flags.common.iter().any(|f| f == "-mthumb"));
assert!(config
.compiler_flags
.common
.cxx
.iter()
.any(|f| f == "-mfloat-abi=soft"));
.any(|f| f == "-std=gnu++11"));
assert!(
!config
.compiler_flags
.common
.iter()
.any(|f| f == "-mfloat-abi=soft"),
"nxplpc should mirror ArduinoCore-LPC8xx platform.txt, which does not pass -mfloat-abi"
);
}

#[test]
fn linker_flags_include_gc_sections() {
fn linker_flags_match_arduino_core_recipe() {
let config = get_nxplpc_config("lpc845").unwrap();
assert!(config.linker_flags.iter().any(|f| f == "-Wl,--gc-sections"));
assert!(config.linker_flags.iter().any(|f| f == "-nostartfiles"));
assert!(
!config.linker_flags.iter().any(|f| f == "-nostartfiles"),
"ArduinoCore-LPC8xx platform.txt does not pass -nostartfiles"
);
assert!(
config.linker_libs.iter().any(|f| f == "-lc"),
"ArduinoCore-LPC8xx links the standard C library name under nano.specs"
);
assert!(
!config.linker_libs.iter().any(|f| f == "-lc_nano"),
"ArduinoCore-LPC8xx platform.txt uses -lc, not -lc_nano"
);
}

#[test]
Expand Down
54 changes: 11 additions & 43 deletions crates/fbuild-build/src/source_scanner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,13 +277,6 @@ impl SourceScanner {
}

let prototypes = extract_function_prototypes(&combined);
let existing_decls = find_existing_forward_declarations(&combined);

// Remove existing forward declarations from the body
let mut body = combined.clone();
for decl in &existing_decls {
body = body.replace(decl, "");
}

// Build output
let mut output = String::new();
Expand All @@ -310,7 +303,7 @@ impl SourceScanner {
));
}

output.push_str(&body);
output.push_str(&combined);

// Write to build directory
std::fs::create_dir_all(&self.build_dir)?;
Expand Down Expand Up @@ -655,17 +648,6 @@ pub fn extract_function_prototypes(source: &str) -> Vec<String> {
.collect()
}

/// Find existing forward declarations in source.
fn find_existing_forward_declarations(source: &str) -> Vec<String> {
let Some(tree) = parse_cpp_source(source) else {
return Vec::new();
};

let mut declarations = Vec::new();
collect_forward_declarations(tree.root_node(), source, &mut declarations);
declarations
}

fn parse_cpp_source(source: &str) -> Option<tree_sitter::Tree> {
let mut parser = Parser::new();
let language = tree_sitter_cpp::LANGUAGE.into();
Expand Down Expand Up @@ -708,10 +690,20 @@ fn prototype_from_function_definition(node: Node<'_>, source: &str) -> Option<St
if signature.contains("::") || signature.starts_with('#') {
return None;
}
if is_arduino_entry_point_signature(&signature) {
return None;
}

Some(signature)
}

fn is_arduino_entry_point_signature(signature: &str) -> bool {
matches!(
signature.trim(),
"void setup()" | "void setup(void)" | "void loop()" | "void loop(void)"
)
}

fn has_skipped_function_context(node: Node<'_>) -> bool {
let mut current = node.parent();
while let Some(parent) = current {
Expand All @@ -727,30 +719,6 @@ fn has_skipped_function_context(node: Node<'_>) -> bool {
false
}

fn collect_forward_declarations(node: Node<'_>, source: &str, declarations: &mut Vec<String>) {
if node.kind() == "declaration"
&& !has_skipped_function_context(node)
&& has_descendant_kind(node, "function_declarator")
{
if let Some(text) = source.get(node.start_byte()..node.end_byte()) {
let declaration = text.trim();
if declaration.ends_with(';') && !declaration.contains("::") {
declarations.push(declaration.to_string());
}
}
return;
}

let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect_forward_declarations(child, source, declarations);
}
}

fn has_descendant_kind(node: Node<'_>, kind: &str) -> bool {
find_descendant_kind(node, kind).is_some()
}

fn find_descendant_kind<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
Expand Down
44 changes: 31 additions & 13 deletions crates/fbuild-build/src/source_scanner/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,9 @@ fn test_scan_multiple_ino_files_uses_platformio_main_first() {
assert!(sources[0].ends_with("main.ino.cpp"));
let content = fs::read_to_string(&sources[0]).unwrap();

let main_pos = content.find("void setup()").unwrap();
let a_pos = content.find("void aTab()").unwrap();
let z_pos = content.find("void zTab()").unwrap();
let main_pos = content.rfind("void setup()").unwrap();
let a_pos = content.rfind("void aTab()").unwrap();
let z_pos = content.rfind("void zTab()").unwrap();
assert!(main_pos < a_pos);
assert!(a_pos < z_pos);
}
Expand All @@ -141,9 +141,9 @@ fn test_scan_multiple_ino_files_uses_arduino_named_primary_first() {
assert!(sources[0].ends_with("Blink.ino.cpp"));
let content = fs::read_to_string(&sources[0]).unwrap();

let primary_pos = content.find("void setup()").unwrap();
let a_pos = content.find("void aTab()").unwrap();
let z_pos = content.find("void zTab()").unwrap();
let primary_pos = content.rfind("void setup()").unwrap();
let a_pos = content.rfind("void aTab()").unwrap();
let z_pos = content.rfind("void zTab()").unwrap();
assert!(primary_pos < a_pos);
assert!(a_pos < z_pos);
}
Expand All @@ -162,9 +162,9 @@ fn test_scan_multiple_ino_files_falls_back_to_setup_loop_primary() {
assert!(sources[0].ends_with("z_entry.ino.cpp"));
let content = fs::read_to_string(&sources[0]).unwrap();

let primary_pos = content.find("void setup()").unwrap();
let a_pos = content.find("void aTab()").unwrap();
let b_pos = content.find("void bTab()").unwrap();
let primary_pos = content.rfind("void setup()").unwrap();
let a_pos = content.rfind("void aTab()").unwrap();
let b_pos = content.rfind("void bTab()").unwrap();
assert!(primary_pos < a_pos);
assert!(a_pos < b_pos);
}
Expand Down Expand Up @@ -336,17 +336,35 @@ fn test_preprocess_with_custom_functions() {
let sources = scanner.scan_sketch_sources().unwrap();
let content = fs::read_to_string(&sources[0]).unwrap();

// Should have auto-generated prototypes
// Should have auto-generated prototypes for custom helpers, but not for
// Arduino-owned setup()/loop().
assert!(content.contains("int add(int a, int b)"));
assert!(content.contains("void setup()"));
assert!(!content.contains("void setup();"));
assert!(!content.contains("void loop();"));
}

#[test]
fn test_preprocess_preserves_existing_forward_declarations() {
let (_tmp, src_dir, build_dir) = setup_project(&[(
"sketch.ino",
"extern void helper();\n\nvoid setup() {\n helper();\n}\n\nvoid loop() {}\n",
)]);
let scanner = SourceScanner::new(&src_dir, &build_dir);
let sources = scanner.scan_sketch_sources().unwrap();
let content = fs::read_to_string(&sources[0]).unwrap();

assert!(content.contains("extern void helper();"));
}

#[test]
fn test_function_prototype_extraction() {
let source = "void setup() {\n}\nint compute(float x, int y) {\n return 0;\n}\nconst char* getName() {\n return \"\";\n}\n";
let protos = extract_function_prototypes(source);
assert!(protos.len() >= 2);
assert!(protos.iter().any(|p| p.contains("setup")));
assert!(
!protos.iter().any(|p| p.contains("setup")),
"Arduino entry points are declared by Arduino.h and should not be auto-prototyped"
);
assert!(protos.iter().any(|p| p.contains("compute")));
}

Expand Down Expand Up @@ -397,7 +415,7 @@ void helper() {}
void Controller::external_tick() {}
"#;
let protos = extract_function_prototypes(source);
assert!(protos.iter().any(|p| p == "void setup()"));
assert!(!protos.iter().any(|p| p == "void setup()"));
assert!(!protos.iter().any(|p| p.contains("if")));
assert!(!protos.iter().any(|p| p.contains("while")));
assert!(!protos.iter().any(|p| p.contains("callback")));
Expand Down
86 changes: 86 additions & 0 deletions crates/fbuild-build/tests/nxplpc_core_compile_commands.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
//! Verifies fbuild's nxplpc compile command shape against ArduinoCore-LPC8xx.

use fbuild_build::{BuildOrchestrator, BuildParams};
use fbuild_core::BuildProfile;
use serde_json::Value;
use std::fs;
use std::path::{Path, PathBuf};

fn arduino_core_repo() -> Option<PathBuf> {
let home = std::env::var_os("USERPROFILE")
.map(PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(PathBuf::from))?;
let repo = home.join("dev").join("ArduinoCore-LPC8xx");
repo.join("platformio.ini").is_file().then_some(repo)
}

fn build_core_repo(repo: &Path, env_name: &str) -> tempfile::TempDir {
let tmp = tempfile::TempDir::new().expect("tempdir");
let build_dir = tmp
.path()
.join(".fbuild/build")
.join(env_name)
.join("release");

let params = BuildParams {
project_dir: repo.to_path_buf(),
env_name: env_name.to_string(),
clean: true,
profile: BuildProfile::Release,
build_dir,
verbose: true,
jobs: None,
generate_compiledb: true,
compiledb_only: false,
log_sender: None,
symbol_analysis: false,
symbol_analysis_path: None,
no_timestamp: false,
src_dir: None,
pio_env: Default::default(),
extra_build_flags: Vec::new(),
watch_set_cache: None,
bloat_analysis: false,
};

let orchestrator = fbuild_build::nxplpc::orchestrator::NxpLpcOrchestrator;
let result = orchestrator
.build(&params)
.expect("ArduinoCore-LPC8xx nxplpc build should succeed");
assert!(result.success);
tmp
}

#[test]
#[ignore = "requires local ~/dev/ArduinoCore-LPC8xx checkout and ARM toolchain package"]
fn arduino_core_lpc845brk_compile_commands_match_platform_txt() {
let Some(repo) = arduino_core_repo() else {
eprintln!("skipping: ~/dev/ArduinoCore-LPC8xx not found");
return;
};
let tmp = build_core_repo(&repo, "lpc845brk");
let compile_db = tmp
.path()
.join(".fbuild/build/lpc845brk/release/compile_commands.json");
let text = fs::read_to_string(&compile_db).expect("compile_commands.json");
let entries: Vec<Value> = serde_json::from_str(&text).expect("valid compile database");
let args = entries
.first()
.and_then(|entry| entry.get("arguments"))
.and_then(Value::as_array)
.expect("first compile command has arguments");

let has = |needle: &str| args.iter().any(|arg| arg.as_str() == Some(needle));
assert!(has("-std=gnu++11"));
assert!(has("-fno-use-cxa-atexit"));
assert!(!has("-std=gnu++17"));
assert!(!args.iter().any(|arg| {
arg.as_str()
.is_some_and(|arg| arg == "-flto" || arg.starts_with("-flto="))
}));
assert!(!args.iter().any(|arg| {
arg.as_str()
.is_some_and(|arg| arg.starts_with("-mfloat-abi"))
}));
assert!(!has("-nostartfiles"));
}
2 changes: 1 addition & 1 deletion tests/platform/lpc845_build_flags/src/main.ino
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

#include <Arduino.h>

extern void check_flag_no_op();
extern "C" void check_flag_no_op(void);

void setup() {
check_flag_no_op();
Expand Down
Loading