From 1f2a42434adaf79ead7f4ff43843e8d544bee637 Mon Sep 17 00:00:00 2001 From: zackees Date: Tue, 21 Apr 2026 20:48:12 -0700 Subject: [PATCH] fix: use Teensy ARM GCC toolchain --- crates/fbuild-build/src/teensy/README.md | 2 +- .../fbuild-build/src/teensy/orchestrator.rs | 8 +- .../fbuild-packages/src/toolchain/README.md | 3 +- crates/fbuild-packages/src/toolchain/mod.rs | 2 + .../src/toolchain/teensy_arm.rs | 343 ++++++++++++++++++ 5 files changed, 352 insertions(+), 6 deletions(-) create mode 100644 crates/fbuild-packages/src/toolchain/teensy_arm.rs diff --git a/crates/fbuild-build/src/teensy/README.md b/crates/fbuild-build/src/teensy/README.md index 24285288..51ad475e 100644 --- a/crates/fbuild-build/src/teensy/README.md +++ b/crates/fbuild-build/src/teensy/README.md @@ -1,6 +1,6 @@ # Teensy Build Support -Compiler, linker, and orchestrator for Teensy boards (4.0, 4.1, 3.x, LC) using the ARM arm-none-eabi toolchain. +Compiler, linker, and orchestrator for Teensy boards (4.0, 4.1, 3.x, LC) using the Teensy-compatible PlatformIO/PJRC arm-none-eabi toolchain. ## Modules diff --git a/crates/fbuild-build/src/teensy/orchestrator.rs b/crates/fbuild-build/src/teensy/orchestrator.rs index d46594ea..28689614 100644 --- a/crates/fbuild-build/src/teensy/orchestrator.rs +++ b/crates/fbuild-build/src/teensy/orchestrator.rs @@ -3,7 +3,7 @@ //! Build phases: //! 1. Parse platformio.ini //! 2. Load board config (teensy40/teensy41) -//! 3. Ensure ARM GCC toolchain +//! 3. Ensure Teensy-compatible ARM GCC toolchain //! 4. Ensure Teensy cores //! 5. Setup build directories //! 6. Scan source files @@ -319,10 +319,10 @@ impl BuildOrchestrator for TeensyOrchestrator { fbuild_core::FbuildError::ConfigError("missing 'board' in environment config".into()) })?; - // 3. Ensure ARM GCC toolchain - let toolchain = fbuild_packages::toolchain::ArmToolchain::new(¶ms.project_dir); + // 3. Ensure Teensy-compatible ARM GCC toolchain + let toolchain = fbuild_packages::toolchain::TeensyArmToolchain::new(¶ms.project_dir); let toolchain_dir = fbuild_packages::Package::ensure_installed(&toolchain)?; - tracing::info!("arm-gcc toolchain at {}", toolchain_dir.display()); + tracing::info!("Teensy ARM GCC toolchain at {}", toolchain_dir.display()); use fbuild_packages::Toolchain; pipeline::log_toolchain_version( diff --git a/crates/fbuild-packages/src/toolchain/README.md b/crates/fbuild-packages/src/toolchain/README.md index ba77ac40..5833f8c0 100644 --- a/crates/fbuild-packages/src/toolchain/README.md +++ b/crates/fbuild-packages/src/toolchain/README.md @@ -4,9 +4,10 @@ Platform-specific toolchain management: download, cache, and provide tool paths ## Modules -- **`mod.rs`** -- Module root; re-exports `AvrToolchain`, `ArmToolchain`, `Esp32Toolchain`, `Esp8266Toolchain`, `ClangComponent` +- **`mod.rs`** -- Module root; re-exports `AvrToolchain`, `ArmToolchain`, `TeensyArmToolchain`, `Esp32Toolchain`, `Esp8266Toolchain`, `ClangComponent` - **`avr.rs`** -- AVR-GCC 7.3.0 toolchain from Arduino's CDN - **`arm.rs`** -- ARM GCC 15.2 toolchain (arm-none-eabi) from developer.arm.com +- **`teensy_arm.rs`** -- Teensy-pinned ARM GCC 11.3.1 package from PlatformIO/PJRC - **`esp32.rs`** -- ESP32 RISC-V (`riscv32-esp-elf`) and Xtensa (`xtensa-esp-elf`) GCC from Espressif - **`esp32_metadata.rs`** -- Resolves ESP32 toolchain URLs from `tools.json` metadata packages - **`esp8266.rs`** -- ESP8266 Xtensa LX106 GCC from esp-quick-toolchain releases diff --git a/crates/fbuild-packages/src/toolchain/mod.rs b/crates/fbuild-packages/src/toolchain/mod.rs index 938f9264..886db7d0 100644 --- a/crates/fbuild-packages/src/toolchain/mod.rs +++ b/crates/fbuild-packages/src/toolchain/mod.rs @@ -10,6 +10,7 @@ pub mod esp8266; pub mod esp_qemu; pub mod riscv; pub mod rp2040_pqt; +pub mod teensy_arm; pub use arm::ArmToolchain; pub use arm_gcc8::ArmGcc8Toolchain; @@ -22,3 +23,4 @@ pub use esp_qemu::build_windows_qemu_path_env; pub use esp_qemu::{EspQemu, EspQemuArch, EspQemuRiscv32, EspQemuXtensa}; pub use riscv::RiscvToolchain; pub use rp2040_pqt::Rp2040PqtToolchain; +pub use teensy_arm::TeensyArmToolchain; diff --git a/crates/fbuild-packages/src/toolchain/teensy_arm.rs b/crates/fbuild-packages/src/toolchain/teensy_arm.rs new file mode 100644 index 00000000..29c903fe --- /dev/null +++ b/crates/fbuild-packages/src/toolchain/teensy_arm.rs @@ -0,0 +1,343 @@ +//! Teensy ARM GCC toolchain package. +//! +//! Teensyduino 1.60 is compatible with PlatformIO's Teensy-pinned +//! `toolchain-gccarmnoneeabi-teensy@1.110301.0` package, which contains +//! arm-none-eabi GCC 11.3.1. + +use std::path::{Path, PathBuf}; + +use crate::{CacheSubdir, PackageBase, PackageInfo, Toolchain}; + +const TEENSY_ARM_TOOLCHAIN_NAME: &str = "toolchain-gccarmnoneeabi-teensy"; +const TEENSY_ARM_TOOLCHAIN_VERSION: &str = "1.110301.0"; +const TEENSY_ARM_TOOLCHAIN_BASE_URL: &str = + "https://dl.registry.platformio.org/download/platformio/tool/toolchain-gccarmnoneeabi-teensy/1.110301.0"; + +/// Teensy-compatible ARM GCC toolchain manager. +pub struct TeensyArmToolchain { + base: PackageBase, + install_dir: Option, +} + +impl TeensyArmToolchain { + pub fn new(project_dir: &Path) -> Self { + let package = platform_package(); + Self { + base: PackageBase::new( + TEENSY_ARM_TOOLCHAIN_NAME, + TEENSY_ARM_TOOLCHAIN_VERSION, + &package.url(), + TEENSY_ARM_TOOLCHAIN_NAME, + Some(package.checksum), + CacheSubdir::Toolchains, + project_dir, + ), + install_dir: None, + } + } + + #[cfg(test)] + fn with_cache_root(project_dir: &Path, cache_root: &Path) -> Self { + let package = platform_package(); + Self { + base: PackageBase::with_cache_root( + TEENSY_ARM_TOOLCHAIN_NAME, + TEENSY_ARM_TOOLCHAIN_VERSION, + &package.url(), + TEENSY_ARM_TOOLCHAIN_NAME, + Some(package.checksum), + CacheSubdir::Toolchains, + project_dir, + cache_root, + ), + 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!( + "Teensy ARM GCC bin directory not found at {}", + bin_dir.display() + ))); + } + + for tool in REQUIRED_TOOLS { + let tool_path = tool_binary(&bin_dir, tool); + if !tool_path.exists() { + return Err(fbuild_core::FbuildError::PackageError(format!( + "required Teensy ARM GCC tool {} not found at {}", + tool, + tool_path.display() + ))); + } + } + + Ok(()) + } +} + +impl crate::Package for TeensyArmToolchain { + 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 TeensyArmToolchain { + 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") + } +} + +const REQUIRED_TOOLS: &[&str] = &[ + "arm-none-eabi-gcc", + "arm-none-eabi-g++", + "arm-none-eabi-ar", + "arm-none-eabi-objcopy", + "arm-none-eabi-size", +]; + +#[derive(Debug, Clone, Copy)] +struct TeensyArmPlatformPackage { + platform: &'static str, + filename: &'static str, + checksum: &'static str, +} + +fn all_platform_packages() -> [TeensyArmPlatformPackage; 6] { + [ + TeensyArmPlatformPackage { + platform: "windows", + filename: "toolchain-gccarmnoneeabi-teensy-windows-1.110301.0.tar.gz", + checksum: "cfa2479b0eb96e4081ad458891331edc2d0fa8f19bcd3ef54645d886ad1d90b1", + }, + TeensyArmPlatformPackage { + platform: "macos", + filename: "toolchain-gccarmnoneeabi-teensy-darwin_x86_64-1.110301.0.tar.gz", + checksum: "8f88880ea0a23b01c5baae3e3c343b94a84cea51bce957f4b551834628e6f2de", + }, + TeensyArmPlatformPackage { + platform: "linux-aarch64", + filename: "toolchain-gccarmnoneeabi-teensy-linux_aarch64-1.110301.0.tar.gz", + checksum: "8c626d4cf321b85c7f602c76a690151eac650bb3b1bd9c47424bcc4775bd4126", + }, + TeensyArmPlatformPackage { + platform: "linux-arm", + filename: "toolchain-gccarmnoneeabi-teensy-linux_armv6l-1.110301.0.tar.gz", + checksum: "006da609b85a2dff842ec8b443b9ecae8c63cf7fc8e799df5481eff909a9c328", + }, + TeensyArmPlatformPackage { + platform: "linux-i686", + filename: "toolchain-gccarmnoneeabi-teensy-linux_i686-1.110301.0.tar.gz", + checksum: "392330e8fdca75bc63f261ceed304683367df66dc0dd0cdb9ae501e1addfdb76", + }, + TeensyArmPlatformPackage { + platform: "linux-x86_64", + filename: "toolchain-gccarmnoneeabi-teensy-linux_x86_64-1.110301.0.tar.gz", + checksum: "958891c6cc68862bd07af7914291173721d1c5a0fcbbb9b05e8e17564dd652aa", + }, + ] +} + +fn platform_package() -> TeensyArmPlatformPackage { + let key = if cfg!(target_os = "windows") { + "windows" + } else if cfg!(target_os = "macos") { + "macos" + } else if cfg!(target_arch = "aarch64") { + "linux-aarch64" + } else if cfg!(target_arch = "arm") { + "linux-arm" + } else if cfg!(target_arch = "x86") { + "linux-i686" + } else { + "linux-x86_64" + }; + + all_platform_packages() + .into_iter() + .find(|package| package.platform == key) + .expect("no Teensy ARM GCC package for current platform") +} + +impl TeensyArmPlatformPackage { + fn url(&self) -> String { + format!("{}/{}", TEENSY_ARM_TOOLCHAIN_BASE_URL, self.filename) + } +} + +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::*; + use crate::{Package, Toolchain}; + use std::collections::HashMap; + + #[test] + fn test_platform_package_returns_platformio_url() { + let package = platform_package(); + let url = package.url(); + assert!(url.starts_with("https://dl.registry.platformio.org")); + assert!(url.contains(TEENSY_ARM_TOOLCHAIN_NAME)); + assert!(url.contains(TEENSY_ARM_TOOLCHAIN_VERSION)); + } + + #[test] + fn test_tool_name_platform() { + let name = tool_name("arm-none-eabi-gcc"); + if cfg!(windows) { + assert_eq!(name, "arm-none-eabi-gcc.exe"); + } else { + assert_eq!(name, "arm-none-eabi-gcc"); + } + } + + #[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-11.3.1"); + std::fs::create_dir_all(nested.join("bin")).unwrap(); + assert_eq!(find_bin_root(tmp.path()), nested); + } + + #[test] + fn test_teensy_arm_toolchain_get_tools() { + let tmp = tempfile::TempDir::new().unwrap(); + let tc = TeensyArmToolchain::new(tmp.path()); + let tools: HashMap = tc.get_all_tools(); + assert!(tools.contains_key("gcc")); + assert!(tools.contains_key("g++")); + assert!(tools.contains_key("ar")); + assert!(tools.contains_key("objcopy")); + assert!(tools.contains_key("size")); + } + + #[test] + fn test_teensy_arm_toolchain_not_installed() { + let tmp = tempfile::TempDir::new().unwrap(); + let tc = TeensyArmToolchain::with_cache_root(tmp.path(), &tmp.path().join("cache")); + assert!(!tc.is_installed()); + } + + #[test] + fn test_package_info_uses_platformio_package_version() { + let tmp = tempfile::TempDir::new().unwrap(); + let tc = TeensyArmToolchain::with_cache_root(tmp.path(), &tmp.path().join("cache")); + let info = tc.get_info(); + assert_eq!(info.name, TEENSY_ARM_TOOLCHAIN_NAME); + assert_eq!(info.version, TEENSY_ARM_TOOLCHAIN_VERSION); + assert!(info.url.contains(TEENSY_ARM_TOOLCHAIN_VERSION)); + } + + #[test] + fn test_all_checksums_are_valid_sha256() { + for package in all_platform_packages() { + assert_eq!( + package.checksum.len(), + 64, + "checksum for {} has wrong length", + package.platform, + ); + assert!( + package.checksum.chars().all(|c| c.is_ascii_hexdigit()), + "checksum for {} contains non-hex characters", + package.platform, + ); + } + } +}