diff --git a/crates/fbuild-build/src/nxplpc/configs/nxplpc.json b/crates/fbuild-build/src/nxplpc/configs/nxplpc.json index 62a4772e..258005ae 100644 --- a/crates/fbuild-build/src/nxplpc/configs/nxplpc.json +++ b/crates/fbuild-build/src/nxplpc/configs/nxplpc.json @@ -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" ], @@ -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"], diff --git a/crates/fbuild-build/src/nxplpc/mcu_config.rs b/crates/fbuild-build/src/nxplpc/mcu_config.rs index 4141f12f..7363403a 100644 --- a/crates/fbuild-build/src/nxplpc/mcu_config.rs +++ b/crates/fbuild-build/src/nxplpc/mcu_config.rs @@ -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] diff --git a/crates/fbuild-build/src/source_scanner.rs b/crates/fbuild-build/src/source_scanner.rs index 2fae9307..77d9f9cc 100644 --- a/crates/fbuild-build/src/source_scanner.rs +++ b/crates/fbuild-build/src/source_scanner.rs @@ -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(); @@ -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)?; @@ -655,17 +648,6 @@ pub fn extract_function_prototypes(source: &str) -> Vec { .collect() } -/// Find existing forward declarations in source. -fn find_existing_forward_declarations(source: &str) -> Vec { - 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 { let mut parser = Parser::new(); let language = tree_sitter_cpp::LANGUAGE.into(); @@ -708,10 +690,20 @@ fn prototype_from_function_definition(node: Node<'_>, source: &str) -> Option 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 { @@ -727,30 +719,6 @@ fn has_skipped_function_context(node: Node<'_>) -> bool { false } -fn collect_forward_declarations(node: Node<'_>, source: &str, declarations: &mut Vec) { - 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> { let mut cursor = node.walk(); for child in node.children(&mut cursor) { diff --git a/crates/fbuild-build/src/source_scanner/tests.rs b/crates/fbuild-build/src/source_scanner/tests.rs index d75ed11a..1c605a11 100644 --- a/crates/fbuild-build/src/source_scanner/tests.rs +++ b/crates/fbuild-build/src/source_scanner/tests.rs @@ -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); } @@ -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); } @@ -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); } @@ -336,9 +336,24 @@ 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] @@ -346,7 +361,10 @@ 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"))); } @@ -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"))); diff --git a/crates/fbuild-build/tests/nxplpc_core_compile_commands.rs b/crates/fbuild-build/tests/nxplpc_core_compile_commands.rs new file mode 100644 index 00000000..b0e65906 --- /dev/null +++ b/crates/fbuild-build/tests/nxplpc_core_compile_commands.rs @@ -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 { + 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(¶ms) + .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 = 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")); +} diff --git a/tests/platform/lpc845_build_flags/src/main.ino b/tests/platform/lpc845_build_flags/src/main.ino index 53f1edd5..9d820b49 100644 --- a/tests/platform/lpc845_build_flags/src/main.ino +++ b/tests/platform/lpc845_build_flags/src/main.ino @@ -7,7 +7,7 @@ #include -extern void check_flag_no_op(); +extern "C" void check_flag_no_op(void); void setup() { check_flag_no_op();