diff --git a/crates/fbuild-build/src/apollo3/README.md b/crates/fbuild-build/src/apollo3/README.md new file mode 100644 index 00000000..37f02872 --- /dev/null +++ b/crates/fbuild-build/src/apollo3/README.md @@ -0,0 +1,3 @@ +# Apollo3 Build Orchestrator + +Build orchestrator for the Ambiq Apollo3 platform (ARM Cortex-M4F). Uses the generic ARM compiler/linker with mbed-os response files from the SparkFun Arduino Apollo3 core. diff --git a/crates/fbuild-build/src/apollo3/configs/README.md b/crates/fbuild-build/src/apollo3/configs/README.md new file mode 100644 index 00000000..297211a5 --- /dev/null +++ b/crates/fbuild-build/src/apollo3/configs/README.md @@ -0,0 +1,3 @@ +# Apollo3 MCU Configurations + +Embedded JSON configs for Apollo3 (Ambiq Micro) ARM Cortex-M4F MCUs. diff --git a/crates/fbuild-build/src/apollo3/configs/apollo3.json b/crates/fbuild-build/src/apollo3/configs/apollo3.json new file mode 100644 index 00000000..aa01c1ce --- /dev/null +++ b/crates/fbuild-build/src/apollo3/configs/apollo3.json @@ -0,0 +1,65 @@ +{ + "name": "Apollo3", + "description": "Ambiq Micro Apollo3 (ARM Cortex-M4F)", + "architecture": "arm-cortex-m4f", + + "compiler_flags": { + "common": [ + "-mcpu=cortex-m4", + "-mthumb", + "-mfloat-abi=hard", + "-mfpu=fpv4-sp-d16", + "-fdata-sections", + "-ffunction-sections", + "-fmessage-length=0", + "-fno-exceptions", + "-fomit-frame-pointer", + "-funsigned-char", + "-MMD" + ], + "c": [ + "-std=gnu11" + ], + "cxx": [ + "-std=gnu++14", + "-fno-rtti" + ] + }, + + "linker_flags": [ + "-mcpu=cortex-m4", + "-mthumb", + "-mfloat-abi=hard", + "-mfpu=fpv4-sp-d16", + "-Wl,--gc-sections", + "-Wl,-n" + ], + + "linker_libs": [ + "-lsupc++", + "-lstdc++", + "-lm" + ], + + "objcopy": { + "output_format": "binary", + "remove_sections": [] + }, + + "profiles": { + "release": { + "compile_flags": ["-Os"], + "link_flags": [] + }, + "quick": { + "compile_flags": ["-Os"], + "link_flags": [] + } + }, + + "defines": [ + ["ARDUINO", "10808"], + "MBED_NO_GLOBAL_USING_DIRECTIVE", + "CORDIO_ZERO_COPY_HCI" + ] +} diff --git a/crates/fbuild-build/src/apollo3/mcu_config.rs b/crates/fbuild-build/src/apollo3/mcu_config.rs new file mode 100644 index 00000000..42a18475 --- /dev/null +++ b/crates/fbuild-build/src/apollo3/mcu_config.rs @@ -0,0 +1,77 @@ +//! Data-driven Apollo3 MCU configuration from embedded JSON. +//! +//! Maps Apollo3 MCU names to the appropriate ARM Cortex-M4F configuration. + +use fbuild_core::Result; + +use crate::generic_arm::ArmMcuConfig; + +const APOLLO3_JSON: &str = include_str!("configs/apollo3.json"); + +/// Load MCU configuration for the Apollo3. +pub fn get_apollo3_config_for_mcu(mcu: &str) -> Result { + let json = match mcu { + "apollo3" => APOLLO3_JSON, + _ => { + // Default to Apollo3 for any Ambiq MCU variant + if mcu.starts_with("ama3b") || mcu.contains("apollo") { + APOLLO3_JSON + } else { + return Err(fbuild_core::FbuildError::ConfigError(format!( + "unsupported Apollo3 MCU: '{}' (supported: apollo3, ama3b*)", + mcu + ))); + } + } + }; + serde_json::from_str(json).map_err(|e| { + fbuild_core::FbuildError::ConfigError(format!( + "failed to parse Apollo3 MCU config for '{}': {}", + mcu, e + )) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_load_apollo3_config() { + let config = get_apollo3_config_for_mcu("apollo3").unwrap(); + assert_eq!(config.name, "Apollo3"); + assert_eq!(config.architecture, "arm-cortex-m4f"); + } + + #[test] + fn test_load_apollo3_ama3b_variant() { + let config = get_apollo3_config_for_mcu("ama3b1kk").unwrap(); + assert_eq!(config.name, "Apollo3"); + } + + #[test] + fn test_unsupported_mcu() { + assert!(get_apollo3_config_for_mcu("stm32f103").is_err()); + } + + #[test] + fn test_apollo3_compiler_flags() { + let config = get_apollo3_config_for_mcu("apollo3").unwrap(); + assert!(config + .compiler_flags + .common + .contains(&"-mcpu=cortex-m4".to_string())); + assert!(config + .compiler_flags + .common + .contains(&"-mthumb".to_string())); + assert!(config + .compiler_flags + .common + .contains(&"-mfloat-abi=hard".to_string())); + assert!(config + .compiler_flags + .common + .contains(&"-mfpu=fpv4-sp-d16".to_string())); + } +} diff --git a/crates/fbuild-build/src/apollo3/mod.rs b/crates/fbuild-build/src/apollo3/mod.rs new file mode 100644 index 00000000..22bcd93c --- /dev/null +++ b/crates/fbuild-build/src/apollo3/mod.rs @@ -0,0 +1,27 @@ +//! Apollo3 platform build support (Ambiq Micro Apollo3 / SparkFun Artemis). + +pub mod mcu_config; +pub mod orchestrator; + +pub use orchestrator::Apollo3Orchestrator; + +/// Apollo3 platform support. +pub struct Apollo3PlatformSupport; + +impl crate::PlatformSupport for Apollo3PlatformSupport { + fn create_orchestrator(&self) -> Box { + orchestrator::create() + } + + fn install_deps(&self, project_dir: &std::path::Path) -> fbuild_core::Result<()> { + use fbuild_packages::Package; + let tc = fbuild_packages::toolchain::ArmGcc8Toolchain::new(project_dir); + Package::ensure_installed(&tc)?; + tracing::info!("ARM GCC 8 toolchain installed"); + Ok(()) + } + + fn default_board_id(&self) -> &str { + "SparkFun_RedBoard_Artemis_ATP" + } +} diff --git a/crates/fbuild-build/src/apollo3/orchestrator.rs b/crates/fbuild-build/src/apollo3/orchestrator.rs new file mode 100644 index 00000000..79110b93 --- /dev/null +++ b/crates/fbuild-build/src/apollo3/orchestrator.rs @@ -0,0 +1,281 @@ +//! Apollo3 build orchestrator — wires together config, packages, compiler, linker. +//! +//! Build phases: +//! 1. Parse platformio.ini +//! 2. Load board config (SparkFun_RedBoard_Artemis_ATP, etc.) +//! 3. Ensure ARM GCC toolchain +//! 4. Ensure Apollo3 cores (SparkFun Arduino Apollo3 core) +//! 5. Setup build directories +//! 6. Scan source files +//! 7. Parse mbed response files for flags/defines/includes +//! 8. Compile core + variant sources +//! 9. Compile sketch sources +//! 10. Link (with SVL linker script + libmbed-os.a) + convert to binary + +use std::path::Path; +use std::time::Instant; + +use fbuild_core::{Platform, Result}; + +use crate::compile_database::TargetArchitecture; +use crate::generic_arm::{ArmCompiler, ArmLinker}; +use crate::pipeline; +use crate::{BuildOrchestrator, BuildParams, BuildResult, SourceScanner}; + +/// Apollo3 platform build orchestrator. +pub struct Apollo3Orchestrator; + +impl BuildOrchestrator for Apollo3Orchestrator { + fn platform(&self) -> Platform { + Platform::Apollo3 + } + + fn build(&self, params: &BuildParams) -> Result { + let start = Instant::now(); + + // 1-2. Parse config, load board, setup build dirs, resolve src dir, collect flags + let mut ctx = pipeline::BuildContext::new( + ¶ms.project_dir, + ¶ms.env_name, + params.clean, + params.profile, + params.log_sender.clone(), + )?; + + // 3. Ensure ARM GCC 8 toolchain (Apollo3/mbed-os requires GCC 8) + let toolchain = fbuild_packages::toolchain::ArmGcc8Toolchain::new(¶ms.project_dir); + let toolchain_dir = fbuild_packages::Package::ensure_installed(&toolchain)?; + tracing::info!("arm-gcc8 toolchain at {}", toolchain_dir.display()); + + use fbuild_packages::Toolchain; + pipeline::log_toolchain_version( + &toolchain.get_gcc_path(), + "arm-none-eabi-gcc", + &mut ctx.build_log, + ); + + // 4. Ensure Apollo3 cores (SparkFun Arduino Apollo3 core) + let framework = fbuild_packages::library::Apollo3Cores::new(¶ms.project_dir); + let framework_dir = fbuild_packages::Package::ensure_installed(&framework)?; + tracing::info!("Apollo3 cores at {}", framework_dir.display()); + + // 5. Scan sources (core + variant) + let core_dir = framework.get_core_dir(&ctx.board.core); + let variant_dir = framework.get_variant_dir(&ctx.board.variant); + + let scanner = SourceScanner::new(&ctx.src_dir, &ctx.src_build_dir); + let mut sources = scanner.scan_all(Some(&core_dir), Some(&variant_dir))?; + + // Also scan variant/config for pin definitions + let variant_config_dir = variant_dir.join("config"); + if variant_config_dir.exists() { + sources + .variant_sources + .extend(scanner.scan_core_sources(&variant_config_dir)); + } + + // Scan the arduino/mbed-bridge core sources + let mbed_bridge_dir = core_dir.join("mbed-bridge"); + if mbed_bridge_dir.exists() { + sources + .core_sources + .extend(scanner.scan_core_sources(&mbed_bridge_dir)); + let core_api_dir = mbed_bridge_dir.join("core-api"); + if core_api_dir.exists() { + sources + .core_sources + .extend(scanner.scan_core_sources(&core_api_dir)); + } + } + + tracing::info!( + "sources: {} sketch, {} core, {} variant", + sources.sketch_sources.len(), + sources.core_sources.len(), + sources.variant_sources.len(), + ); + + // 6. Build MCU config + merge defines from mbed response files + let mcu_config = + super::mcu_config::get_apollo3_config_for_mcu(&ctx.board.mcu.to_lowercase())?; + let mut defines = ctx.board.get_defines(); + defines.extend(mcu_config.defines_map()); + + // Parse mbed symbol defines from variant response files + let ld_symbols = framework.read_mbed_response_file(&ctx.board.variant, ".ld-symbols"); + for token in ld_symbols.split_whitespace() { + if let Some(def) = token.strip_prefix("-D") { + if let Some((key, val)) = def.split_once('=') { + defines.insert(key.to_string(), val.to_string()); + } else { + defines.insert(def.to_string(), "1".to_string()); + } + } + } + + // 7. Build include dirs + let mut include_dirs = ctx.board.get_include_paths(&framework_dir); + include_dirs.push(ctx.src_dir.clone()); + pipeline::discover_project_includes(¶ms.project_dir, &mut include_dirs); + + // Note: Do NOT add toolchain sysroot includes for Apollo3. + // GCC 9 handles its own internal multilib include paths; adding + // generic -I paths breaks bits/c++config.h resolution. + + // mbed-bridge includes + if mbed_bridge_dir.exists() { + include_dirs.push(mbed_bridge_dir.clone()); + let core_api = mbed_bridge_dir.join("core-api"); + if core_api.exists() { + include_dirs.push(core_api.clone()); + let api_dir = core_api.join("api"); + if api_dir.exists() { + include_dirs.push(api_dir); + } + let deprecated = core_api.join("api").join("deprecated"); + if deprecated.exists() { + include_dirs.push(deprecated); + } + } + } + + // Variant config includes + if variant_config_dir.exists() { + include_dirs.push(variant_config_dir); + } + + // Parse mbed .includes response file for additional include paths + let mbed_includes = framework.read_mbed_response_file(&ctx.board.variant, ".includes"); + let core_prefix = framework.get_core_dir(""); // cores/ parent dir + for token in mbed_includes.lines() { + let token = token.trim().trim_matches('"'); + // Response file uses -iwithprefixbefore which prepends the -iprefix path + // The -iprefix is set to {runtime.platform.path}/cores/ in platform.txt + if let Some(relative) = token.strip_prefix("-iwithprefixbefore") { + let abs_path = core_prefix.join(relative); + if abs_path.exists() { + include_dirs.push(abs_path); + } + } else if let Some(path) = token.strip_prefix("-I") { + let p = std::path::PathBuf::from(path); + if p.exists() { + include_dirs.push(p); + } + } + } + + // Add mbed_config.h via -include flag (handled as a define) + let mbed_config_h = framework.get_mbed_config_h(&ctx.board.variant); + let sdk_h = core_dir.join("sdk").join("ArduinoSDK.h"); + + // Build extra compiler flags for the -include directives + let mut extra_common_flags: Vec = Vec::new(); + if mbed_config_h.exists() { + extra_common_flags.push("-include".to_string()); + extra_common_flags.push(mbed_config_h.to_string_lossy().to_string()); + } + if sdk_h.exists() { + extra_common_flags.push("-include".to_string()); + extra_common_flags.push(sdk_h.to_string_lossy().to_string()); + } + + // Merge extra flags into the MCU config's common flags + let mut augmented_config = mcu_config.clone(); + augmented_config + .compiler_flags + .common + .extend(extra_common_flags); + + let compiler = ArmCompiler::new( + toolchain.get_gcc_path(), + toolchain.get_gxx_path(), + &ctx.board.mcu, + &ctx.board.f_cpu, + defines, + include_dirs, + augmented_config.clone(), + params.profile, + params.verbose, + ); + + // 8. Create linker + let linker_script = framework.get_linker_script(); + + // Parse extra linker flags from mbed response file + let mbed_ld_flags = framework.read_mbed_response_file(&ctx.board.variant, ".ld-flags"); + let mut augmented_linker_config = augmented_config; + for token in mbed_ld_flags.split_whitespace() { + // Skip -D defines (already handled) and flags already in config + if token.starts_with("-D") { + continue; + } + if !augmented_linker_config + .linker_flags + .contains(&token.to_string()) + { + augmented_linker_config.linker_flags.push(token.to_string()); + } + } + + // Add libmbed-os.a to linker libs + let mbed_lib = framework.get_mbed_lib(&ctx.board.variant); + if mbed_lib.exists() { + // Wrap in --whole-archive so all symbols are included + augmented_linker_config + .linker_libs + .insert(0, "-Wl,--no-whole-archive".to_string()); + augmented_linker_config + .linker_libs + .insert(0, mbed_lib.to_string_lossy().to_string()); + augmented_linker_config + .linker_libs + .insert(0, "-Wl,--whole-archive".to_string()); + } + + let linker = ArmLinker::new( + toolchain.get_gcc_path(), + toolchain.get_ar_path(), + toolchain.get_objcopy_path(), + toolchain.get_size_path(), + linker_script, + augmented_linker_config, + params.profile, + ctx.board.max_flash, + ctx.board.max_ram, + params.verbose, + ); + + // 9. Run shared sequential build pipeline + pipeline::run_sequential_build( + &compiler, + &linker, + ctx, + params, + &sources, + TargetArchitecture::Arm, + "APOLLO3", + start, + ) + } +} + +/// Create an Apollo3 orchestrator. +pub fn create() -> Box { + Box::new(Apollo3Orchestrator) +} + +/// Check if a project is configured for Apollo3. +pub fn is_apollo3_project(project_dir: &Path, env_name: &str) -> bool { + crate::pipeline::is_platform_project(project_dir, env_name, fbuild_core::Platform::Apollo3) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_apollo3_orchestrator_platform() { + let orch = Apollo3Orchestrator; + assert_eq!(orch.platform(), Platform::Apollo3); + } +} diff --git a/crates/fbuild-build/src/lib.rs b/crates/fbuild-build/src/lib.rs index 0ca161ae..5aaf1f39 100644 --- a/crates/fbuild-build/src/lib.rs +++ b/crates/fbuild-build/src/lib.rs @@ -3,6 +3,7 @@ //! Each platform has its own orchestrator implementing the `BuildOrchestrator` trait. //! Orchestrators handle: source scanning, compilation, linking, size reporting. +pub mod apollo3; pub mod avr; pub mod build_output; pub mod ch32v; @@ -52,6 +53,7 @@ pub trait PlatformSupport: Send + Sync { /// Returns `Err` for platforms without a native orchestrator. pub fn get_platform_support(platform: Platform) -> Result> { match platform { + Platform::Apollo3 => Ok(Box::new(apollo3::Apollo3PlatformSupport)), Platform::AtmelAvr | Platform::AtmelMegaAvr => { Ok(Box::new(avr::AvrPlatformSupport)) } diff --git a/crates/fbuild-config/assets/boards/json/SparkFun_RedBoard_Artemis_ATP.json b/crates/fbuild-config/assets/boards/json/SparkFun_RedBoard_Artemis_ATP.json new file mode 100644 index 00000000..36aba960 --- /dev/null +++ b/crates/fbuild-config/assets/boards/json/SparkFun_RedBoard_Artemis_ATP.json @@ -0,0 +1,25 @@ +{ + "build": { + "core": "arduino", + "extra_flags": "-DARDUINO_APOLLO3_SFE_ARTEMIS_ATP", + "f_cpu": "48000000L", + "mcu": "apollo3", + "variant": "SFE_ARTEMIS_ATP" + }, + "fcpu": 48000000, + "frameworks": [ + "arduino" + ], + "id": "SparkFun_RedBoard_Artemis_ATP", + "mcu": "AMA3B1KK", + "name": "SparkFun RedBoard Artemis ATP", + "platform": "apollo3blue", + "ram": 393216, + "rom": 983040, + "upload": { + "protocol": "svl", + "speed": 460800 + }, + "url": "https://www.sparkfun.com/products/15442", + "vendor": "SparkFun" +} diff --git a/crates/fbuild-config/assets/boards/json/SparkFun_Thing_Plus_expLoRaBLE.json b/crates/fbuild-config/assets/boards/json/SparkFun_Thing_Plus_expLoRaBLE.json new file mode 100644 index 00000000..ba642d0d --- /dev/null +++ b/crates/fbuild-config/assets/boards/json/SparkFun_Thing_Plus_expLoRaBLE.json @@ -0,0 +1,25 @@ +{ + "build": { + "core": "arduino", + "extra_flags": "-DLoRa_THING_PLUS_expLoRaBLE", + "f_cpu": "48000000L", + "mcu": "apollo3", + "variant": "LoRa_THING_PLUS_expLoRaBLE" + }, + "fcpu": 48000000, + "frameworks": [ + "arduino" + ], + "id": "SparkFun_Thing_Plus_expLoRaBLE", + "mcu": "AMA3B1KK", + "name": "SparkFun Thing Plus expLoRaBLE", + "platform": "apollo3blue", + "ram": 393216, + "rom": 983040, + "upload": { + "protocol": "svl", + "speed": 460800 + }, + "url": "https://www.sparkfun.com/products/17354", + "vendor": "SparkFun" +} diff --git a/crates/fbuild-config/src/board.rs b/crates/fbuild-config/src/board.rs index 4f8eab30..a3a85fb3 100644 --- a/crates/fbuild-config/src/board.rs +++ b/crates/fbuild-config/src/board.rs @@ -234,6 +234,8 @@ impl BoardConfig { Some(fbuild_core::Platform::RenesasRa) } else if mcu.starts_with("ch32") { Some(fbuild_core::Platform::Ch32v) + } else if mcu.starts_with("apollo3") || mcu.starts_with("ama3b") { + Some(fbuild_core::Platform::Apollo3) } else { None } @@ -338,6 +340,7 @@ impl BoardConfig { Some(fbuild_core::Platform::AtmelSam) => "SAM".to_string(), Some(fbuild_core::Platform::Teensy) => "TEENSY".to_string(), Some(fbuild_core::Platform::Ch32v) => "CH32V".to_string(), + Some(fbuild_core::Platform::Apollo3) => "APOLLO3".to_string(), Some(fbuild_core::Platform::Wasm) | None => String::new(), } } diff --git a/crates/fbuild-core/src/lib.rs b/crates/fbuild-core/src/lib.rs index 898e139f..bb2ab58c 100644 --- a/crates/fbuild-core/src/lib.rs +++ b/crates/fbuild-core/src/lib.rs @@ -71,6 +71,7 @@ impl BuildProfile { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum Platform { + Apollo3, AtmelAvr, AtmelMegaAvr, AtmelSam, @@ -93,9 +94,12 @@ impl Platform { /// bare names, owner-prefixed, versioned, git URLs, git refs, local paths. pub fn from_platform_str(s: &str) -> Option { let s = s.to_lowercase(); + // Apollo3 (Ambiq Micro) — check before generic substring matches. + if s.contains("apollo3") { + Some(Self::Apollo3) // Check espressif8266 before espressif32 to avoid false match // ("espressif8266" does not contain "espressif32", but be explicit). - if s.contains("espressif8266") { + } else if s.contains("espressif8266") { Some(Self::Espressif8266) } else if s.contains("espressif32") { Some(Self::Espressif32) diff --git a/crates/fbuild-packages/src/library/apollo3_core.rs b/crates/fbuild-packages/src/library/apollo3_core.rs new file mode 100644 index 00000000..95a9842b --- /dev/null +++ b/crates/fbuild-packages/src/library/apollo3_core.rs @@ -0,0 +1,246 @@ +//! Apollo3 (SparkFun Arduino Apollo3) framework package. +//! +//! Downloads and manages the Arduino Apollo3 core from GitHub. +//! Provides paths to: cores/arduino, variants/, libraries/, tools/. +//! The Apollo3 core uses mbed-os with pre-built libraries and response files +//! in each variant directory. + +use std::path::{Path, PathBuf}; + +use crate::{CacheSubdir, Framework, PackageBase, PackageInfo}; + +const APOLLO3_CORE_VERSION: &str = "2.2.1"; +/// Use the Arduino board manager package which includes all submodules pre-packaged +/// (mbed-os, mbed-bridge, SVL uploader with linker scripts). +const APOLLO3_CORE_URL: &str = + "https://github.com/sparkfun/Arduino_Apollo3/releases/download/v2.2.1/Arduino_Apollo3.tar.gz"; + +/// Apollo3 (SparkFun Arduino) core framework manager. +pub struct Apollo3Cores { + base: PackageBase, + install_dir: Option, +} + +impl Apollo3Cores { + pub fn new(project_dir: &Path) -> Self { + Self { + base: PackageBase::new( + "apollo3-core", + APOLLO3_CORE_VERSION, + APOLLO3_CORE_URL, + APOLLO3_CORE_URL, + None, + CacheSubdir::Platforms, + project_dir, + ), + install_dir: None, + } + } + + #[cfg(test)] + fn with_cache_root(project_dir: &Path, cache_root: &Path) -> Self { + Self { + base: PackageBase::with_cache_root( + "apollo3-core", + APOLLO3_CORE_VERSION, + APOLLO3_CORE_URL, + APOLLO3_CORE_URL, + None, + CacheSubdir::Platforms, + project_dir, + cache_root, + ), + install_dir: None, + } + } + + /// Get the resolved root directory of the core. + fn resolved_dir(&self) -> PathBuf { + self.install_dir + .clone() + .unwrap_or_else(|| find_core_root(&self.base.install_path())) + } + + /// Validate the extracted core has required structure. + fn validate(install_dir: &Path) -> fbuild_core::Result<()> { + let root = find_core_root(install_dir); + + // Arduino.h lives inside the mbed-bridge subdir in the Apollo3 core + let arduino_h = root.join("cores/arduino/mbed-bridge/Arduino.h"); + if !arduino_h.exists() { + return Err(fbuild_core::FbuildError::PackageError(format!( + "Apollo3 core missing cores/arduino/mbed-bridge/Arduino.h (in {})", + root.display() + ))); + } + + Ok(()) + } + + /// Get the core source directory for a specific core name. + pub fn get_core_dir(&self, core_name: &str) -> PathBuf { + self.get_cores_dir().join(core_name) + } + + /// Get the variant directory for a specific variant name. + pub fn get_variant_dir(&self, variant_name: &str) -> PathBuf { + self.get_variants_dir().join(variant_name) + } + + /// Get the linker script for SVL (SparkFun Variable Loader). + /// The linker script is at tools/uploaders/svl/0x10000.ld. + pub fn get_linker_script(&self) -> PathBuf { + self.resolved_dir() + .join("tools") + .join("uploaders") + .join("svl") + .join("0x10000.ld") + } + + /// Get the mbed directory for a given variant. + pub fn get_mbed_dir(&self, variant_name: &str) -> PathBuf { + self.get_variant_dir(variant_name).join("mbed") + } + + /// Read a mbed response file (e.g. `.c-flags`, `.cxx-flags`, `.ld-flags`). + /// Returns the file contents as a string, or empty string if not found. + pub fn read_mbed_response_file(&self, variant_name: &str, filename: &str) -> String { + let path = self.get_mbed_dir(variant_name).join(filename); + std::fs::read_to_string(&path).unwrap_or_default() + } + + /// Get the path to the pre-built libmbed-os.a for a variant. + pub fn get_mbed_lib(&self, variant_name: &str) -> PathBuf { + self.get_mbed_dir(variant_name).join("libmbed-os.a") + } + + /// Get the path to mbed_config.h for a variant. + pub fn get_mbed_config_h(&self, variant_name: &str) -> PathBuf { + self.get_mbed_dir(variant_name).join("mbed_config.h") + } +} + +impl crate::Package for Apollo3Cores { + fn ensure_installed(&self) -> fbuild_core::Result { + if self.is_installed() { + return Ok(self.resolved_dir()); + } + + let rt = tokio::runtime::Handle::try_current().ok(); + let install_path = if let Some(handle) = rt { + handle.block_on(self.base.staged_install(Self::validate))? + } else { + let rt = tokio::runtime::Runtime::new().map_err(|e| { + fbuild_core::FbuildError::PackageError(format!( + "failed to create tokio runtime: {}", + e + )) + })?; + rt.block_on(self.base.staged_install(Self::validate))? + }; + + Ok(find_core_root(&install_path)) + } + + fn is_installed(&self) -> bool { + if !self.base.is_cached() { + return false; + } + let root = find_core_root(&self.base.install_path()); + root.join("cores") + .join("arduino") + .join("mbed-bridge") + .join("Arduino.h") + .exists() + } + + fn get_info(&self) -> PackageInfo { + self.base.get_info() + } +} + +impl Framework for Apollo3Cores { + fn get_cores_dir(&self) -> PathBuf { + self.resolved_dir().join("cores") + } + + fn get_variants_dir(&self) -> PathBuf { + self.resolved_dir().join("variants") + } + + fn get_libraries_dir(&self) -> PathBuf { + self.resolved_dir().join("libraries") + } +} + +/// Find the actual core root inside an extracted archive. +/// +/// GitHub archives extract as `Arduino_Apollo3-2.2.1/` with the core inside. +fn find_core_root(install_dir: &Path) -> PathBuf { + if install_dir.join("cores").exists() { + return install_dir.to_path_buf(); + } + + if let Ok(entries) = std::fs::read_dir(install_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() && path.join("cores").exists() { + return path; + } + } + } + + install_dir.to_path_buf() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Package; + + #[test] + fn test_apollo3_cores_not_installed() { + let tmp = tempfile::TempDir::new().unwrap(); + let core = Apollo3Cores::with_cache_root(tmp.path(), &tmp.path().join("cache")); + assert!(!core.is_installed()); + } + + #[test] + fn test_find_core_root_direct() { + let tmp = tempfile::TempDir::new().unwrap(); + std::fs::create_dir_all(tmp.path().join("cores/arduino")).unwrap(); + assert_eq!(find_core_root(tmp.path()), tmp.path().to_path_buf()); + } + + #[test] + fn test_find_core_root_nested() { + let tmp = tempfile::TempDir::new().unwrap(); + let nested = tmp.path().join("Arduino_Apollo3-2.2.1"); + std::fs::create_dir_all(nested.join("cores/arduino")).unwrap(); + assert_eq!(find_core_root(tmp.path()), nested); + } + + #[test] + fn test_get_linker_script() { + let tmp = tempfile::TempDir::new().unwrap(); + let core = Apollo3Cores::new(tmp.path()); + let script = core.get_linker_script(); + assert!(script.to_string_lossy().contains("0x10000.ld")); + } + + #[test] + fn test_get_mbed_dir() { + let tmp = tempfile::TempDir::new().unwrap(); + let core = Apollo3Cores::new(tmp.path()); + let mbed_dir = core.get_mbed_dir("SFE_ARTEMIS_ATP"); + assert!(mbed_dir.to_string_lossy().contains("SFE_ARTEMIS_ATP")); + assert!(mbed_dir.to_string_lossy().contains("mbed")); + } + + #[test] + fn test_validate_missing_arduino_h() { + let tmp = tempfile::TempDir::new().unwrap(); + let result = Apollo3Cores::validate(tmp.path()); + assert!(result.is_err()); + } +} diff --git a/crates/fbuild-packages/src/library/mod.rs b/crates/fbuild-packages/src/library/mod.rs index 0268a192..94d14cab 100644 --- a/crates/fbuild-packages/src/library/mod.rs +++ b/crates/fbuild-packages/src/library/mod.rs @@ -1,5 +1,6 @@ //! Library dependency management. +pub mod apollo3_core; pub mod arduino_api; pub mod arduino_core; pub mod attiny_core; @@ -25,6 +26,7 @@ pub mod silabs_core; pub mod stm32_core; pub mod teensy_core; +pub use apollo3_core::Apollo3Cores; pub use arduino_core::ArduinoCore; pub use attiny_core::ATTinyCore; pub use avr_framework::AvrFramework; diff --git a/crates/fbuild-packages/src/toolchain/arm_gcc8.rs b/crates/fbuild-packages/src/toolchain/arm_gcc8.rs new file mode 100644 index 00000000..59c0a3b8 --- /dev/null +++ b/crates/fbuild-packages/src/toolchain/arm_gcc8.rs @@ -0,0 +1,212 @@ +//! ARM GCC 9 toolchain package (for Apollo3/mbed-os platforms). +//! +//! The SparkFun Apollo3 core uses mbed-os headers that are incompatible with +//! newer GCC versions (GCC 15+). PlatformIO's platform-apollo3blue specifies +//! `toolchain-gccarmnoneeabi@1.90201.191206` (GCC 9.2.1). +//! Downloads from the PlatformIO registry. + +use std::path::{Path, PathBuf}; + +use crate::{CacheSubdir, PackageBase, PackageInfo, Toolchain}; + +const ARM_GCC9_VERSION: &str = "9.2.1"; + +/// ARM GCC 8 toolchain manager (for Apollo3/mbed-os). +pub struct ArmGcc8Toolchain { + base: PackageBase, + install_dir: Option, +} + +impl ArmGcc8Toolchain { + pub fn new(project_dir: &Path) -> Self { + let url = platform_url(); + Self { + base: PackageBase::new( + "arm-gcc8", + ARM_GCC9_VERSION, + &url, + &url, + None, + CacheSubdir::Toolchains, + project_dir, + ), + install_dir: None, + } + } + + fn resolved_dir(&self) -> PathBuf { + self.install_dir + .clone() + .unwrap_or_else(|| find_bin_root(&self.base.install_path())) + } + + fn validate(install_dir: &Path) -> fbuild_core::Result<()> { + let root = find_bin_root(install_dir); + let bin_dir = root.join("bin"); + + if !bin_dir.exists() { + return Err(fbuild_core::FbuildError::PackageError(format!( + "arm-gcc8 bin directory not found at {}", + bin_dir.display() + ))); + } + + let gcc = tool_binary(&bin_dir, "arm-none-eabi-gcc"); + if !gcc.exists() { + return Err(fbuild_core::FbuildError::PackageError(format!( + "arm-none-eabi-gcc not found at {}", + gcc.display() + ))); + } + + Ok(()) + } +} + +impl crate::Package for ArmGcc8Toolchain { + fn ensure_installed(&self) -> fbuild_core::Result { + if self.is_installed() { + return Ok(self.resolved_dir()); + } + + let rt = tokio::runtime::Handle::try_current().ok(); + let install_path = if let Some(handle) = rt { + handle.block_on(self.base.staged_install(Self::validate))? + } else { + let rt = tokio::runtime::Runtime::new().map_err(|e| { + fbuild_core::FbuildError::PackageError(format!( + "failed to create tokio runtime: {}", + e + )) + })?; + rt.block_on(self.base.staged_install(Self::validate))? + }; + + Ok(find_bin_root(&install_path)) + } + + fn is_installed(&self) -> bool { + if !self.base.is_cached() { + return false; + } + let root = find_bin_root(&self.base.install_path()); + root.join("bin") + .join(tool_name("arm-none-eabi-gcc")) + .exists() + } + + fn get_info(&self) -> PackageInfo { + self.base.get_info() + } +} + +impl Toolchain for ArmGcc8Toolchain { + fn get_gcc_path(&self) -> PathBuf { + tool_binary(&self.resolved_dir().join("bin"), "arm-none-eabi-gcc") + } + + fn get_gxx_path(&self) -> PathBuf { + tool_binary(&self.resolved_dir().join("bin"), "arm-none-eabi-g++") + } + + fn get_ar_path(&self) -> PathBuf { + tool_binary(&self.resolved_dir().join("bin"), "arm-none-eabi-ar") + } + + fn get_objcopy_path(&self) -> PathBuf { + tool_binary(&self.resolved_dir().join("bin"), "arm-none-eabi-objcopy") + } + + fn get_size_path(&self) -> PathBuf { + tool_binary(&self.resolved_dir().join("bin"), "arm-none-eabi-size") + } + + fn get_bin_dir(&self) -> PathBuf { + self.resolved_dir().join("bin") + } +} + +/// PlatformIO registry base for toolchain-gccarmnoneeabi 1.90201.191206 (GCC 9.2.1). +const PIO_DL_BASE: &str = "https://dl.registry.platformio.org/download/platformio/tool/toolchain-gccarmnoneeabi/1.90201.191206"; + +/// Get the platform-specific download URL and optional SHA-256 checksum. +fn platform_package() -> (&'static str, Option<&'static str>) { + if cfg!(target_os = "windows") { + ( + "toolchain-gccarmnoneeabi-windows_amd64-1.90201.191206.tar.gz", + Some("31301e144002f2043f60c518b87327dfcfca9f1dc0c1add72322d553d5733f0e"), + ) + } else if cfg!(target_os = "macos") { + ( + "toolchain-gccarmnoneeabi-darwin_x86_64-1.90201.191206.tar.gz", + Some("309fb7cd5c1b12f1ba8daa6f7554cc95c96a81246b6ff4833cbb31436f8f6add"), + ) + } else { + ( + "toolchain-gccarmnoneeabi-linux_x86_64-1.90201.191206.tar.gz", + Some("140fb263798b9dc1950b3831c44d9ab01196f883012b78658b1e002b9035d26c"), + ) + } +} + +fn platform_url() -> String { + let (filename, _) = platform_package(); + format!("{}/{}", PIO_DL_BASE, filename) +} + +/// Find the actual root directory containing bin/ inside an extracted archive. +fn find_bin_root(install_dir: &Path) -> PathBuf { + if install_dir.join("bin").exists() { + return install_dir.to_path_buf(); + } + + if let Ok(entries) = std::fs::read_dir(install_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() && path.join("bin").exists() { + return path; + } + } + } + + install_dir.to_path_buf() +} + +fn tool_name(name: &str) -> String { + if cfg!(windows) { + format!("{}.exe", name) + } else { + name.to_string() + } +} + +fn tool_binary(bin_dir: &Path, name: &str) -> PathBuf { + bin_dir.join(tool_name(name)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_platform_url_is_valid() { + let url = platform_url(); + assert!(url.starts_with("https://dl.registry.platformio.org")); + assert!(url.contains("toolchain-gccarmnoneeabi")); + } + + #[test] + fn test_find_bin_root_direct() { + let tmp = tempfile::TempDir::new().unwrap(); + std::fs::create_dir_all(tmp.path().join("bin")).unwrap(); + assert_eq!(find_bin_root(tmp.path()), tmp.path().to_path_buf()); + } + + #[test] + fn test_find_bin_root_nested() { + let tmp = tempfile::TempDir::new().unwrap(); + let nested = tmp.path().join("gcc-arm-none-eabi-8-2018-q4-major"); + std::fs::create_dir_all(nested.join("bin")).unwrap(); + assert_eq!(find_bin_root(tmp.path()), nested); + } +} diff --git a/crates/fbuild-packages/src/toolchain/mod.rs b/crates/fbuild-packages/src/toolchain/mod.rs index ffa091a3..31b4c42d 100644 --- a/crates/fbuild-packages/src/toolchain/mod.rs +++ b/crates/fbuild-packages/src/toolchain/mod.rs @@ -1,6 +1,7 @@ //! Toolchain package management — AVR-GCC, ARM GCC, ESP32, RISC-V, clang tools, and other platform toolchains. pub mod arm; +pub mod arm_gcc8; pub mod avr; pub mod clang; pub mod esp32; @@ -9,6 +10,7 @@ pub mod esp8266; pub mod riscv; pub use arm::ArmToolchain; +pub use arm_gcc8::ArmGcc8Toolchain; pub use avr::AvrToolchain; pub use clang::{ClangComponent, ClangComponentKind}; pub use esp32::Esp32Toolchain;