diff --git a/crates/fbuild-build/src/avr/avr_linker.rs b/crates/fbuild-build/src/avr/avr_linker.rs index 3d37bbe8..2a8e62ed 100644 --- a/crates/fbuild-build/src/avr/avr_linker.rs +++ b/crates/fbuild-build/src/avr/avr_linker.rs @@ -134,6 +134,14 @@ impl Linker for AvrLinker { &self.size_path } + fn ar_path(&self) -> Option<&Path> { + Some(&self.ar_path) + } + + fn objcopy_path(&self) -> Option<&Path> { + Some(&self.objcopy_path) + } + fn report_size(&self, elf_path: &Path) -> Result { crate::linker::LinkerBase::report_size( &self.size_path, diff --git a/crates/fbuild-build/src/avr/orchestrator.rs b/crates/fbuild-build/src/avr/orchestrator.rs index 43b62fb4..f48b4a99 100644 --- a/crates/fbuild-build/src/avr/orchestrator.rs +++ b/crates/fbuild-build/src/avr/orchestrator.rs @@ -223,6 +223,22 @@ impl BuildOrchestrator for AvrOrchestrator { let mcu_config = super::mcu_config::get_avr_config()?; + // Snapshot for the optional build_info.json emitter — captured + // before `defines` is moved into the compiler. See + // FastLED/fbuild#297. + let info_snapshot = if params.emit_build_info { + Some(crate::build_info::BuildInfoSnapshot { + board: ctx.board.clone(), + defines: defines.clone(), + include_dirs: include_dirs.clone(), + sketch_sources: sources.sketch_sources.clone(), + link_flags: ctx.overlay_link_flags.clone(), + link_libs: ctx.overlay_link_libs.clone(), + }) + } else { + None + }; + let compiler = AvrCompiler::new( toolchain.get_gcc_path(), toolchain.get_gxx_path(), @@ -286,6 +302,39 @@ impl BuildOrchestrator for AvrOrchestrator { start, )?; + // Emit build_info.json (PIO-compatible) for downstream FastLED + // tooling when the caller opted in. See FastLED/fbuild#297. + if let Some(snap) = info_snapshot { + if build_result.success { + let example_name = params + .example_name + .clone() + .or_else(|| crate::build_info::default_example_name(¶ms.project_dir)); + let inputs = crate::build_info::OrchestratorBuildInfoInputs { + project_dir: ¶ms.project_dir, + env_name: ¶ms.env_name, + board: &snap.board, + compiler: &compiler, + linker: &linker, + include_dirs: &snap.include_dirs, + defines: &snap.defines, + link_libs: &snap.link_libs, + link_flags: &snap.link_flags, + sketch_sources: &snap.sketch_sources, + frameworks: vec!["arduino".to_string()], + platform: "atmelavr", + prog_path: build_result + .firmware_path + .as_deref() + .or(build_result.elf_path.as_deref()), + example_name: example_name.as_deref(), + }; + if let Err(e) = crate::build_info::emit_build_info_for_orchestrator(inputs) { + tracing::warn!("failed to emit build_info.json: {}", e); + } + } + } + // 10. Persist fingerprint so the next warm invocation can hit the // fast path. Skip this for compile-db-only / symbol-analysis runs // — they don't produce the full artifact set the fast path diff --git a/crates/fbuild-build/src/build_info.rs b/crates/fbuild-build/src/build_info.rs new file mode 100644 index 00000000..b8b6b849 --- /dev/null +++ b/crates/fbuild-build/src/build_info.rs @@ -0,0 +1,560 @@ +//! `build_info.json` emitter for PlatformIO-compatible downstream tooling. +//! +//! Mirrors the JSON blob produced by `pio project metadata --json-output` +//! (a single top-level map keyed by env name → per-env metadata). Consumed +//! by FastLED's `ci/compiled_size.py` and related size/symbol scripts so +//! they keep working when the build is driven by fbuild instead of PIO. +//! +//! Tracking: FastLED/fbuild#297. +//! +//! ## File layout +//! +//! Written to `/.build/pio//build_info_.json` when +//! an example name is supplied, otherwise `build_info.json` in the same +//! directory. This matches the path candidates that the FastLED consumer +//! scripts probe. +//! +//! ## Gating +//! +//! Generation is opt-in via the `--emit-build-info` CLI flag (plumbed +//! through `BuildParams::emit_build_info`). Non-CI builds skip the I/O. + +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +use serde::{Deserialize, Serialize}; + +use fbuild_core::{FbuildError, Result}; + +/// Per-env metadata blob written under the env-name key. +/// +/// Field set mirrors what `pio project metadata --json-output` returns for +/// each environment. Any field that fbuild cannot populate is left empty +/// (empty string, empty vec) rather than omitted, so consumers can probe +/// keys unconditionally. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct BuildInfoMetadata { + /// Absolute path to the linked firmware ELF/HEX/BIN — the primary + /// artifact consumers reach for via `prog_path`. + pub prog_path: String, + /// Absolute path to the C compiler (e.g. `avr-gcc`, `xtensa-esp32-elf-gcc`). + pub cc_path: String, + /// Absolute path to the C++ compiler (e.g. `avr-g++`). + pub cxx_path: String, + /// Absolute path to the archiver (`ar`). + pub ar_path: String, + /// Absolute path to `objcopy`. + pub objcopy_path: String, + /// Absolute path to `objdump`. + pub objdump_path: String, + /// Absolute path to `addr2line`. + pub addr2line_path: String, + /// Absolute path to the `size` tool. + pub size_path: String, + + /// C compile flags (no `-D` / `-I`, those go to `defines` / `includes`). + pub cc_flags: Vec, + /// C++ compile flags. + pub cxx_flags: Vec, + /// Link-time flags. + pub link_flags: Vec, + /// Link libraries (e.g. `c`, `m`, `gcc`). + pub libs: Vec, + /// Preprocessor defines as `KEY=VALUE` strings (PIO emits a flat list). + pub defines: Vec, + /// Include directory paths. + pub includes: Vec, + + /// Raw `board.build.extra_flags` from board JSON / `platformio.ini`. + pub extra_flags: Vec, + /// Sketch source files compiled into the firmware. + pub srcs: Vec, + /// Frameworks resolved for this env (e.g. `arduino`). + pub frameworks: Vec, + /// PlatformIO platform string (e.g. `atmelavr`, `espressif32`). + pub platform: String, + /// Board id (e.g. `uno`, `esp32dev`). + pub board: String, +} + +/// Builder that accumulates per-field data and writes the final +/// `build_info_.json` (or `build_info.json`) to disk. +/// +/// Designed as a `with_*` chain so orchestrators can populate only the +/// fields they have without juggling 20-argument helper signatures. +#[derive(Debug, Clone, Default)] +pub struct BuildInfoBuilder { + metadata: BuildInfoMetadata, +} + +impl BuildInfoBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn with_prog_path(mut self, path: Option<&Path>) -> Self { + if let Some(p) = path { + self.metadata.prog_path = p.to_string_lossy().to_string(); + } + self + } + + pub fn with_cc_path(mut self, path: &Path) -> Self { + self.metadata.cc_path = path.to_string_lossy().to_string(); + self + } + + pub fn with_cxx_path(mut self, path: &Path) -> Self { + self.metadata.cxx_path = path.to_string_lossy().to_string(); + self + } + + pub fn with_ar_path(mut self, path: &Path) -> Self { + self.metadata.ar_path = path.to_string_lossy().to_string(); + self + } + + pub fn with_objcopy_path(mut self, path: &Path) -> Self { + self.metadata.objcopy_path = path.to_string_lossy().to_string(); + self + } + + pub fn with_objdump_path(mut self, path: &Path) -> Self { + self.metadata.objdump_path = path.to_string_lossy().to_string(); + self + } + + pub fn with_addr2line_path(mut self, path: &Path) -> Self { + self.metadata.addr2line_path = path.to_string_lossy().to_string(); + self + } + + pub fn with_size_path(mut self, path: &Path) -> Self { + self.metadata.size_path = path.to_string_lossy().to_string(); + self + } + + pub fn with_cc_flags(mut self, flags: Vec) -> Self { + self.metadata.cc_flags = flags; + self + } + + pub fn with_cxx_flags(mut self, flags: Vec) -> Self { + self.metadata.cxx_flags = flags; + self + } + + pub fn with_link_flags(mut self, flags: Vec) -> Self { + self.metadata.link_flags = flags; + self + } + + pub fn with_libs(mut self, libs: Vec) -> Self { + self.metadata.libs = libs; + self + } + + /// Set defines from a `HashMap`. + /// + /// Output is sorted for deterministic JSON and serialized as `KEY=VALUE` + /// (or bare `KEY` when value is `"1"`), matching how PIO emits defines. + pub fn with_defines_map(mut self, defines: &HashMap) -> Self { + let mut items: Vec = defines + .iter() + .map(|(k, v)| { + if v == "1" { + k.clone() + } else { + format!("{}={}", k, v) + } + }) + .collect(); + items.sort(); + self.metadata.defines = items; + self + } + + pub fn with_includes(mut self, includes: &[PathBuf]) -> Self { + self.metadata.includes = includes + .iter() + .map(|p| p.to_string_lossy().to_string()) + .collect(); + self + } + + pub fn with_extra_flags(mut self, raw: Option<&str>) -> Self { + if let Some(raw) = raw { + self.metadata.extra_flags = fbuild_core::shell_split::split(raw); + } + self + } + + pub fn with_srcs(mut self, srcs: &[PathBuf]) -> Self { + self.metadata.srcs = srcs + .iter() + .map(|p| p.to_string_lossy().to_string()) + .collect(); + self + } + + pub fn with_frameworks(mut self, frameworks: Vec) -> Self { + self.metadata.frameworks = frameworks; + self + } + + pub fn with_platform(mut self, platform: impl Into) -> Self { + self.metadata.platform = platform.into(); + self + } + + pub fn with_board(mut self, board: impl Into) -> Self { + self.metadata.board = board.into(); + self + } + + /// Consume the builder and return the populated metadata struct. + pub fn build(self) -> BuildInfoMetadata { + self.metadata + } +} + +/// Compute the path where `build_info_.json` (or +/// `build_info.json` when `example_name` is `None`) should be written. +/// +/// Always lands under `/.build/pio//` so the path +/// matches the candidate list in FastLED's `_find_build_info`. +pub fn build_info_path(project_dir: &Path, board: &str, example_name: Option<&str>) -> PathBuf { + let dir = project_dir.join(".build").join("pio").join(board); + let filename = match example_name { + Some(name) if !name.is_empty() => format!("build_info_{}.json", name), + _ => "build_info.json".to_string(), + }; + dir.join(filename) +} + +/// Wrap `metadata` under the env-name key (PIO's convention) and write +/// pretty-printed JSON to disk. +/// +/// Creates the parent directory if necessary. Returns the absolute path of +/// the written file on success. +pub fn write_build_info_json( + project_dir: &Path, + env_name: &str, + board: &str, + example_name: Option<&str>, + metadata: &BuildInfoMetadata, +) -> Result { + let path = build_info_path(project_dir, board, example_name); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent).map_err(|e| { + FbuildError::Other(format!( + "failed to create build_info.json parent dir {}: {}", + parent.display(), + e + )) + })?; + } + let mut envelope: HashMap = HashMap::new(); + envelope.insert(env_name.to_string(), metadata); + let body = serde_json::to_string_pretty(&envelope) + .map_err(|e| FbuildError::Other(format!("failed to serialize build_info.json: {}", e)))?; + std::fs::write(&path, body).map_err(|e| { + FbuildError::Other(format!( + "failed to write build_info.json to {}: {}", + path.display(), + e + )) + })?; + Ok(path) +} + +/// Derive a default example name from a project directory's basename. +/// +/// FastLED's CI invokes fbuild with project dirs like `tests/platform/uno`, +/// `examples/Blink`, etc. The basename is the conventional example name. +/// Returns `None` for unprintable / empty basenames. +pub fn default_example_name(project_dir: &Path) -> Option { + project_dir + .file_name() + .and_then(|n| n.to_str()) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) +} + +/// Snapshot of the per-build data needed by the `build_info.json` emitter, +/// captured before the orchestrator hands off ownership of its +/// `BuildContext` to the shared pipeline. +/// +/// The pipeline consumes `BuildContext` by value, so any orchestrator that +/// wants to emit `build_info.json` *after* a successful link must clone the +/// reachable fields first. This struct factors out that bookkeeping so each +/// orchestrator only has to populate one place. +#[derive(Debug, Clone)] +pub struct BuildInfoSnapshot { + pub board: fbuild_config::BoardConfig, + pub defines: HashMap, + pub include_dirs: Vec, + pub sketch_sources: Vec, + pub link_flags: Vec, + pub link_libs: Vec, +} + +/// Convenience inputs aggregating the data orchestrators already have +/// readily available when they want to emit `build_info.json`. +/// +/// Designed to be cheap to build at the link-result handoff point: each +/// field is a borrowed slice / reference into the existing build context. +/// The orchestrator passes this to [`emit_build_info_for_orchestrator`], +/// which assembles the metadata, writes the JSON, and returns the output +/// path on success. +pub struct OrchestratorBuildInfoInputs<'a> { + pub project_dir: &'a Path, + pub env_name: &'a str, + pub board: &'a fbuild_config::BoardConfig, + pub compiler: &'a dyn crate::compiler::Compiler, + pub linker: &'a dyn crate::linker::Linker, + pub include_dirs: &'a [PathBuf], + pub defines: &'a HashMap, + pub link_libs: &'a [String], + pub link_flags: &'a [String], + pub sketch_sources: &'a [PathBuf], + pub frameworks: Vec, + pub platform: &'a str, + pub prog_path: Option<&'a Path>, + pub example_name: Option<&'a str>, +} + +/// Assemble a [`BuildInfoMetadata`] from orchestrator-side data and write +/// the JSON to disk. +/// +/// Tool paths fbuild does not natively track (`objdump`, `addr2line`) are +/// inferred from the size tool's parent directory and naming prefix when +/// possible, matching FastLED's `insert_tool_aliases` fallback logic. +/// +/// Returns the path of the written file on success. +pub fn emit_build_info_for_orchestrator( + inputs: OrchestratorBuildInfoInputs<'_>, +) -> Result { + let cc_path = inputs.compiler.gcc_path(); + let cxx_path = inputs.compiler.gxx_path(); + let size_path = inputs.linker.size_tool_path(); + // Filter out the include/define payload from compiler flags so the + // `cc_flags` / `cxx_flags` arrays match PIO's convention (which keeps + // `-D` and `-I` in `defines` / `includes` instead). Splitting also lets + // size-check scripts diff flags without churn from include paths. + let cc_flags = strip_define_include(inputs.compiler.c_flags()); + let cxx_flags = strip_define_include(inputs.compiler.cpp_flags()); + + let (objdump_path, addr2line_path) = derive_objdump_addr2line(size_path); + let ar_path = inputs + .linker + .ar_path() + .map(|p| p.to_path_buf()) + .unwrap_or_default(); + let objcopy_path = inputs + .linker + .objcopy_path() + .map(|p| p.to_path_buf()) + .unwrap_or_default(); + + let metadata = BuildInfoBuilder::new() + .with_prog_path(inputs.prog_path) + .with_cc_path(cc_path) + .with_cxx_path(cxx_path) + .with_ar_path(&ar_path) + .with_objcopy_path(&objcopy_path) + .with_objdump_path(&objdump_path) + .with_addr2line_path(&addr2line_path) + .with_size_path(size_path) + .with_cc_flags(cc_flags) + .with_cxx_flags(cxx_flags) + .with_link_flags(inputs.link_flags.to_vec()) + .with_libs(inputs.link_libs.to_vec()) + .with_defines_map(inputs.defines) + .with_includes(inputs.include_dirs) + .with_extra_flags(inputs.board.extra_flags.as_deref()) + .with_srcs(inputs.sketch_sources) + .with_frameworks(inputs.frameworks) + .with_platform(inputs.platform) + .with_board(&inputs.board.board) + .build(); + + write_build_info_json( + inputs.project_dir, + inputs.env_name, + &inputs.board.board, + inputs.example_name, + &metadata, + ) +} + +/// Drop `-D` and `-I` flags from a compile-flag list so the remainder is +/// safe to drop into `cc_flags` / `cxx_flags` alongside the separate +/// `defines` / `includes` arrays (PIO's convention). +fn strip_define_include(flags: Vec) -> Vec { + flags + .into_iter() + .filter(|f| !f.starts_with("-D") && !f.starts_with("-I")) + .collect() +} + +/// Derive `objdump` and `addr2line` paths from the `size` tool's path by +/// substituting the tool basename suffix. Returns empty paths when the +/// `size` basename doesn't end in `size` (in which case the consumer can +/// fall back to PATH lookups via `insert_tool_aliases`). +fn derive_objdump_addr2line(size_path: &Path) -> (PathBuf, PathBuf) { + let Some(file_name) = size_path.file_name().and_then(|n| n.to_str()) else { + return (PathBuf::new(), PathBuf::new()); + }; + let Some(parent) = size_path.parent() else { + return (PathBuf::new(), PathBuf::new()); + }; + // `avr-size` → prefix `avr-`, suffix `""` (or `.exe` on Windows). + let (prefix, suffix) = split_tool_name(file_name, "size"); + let Some((prefix, suffix)) = prefix.map(|p| (p, suffix)) else { + return (PathBuf::new(), PathBuf::new()); + }; + let objdump = parent.join(format!("{}objdump{}", prefix, suffix)); + let addr2line = parent.join(format!("{}addr2line{}", prefix, suffix)); + (objdump, addr2line) +} + +/// Split a tool basename like `arm-none-eabi-size.exe` around the substring +/// `tool` and return `(Some(prefix), suffix)` on a match. Returns +/// `(None, "")` when the substring is absent. +fn split_tool_name<'a>(file_name: &'a str, tool: &str) -> (Option<&'a str>, &'a str) { + let Some(idx) = file_name.rfind(tool) else { + return (None, ""); + }; + let prefix = &file_name[..idx]; + let suffix = &file_name[idx + tool.len()..]; + (Some(prefix), suffix) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Round-trip the metadata struct through serde and assert the + /// PIO-shaped keys are all present on the wire. + #[test] + fn metadata_serializes_with_expected_keys() { + let mut defines = HashMap::new(); + defines.insert("F_CPU".to_string(), "16000000L".to_string()); + defines.insert("PLATFORMIO".to_string(), "1".to_string()); + let metadata = BuildInfoBuilder::new() + .with_prog_path(Some(Path::new("/tmp/firmware.hex"))) + .with_cc_path(Path::new("/tc/avr-gcc")) + .with_cxx_path(Path::new("/tc/avr-g++")) + .with_ar_path(Path::new("/tc/avr-ar")) + .with_objcopy_path(Path::new("/tc/avr-objcopy")) + .with_objdump_path(Path::new("/tc/avr-objdump")) + .with_addr2line_path(Path::new("/tc/avr-addr2line")) + .with_size_path(Path::new("/tc/avr-size")) + .with_cc_flags(vec!["-std=gnu11".into(), "-Os".into()]) + .with_cxx_flags(vec!["-std=gnu++11".into()]) + .with_link_flags(vec!["-Wl,--gc-sections".into()]) + .with_libs(vec!["m".into()]) + .with_defines_map(&defines) + .with_includes(&[ + PathBuf::from("/cores/arduino"), + PathBuf::from("/variants/standard"), + ]) + .with_extra_flags(Some("-DBOARD_X=1 -DBOARD_Y")) + .with_srcs(&[PathBuf::from("/proj/src/main.cpp")]) + .with_frameworks(vec!["arduino".into()]) + .with_platform("atmelavr") + .with_board("uno") + .build(); + + let json = serde_json::to_value(&metadata).unwrap(); + for key in [ + "prog_path", + "cc_path", + "cxx_path", + "ar_path", + "objcopy_path", + "objdump_path", + "addr2line_path", + "size_path", + "cc_flags", + "cxx_flags", + "link_flags", + "libs", + "defines", + "includes", + "extra_flags", + "srcs", + "frameworks", + "platform", + "board", + ] { + assert!(json.get(key).is_some(), "expected key '{}' in JSON", key); + } + + // Defines must be sorted (deterministic output). + let defines = json.get("defines").unwrap().as_array().unwrap(); + let strs: Vec<&str> = defines.iter().map(|v| v.as_str().unwrap()).collect(); + assert_eq!(strs, vec!["F_CPU=16000000L", "PLATFORMIO"]); + + // extra_flags split with shell semantics. + let extra = json.get("extra_flags").unwrap().as_array().unwrap(); + assert_eq!(extra.len(), 2); + } + + /// File layout: `.build/pio//build_info_.json` when an + /// example name is supplied; `build_info.json` otherwise. Matches the + /// candidates that the FastLED consumer probes. + #[test] + fn path_layout_matches_fastled_consumer() { + let project = Path::new("/some/project"); + let with_example = build_info_path(project, "uno", Some("Blink")); + assert!(with_example.ends_with(".build/pio/uno/build_info_Blink.json")); + + let without = build_info_path(project, "uno", None); + assert!(without.ends_with(".build/pio/uno/build_info.json")); + + let empty_example = build_info_path(project, "uno", Some("")); + assert!(empty_example.ends_with(".build/pio/uno/build_info.json")); + } + + /// Writing produces a parseable JSON envelope keyed by env name. + #[test] + fn write_creates_parseable_envelope() { + let tmp = tempfile::TempDir::new().unwrap(); + let metadata = BuildInfoBuilder::new() + .with_prog_path(Some(Path::new("/tmp/firmware.elf"))) + .with_cc_path(Path::new("/tc/gcc")) + .with_cxx_path(Path::new("/tc/g++")) + .with_platform("atmelavr") + .with_board("uno") + .build(); + + let written = + write_build_info_json(tmp.path(), "uno", "uno", Some("Blink"), &metadata).unwrap(); + assert!(written.exists()); + assert!(written.ends_with(".build/pio/uno/build_info_Blink.json")); + + // Parses as `{env_name: {...}}` and exposes prog_path under the env key. + let blob: HashMap = + serde_json::from_str(&std::fs::read_to_string(&written).unwrap()).unwrap(); + assert_eq!(blob.len(), 1); + let inner = blob.get("uno").expect("envelope keyed by env name"); + assert_eq!(inner.prog_path, "/tmp/firmware.elf"); + assert_eq!(inner.cc_path, "/tc/gcc"); + assert_eq!(inner.platform, "atmelavr"); + assert_eq!(inner.board, "uno"); + } + + #[test] + fn default_example_name_uses_basename() { + assert_eq!( + default_example_name(Path::new("/foo/bar/examples/Blink")), + Some("Blink".to_string()) + ); + assert_eq!( + default_example_name(Path::new("tests/platform/uno")), + Some("uno".to_string()) + ); + } +} diff --git a/crates/fbuild-build/src/compile_many.rs b/crates/fbuild-build/src/compile_many.rs index 094dbfae..7112730f 100644 --- a/crates/fbuild-build/src/compile_many.rs +++ b/crates/fbuild-build/src/compile_many.rs @@ -254,6 +254,8 @@ fn build_one_sketch(inputs: SketchBuildInputs) -> SketchResult { pio_env: pio_env.into_iter().collect(), extra_build_flags: Vec::new(), watch_set_cache: None, + emit_build_info: false, + example_name: None, }; let outcome = match get_orchestrator(platform) { diff --git a/crates/fbuild-build/src/esp32/esp32_linker.rs b/crates/fbuild-build/src/esp32/esp32_linker.rs index 27839a37..374457ed 100644 --- a/crates/fbuild-build/src/esp32/esp32_linker.rs +++ b/crates/fbuild-build/src/esp32/esp32_linker.rs @@ -379,6 +379,14 @@ impl Linker for Esp32Linker { &self.size_path } + fn ar_path(&self) -> Option<&Path> { + Some(&self.ar_path) + } + + fn objcopy_path(&self) -> Option<&Path> { + Some(&self.objcopy_path) + } + fn report_size(&self, elf_path: &Path) -> Result { if let Some(size_info) = self.load_cached_size(elf_path) { tracing::info!("size: firmware.elf is unchanged, reusing cached size report"); diff --git a/crates/fbuild-build/src/esp32/orchestrator/build.rs b/crates/fbuild-build/src/esp32/orchestrator/build.rs index 81b7b95d..aa282b75 100644 --- a/crates/fbuild-build/src/esp32/orchestrator/build.rs +++ b/crates/fbuild-build/src/esp32/orchestrator/build.rs @@ -528,6 +528,22 @@ impl BuildOrchestrator for Esp32Orchestrator { .entry("ARDUINO_VARIANT".to_string()) .or_insert_with(|| format!("\\\"{}\\\"", ctx.board.variant)); + // Snapshot for the optional build_info.json emitter — captured + // here so `defines` / `include_dirs` survive the move into the + // compiler. See FastLED/fbuild#297. + let info_snapshot = if params.emit_build_info { + Some(crate::build_info::BuildInfoSnapshot { + board: ctx.board.clone(), + defines: defines.clone(), + include_dirs: include_dirs.clone(), + sketch_sources: sources.sketch_sources.clone(), + link_flags: ctx.overlay_link_flags.clone(), + link_libs: ctx.overlay_link_libs.clone(), + }) + } else { + None + }; + let compiler = Esp32Compiler::with_temp_dir( toolchain.get_gcc_path(), toolchain.get_gxx_path(), @@ -771,6 +787,40 @@ impl BuildOrchestrator for Esp32Orchestrator { params.symbol_analysis_path.as_deref(), params.verbose, ); + + // Emit build_info.json (PIO-compatible) for downstream FastLED + // tooling when the caller opted in. See FastLED/fbuild#297. + if let Some(snap) = info_snapshot { + let example_name = params + .example_name + .clone() + .or_else(|| crate::build_info::default_example_name(¶ms.project_dir)); + let prog_path = link_result + .bin_path + .as_deref() + .or(link_result.hex_path.as_deref()) + .or(link_result.elf_path.as_deref()); + let inputs = crate::build_info::OrchestratorBuildInfoInputs { + project_dir: ¶ms.project_dir, + env_name: ¶ms.env_name, + board: &snap.board, + compiler: &compiler, + linker: &linker, + include_dirs: &snap.include_dirs, + defines: &snap.defines, + link_libs: &snap.link_libs, + link_flags: &snap.link_flags, + sketch_sources: &snap.sketch_sources, + frameworks: vec!["arduino".to_string()], + platform: "espressif32", + prog_path, + example_name: example_name.as_deref(), + }; + if let Err(e) = crate::build_info::emit_build_info_for_orchestrator(inputs) { + tracing::warn!("failed to emit build_info.json: {}", e); + } + } + let elapsed = start.elapsed().as_secs_f64(); let platform_label = format!("ESP32 ({})", ctx.board.mcu); Ok(crate::pipeline::assemble_build_result( diff --git a/crates/fbuild-build/src/generic_arm/arm_linker.rs b/crates/fbuild-build/src/generic_arm/arm_linker.rs index a409e861..5d1b462f 100644 --- a/crates/fbuild-build/src/generic_arm/arm_linker.rs +++ b/crates/fbuild-build/src/generic_arm/arm_linker.rs @@ -154,6 +154,14 @@ impl Linker for ArmLinker { &self.size_path } + fn ar_path(&self) -> Option<&Path> { + Some(&self.ar_path) + } + + fn objcopy_path(&self) -> Option<&Path> { + Some(&self.objcopy_path) + } + fn report_size(&self, elf_path: &Path) -> Result { crate::linker::LinkerBase::report_size( &self.size_path, diff --git a/crates/fbuild-build/src/lib.rs b/crates/fbuild-build/src/lib.rs index 32dd28e1..11ba3f30 100644 --- a/crates/fbuild-build/src/lib.rs +++ b/crates/fbuild-build/src/lib.rs @@ -7,6 +7,7 @@ pub mod apollo3; mod arduino_props; pub mod avr; pub mod build_fingerprint; +pub mod build_info; pub mod build_output; pub mod ch32v; pub mod compile_database; @@ -179,6 +180,17 @@ pub struct BuildParams { /// the orchestrator falls back to walking on every call, which is /// the pre-existing behaviour. pub watch_set_cache: Option>, + /// When true, emit a PlatformIO-compatible `build_info.json` (or + /// `build_info_.json`) under `/.build/pio//` + /// after a successful link. Consumed by FastLED's size / symbol CI + /// scripts. Opt-in so non-CI users don't pay the I/O cost. See + /// FastLED/fbuild#297. + pub emit_build_info: bool, + /// Optional example name used to disambiguate per-sketch + /// `build_info_.json` outputs. When `None`, the orchestrator + /// derives a default from the project directory basename. Ignored + /// when `emit_build_info` is `false`. + pub example_name: Option, } /// Trait for platform-specific build orchestrators. diff --git a/crates/fbuild-build/src/linker.rs b/crates/fbuild-build/src/linker.rs index 8ae47df4..20d23dcb 100644 --- a/crates/fbuild-build/src/linker.rs +++ b/crates/fbuild-build/src/linker.rs @@ -72,6 +72,18 @@ pub trait Linker: Send + Sync { /// Used to derive the `nm` tool path for symbol analysis. fn size_tool_path(&self) -> &Path; + /// Path to the archiver (`ar`) used by this linker, if known. + /// Surfaced for `build_info.json` emission (FastLED/fbuild#297). + fn ar_path(&self) -> Option<&Path> { + None + } + + /// Path to `objcopy` used by this linker, if known. + /// Surfaced for `build_info.json` emission (FastLED/fbuild#297). + fn objcopy_path(&self) -> Option<&Path> { + None + } + /// Full link pipeline: archive core → link → convert → size → optional symbol analysis. /// /// Skips relinking when the existing firmware.elf is newer than all input diff --git a/crates/fbuild-build/src/stm32/orchestrator/mod.rs b/crates/fbuild-build/src/stm32/orchestrator/mod.rs index bebfffce..c847f9f0 100644 --- a/crates/fbuild-build/src/stm32/orchestrator/mod.rs +++ b/crates/fbuild-build/src/stm32/orchestrator/mod.rs @@ -256,6 +256,22 @@ impl BuildOrchestrator for Stm32Orchestrator { // Toolchain sysroot includes (ARM CMSIS headers, etc.) include_dirs.extend(toolchain.get_include_dirs()); + // Snapshot for the optional build_info.json emitter — captured + // before `defines` is moved into the compiler. See + // FastLED/fbuild#297. + let info_snapshot = if params.emit_build_info { + Some(crate::build_info::BuildInfoSnapshot { + board: ctx.board.clone(), + defines: defines.clone(), + include_dirs: include_dirs.clone(), + sketch_sources: sources.sketch_sources.clone(), + link_flags: ctx.overlay_link_flags.clone(), + link_libs: ctx.overlay_link_libs.clone(), + }) + } else { + None + }; + let compiler = ArmCompiler::new( toolchain.get_gcc_path(), toolchain.get_gxx_path(), @@ -310,7 +326,7 @@ impl BuildOrchestrator for Stm32Orchestrator { }; // 9. Run shared sequential build pipeline - pipeline::run_sequential_build_with_libs( + let build_result = pipeline::run_sequential_build_with_libs( &compiler, &linker, ctx, @@ -321,7 +337,42 @@ impl BuildOrchestrator for Stm32Orchestrator { TargetArchitecture::Arm, "STM32", start, - ) + )?; + + // Emit build_info.json (PIO-compatible) when the caller opted in. + // See FastLED/fbuild#297. + if let Some(snap) = info_snapshot { + if build_result.success { + let example_name = params + .example_name + .clone() + .or_else(|| crate::build_info::default_example_name(¶ms.project_dir)); + let inputs = crate::build_info::OrchestratorBuildInfoInputs { + project_dir: ¶ms.project_dir, + env_name: ¶ms.env_name, + board: &snap.board, + compiler: &compiler, + linker: &linker, + include_dirs: &snap.include_dirs, + defines: &snap.defines, + link_libs: &snap.link_libs, + link_flags: &snap.link_flags, + sketch_sources: &snap.sketch_sources, + frameworks: vec!["arduino".to_string()], + platform: "ststm32", + prog_path: build_result + .firmware_path + .as_deref() + .or(build_result.elf_path.as_deref()), + example_name: example_name.as_deref(), + }; + if let Err(e) = crate::build_info::emit_build_info_for_orchestrator(inputs) { + tracing::warn!("failed to emit build_info.json: {}", e); + } + } + } + + Ok(build_result) } } diff --git a/crates/fbuild-build/src/teensy/orchestrator.rs b/crates/fbuild-build/src/teensy/orchestrator.rs index 16d623b5..c47714e8 100644 --- a/crates/fbuild-build/src/teensy/orchestrator.rs +++ b/crates/fbuild-build/src/teensy/orchestrator.rs @@ -225,6 +225,22 @@ impl BuildOrchestrator for TeensyOrchestrator { // Toolchain sysroot includes (ARM CMSIS headers, etc.) include_dirs.extend(toolchain.get_include_dirs()); + // Snapshot for the optional build_info.json emitter — captured + // before `defines` is moved into the compiler. See + // FastLED/fbuild#297. + let info_snapshot = if params.emit_build_info { + Some(crate::build_info::BuildInfoSnapshot { + board: ctx.board.clone(), + defines: defines.clone(), + include_dirs: include_dirs.clone(), + sketch_sources: sources.sketch_sources.clone(), + link_flags: ctx.overlay_link_flags.clone(), + link_libs: ctx.overlay_link_libs.clone(), + }) + } else { + None + }; + let compiler = TeensyCompiler::new( toolchain.get_gcc_path(), toolchain.get_gxx_path(), @@ -261,6 +277,7 @@ impl BuildOrchestrator for TeensyOrchestrator { params.profile, ctx.board.max_flash, ctx.board.max_ram, + ctx.board.cmsis_dsp_lib.clone(), params.verbose, ); @@ -299,6 +316,39 @@ impl BuildOrchestrator for TeensyOrchestrator { start, )?; + // Emit build_info.json (PIO-compatible) when the caller opted in. + // See FastLED/fbuild#297. + if let Some(snap) = info_snapshot { + if build_result.success { + let example_name = params + .example_name + .clone() + .or_else(|| crate::build_info::default_example_name(¶ms.project_dir)); + let inputs = crate::build_info::OrchestratorBuildInfoInputs { + project_dir: ¶ms.project_dir, + env_name: ¶ms.env_name, + board: &snap.board, + compiler: &compiler, + linker: &linker, + include_dirs: &snap.include_dirs, + defines: &snap.defines, + link_libs: &snap.link_libs, + link_flags: &snap.link_flags, + sketch_sources: &snap.sketch_sources, + frameworks: vec!["arduino".to_string()], + platform: "teensy", + prog_path: build_result + .firmware_path + .as_deref() + .or(build_result.elf_path.as_deref()), + example_name: example_name.as_deref(), + }; + if let Err(e) = crate::build_info::emit_build_info_for_orchestrator(inputs) { + tracing::warn!("failed to emit build_info.json: {}", e); + } + } + } + if build_result.success && !params.compiledb_only && !params.symbol_analysis diff --git a/crates/fbuild-build/tests/avr_build.rs b/crates/fbuild-build/tests/avr_build.rs index 295aca58..fb1a4f08 100644 --- a/crates/fbuild-build/tests/avr_build.rs +++ b/crates/fbuild-build/tests/avr_build.rs @@ -97,6 +97,8 @@ fn build_uno_minimal() { pio_env: Default::default(), extra_build_flags: Vec::new(), watch_set_cache: None, + emit_build_info: false, + example_name: None, }; let orchestrator = fbuild_build::avr::orchestrator::AvrOrchestrator; @@ -192,6 +194,8 @@ fn compare_with_python_output() { pio_env: Default::default(), extra_build_flags: Vec::new(), watch_set_cache: None, + emit_build_info: false, + example_name: None, }; let orchestrator = fbuild_build::avr::orchestrator::AvrOrchestrator; @@ -277,6 +281,8 @@ void loop() { pio_env: Default::default(), extra_build_flags: Vec::new(), watch_set_cache: None, + emit_build_info: false, + example_name: None, }; let orchestrator = fbuild_build::avr::orchestrator::AvrOrchestrator; @@ -341,6 +347,8 @@ fn uno_build_params(project_dir: &Path, build_dir: PathBuf, clean: bool) -> Buil pio_env: Default::default(), extra_build_flags: Vec::new(), watch_set_cache: None, + emit_build_info: false, + example_name: None, } } diff --git a/crates/fbuild-build/tests/eh_frame_strip_esp32.rs b/crates/fbuild-build/tests/eh_frame_strip_esp32.rs index d010e490..b9e3ea4a 100644 --- a/crates/fbuild-build/tests/eh_frame_strip_esp32.rs +++ b/crates/fbuild-build/tests/eh_frame_strip_esp32.rs @@ -38,6 +38,8 @@ fn make_params(project_dir: &Path) -> BuildParams { pio_env: Default::default(), extra_build_flags: Vec::new(), watch_set_cache: None, + emit_build_info: false, + example_name: None, } } diff --git a/crates/fbuild-build/tests/esp32_build.rs b/crates/fbuild-build/tests/esp32_build.rs index 5dca6058..90092938 100644 --- a/crates/fbuild-build/tests/esp32_build.rs +++ b/crates/fbuild-build/tests/esp32_build.rs @@ -79,6 +79,8 @@ void loop() { pio_env: Default::default(), extra_build_flags: Vec::new(), watch_set_cache: None, + emit_build_info: false, + example_name: None, }; let orchestrator = fbuild_build::esp32::orchestrator::Esp32Orchestrator; @@ -168,6 +170,8 @@ void loop() { pio_env: Default::default(), extra_build_flags: Vec::new(), watch_set_cache: None, + emit_build_info: false, + example_name: None, }; let orchestrator = fbuild_build::esp32::orchestrator::Esp32Orchestrator; @@ -250,6 +254,8 @@ void loop() { pio_env: Default::default(), extra_build_flags: Vec::new(), watch_set_cache: None, + emit_build_info: false, + example_name: None, }; let orchestrator = fbuild_build::esp32::orchestrator::Esp32Orchestrator; @@ -333,6 +339,8 @@ void loop() { pio_env: Default::default(), extra_build_flags: Vec::new(), watch_set_cache: None, + emit_build_info: false, + example_name: None, }; let orchestrator = fbuild_build::esp32::orchestrator::Esp32Orchestrator; @@ -406,6 +414,8 @@ fn build_esp32s3_fixture() { pio_env: Default::default(), extra_build_flags: Vec::new(), watch_set_cache: None, + emit_build_info: false, + example_name: None, }; let orchestrator = fbuild_build::esp32::orchestrator::Esp32Orchestrator; @@ -471,6 +481,8 @@ fn build_nightdriverstrip_demo() { pio_env: Default::default(), extra_build_flags: Vec::new(), watch_set_cache: None, + emit_build_info: false, + example_name: None, }; let orchestrator = fbuild_build::esp32::orchestrator::Esp32Orchestrator; @@ -562,6 +574,8 @@ fn incremental_build_at(project_dir: &std::path::Path, env_name: &str) { pio_env: Default::default(), extra_build_flags: Vec::new(), watch_set_cache: None, + emit_build_info: false, + example_name: None, }; let orchestrator = fbuild_build::esp32::orchestrator::Esp32Orchestrator; @@ -653,6 +667,8 @@ fn incremental_nightdriverstrip_one_file_changed() { pio_env: Default::default(), extra_build_flags: Vec::new(), watch_set_cache: None, + emit_build_info: false, + example_name: None, }; let orchestrator = fbuild_build::esp32::orchestrator::Esp32Orchestrator; diff --git a/crates/fbuild-build/tests/stm32_acceptance.rs b/crates/fbuild-build/tests/stm32_acceptance.rs index 4fde6363..8c7f9324 100644 --- a/crates/fbuild-build/tests/stm32_acceptance.rs +++ b/crates/fbuild-build/tests/stm32_acceptance.rs @@ -79,6 +79,8 @@ fn stm32f103c8_blink_with_spi_auto_discovers_library_205_ac4() { pio_env: Default::default(), extra_build_flags: Vec::new(), watch_set_cache: None, + emit_build_info: false, + example_name: None, }; let orchestrator = fbuild_build::stm32::orchestrator::Stm32Orchestrator; diff --git a/crates/fbuild-build/tests/teensy30_acceptance.rs b/crates/fbuild-build/tests/teensy30_acceptance.rs index 975d502d..5991b01c 100644 --- a/crates/fbuild-build/tests/teensy30_acceptance.rs +++ b/crates/fbuild-build/tests/teensy30_acceptance.rs @@ -97,6 +97,8 @@ fn teensy30_analog_output_meets_205_ac2() { pio_env: Default::default(), extra_build_flags: Vec::new(), watch_set_cache: None, + emit_build_info: false, + example_name: None, }; let result = fbuild_build::teensy::orchestrator::TeensyOrchestrator diff --git a/crates/fbuild-build/tests/teensy_build.rs b/crates/fbuild-build/tests/teensy_build.rs index 8b291742..01b19e7a 100644 --- a/crates/fbuild-build/tests/teensy_build.rs +++ b/crates/fbuild-build/tests/teensy_build.rs @@ -80,6 +80,8 @@ void loop() { pio_env: Default::default(), extra_build_flags: Vec::new(), watch_set_cache: None, + emit_build_info: false, + example_name: None, }; let orchestrator = fbuild_build::teensy::orchestrator::TeensyOrchestrator; @@ -152,6 +154,8 @@ void loop() {} pio_env: Default::default(), extra_build_flags: Vec::new(), watch_set_cache: None, + emit_build_info: false, + example_name: None, }; let orchestrator = fbuild_build::teensy::orchestrator::TeensyOrchestrator; @@ -202,6 +206,8 @@ fn build_teensy41_fixture() { pio_env: Default::default(), extra_build_flags: Vec::new(), watch_set_cache: None, + emit_build_info: false, + example_name: None, }; let orchestrator = fbuild_build::teensy::orchestrator::TeensyOrchestrator; @@ -311,6 +317,8 @@ void loop() { pio_env: Default::default(), extra_build_flags: Vec::new(), watch_set_cache: None, + emit_build_info: false, + example_name: None, }; let orchestrator = fbuild_build::teensy::orchestrator::TeensyOrchestrator; diff --git a/crates/fbuild-build/tests/teensylc_acceptance.rs b/crates/fbuild-build/tests/teensylc_acceptance.rs index b3af0054..4e547746 100644 --- a/crates/fbuild-build/tests/teensylc_acceptance.rs +++ b/crates/fbuild-build/tests/teensylc_acceptance.rs @@ -49,6 +49,8 @@ fn teensylc_blink_meets_205_acceptance_criteria() { pio_env: Default::default(), extra_build_flags: Vec::new(), watch_set_cache: None, + emit_build_info: false, + example_name: None, }; let result = fbuild_build::teensy::orchestrator::TeensyOrchestrator diff --git a/crates/fbuild-cli/src/cli/args.rs b/crates/fbuild-cli/src/cli/args.rs index 0ce3967f..31aa58a0 100644 --- a/crates/fbuild-cli/src/cli/args.rs +++ b/crates/fbuild-cli/src/cli/args.rs @@ -97,6 +97,16 @@ pub enum Commands { /// Export build artifacts to a tooling-friendly directory #[arg(long)] output_dir: Option, + /// Emit `/.build/pio//build_info[_].json` + /// after a successful link. PlatformIO-compatible blob consumed by + /// FastLED's size / symbol CI scripts (FastLED/fbuild#297). Off + /// by default — opt in from CI only. + #[arg(long)] + emit_build_info: bool, + /// Optional example name for the per-sketch `build_info_.json`. + /// When omitted, the project directory's basename is used. + #[arg(long)] + example_name: Option, }, /// Deploy firmware to device Deploy { diff --git a/crates/fbuild-cli/src/cli/build.rs b/crates/fbuild-cli/src/cli/build.rs index 074f1f7f..d476ffcb 100644 --- a/crates/fbuild-cli/src/cli/build.rs +++ b/crates/fbuild-cli/src/cli/build.rs @@ -37,6 +37,8 @@ pub async fn run_build( symbol_analysis: Option, no_timestamp: bool, output_dir: Option, + emit_build_info: bool, + example_name: Option, ) -> fbuild_core::Result<()> { // FBUILD_PERF_LOG=1 enables coarse CLI-side timing (daemon handshake + // round-trip). Zero-overhead when unset. @@ -100,6 +102,8 @@ pub async fn run_build( .ok() .filter(|s| !s.is_empty()), output_dir, + emit_build_info, + example_name, pio_env: daemon_client::capture_pio_env(), }; diff --git a/crates/fbuild-cli/src/cli/clang_tools.rs b/crates/fbuild-cli/src/cli/clang_tools.rs index f1dae956..9a5618b0 100644 --- a/crates/fbuild-cli/src/cli/clang_tools.rs +++ b/crates/fbuild-cli/src/cli/clang_tools.rs @@ -48,6 +48,8 @@ pub async fn run_iwyu( None, true, // no_timestamp: compiledb generation doesn't need timestamps None, + false, // emit_build_info + None, // example_name ) .await?; if !db_path.exists() { @@ -472,6 +474,8 @@ pub async fn run_clang_tool( None, true, // no_timestamp: compiledb generation doesn't need timestamps None, + false, // emit_build_info + None, // example_name ) .await?; diff --git a/crates/fbuild-cli/src/cli/dispatch.rs b/crates/fbuild-cli/src/cli/dispatch.rs index 90b833d6..9d2c9ea6 100644 --- a/crates/fbuild-cli/src/cli/dispatch.rs +++ b/crates/fbuild-cli/src/cli/dispatch.rs @@ -69,6 +69,8 @@ pub async fn async_main() { symbol_analysis, no_timestamp, output_dir, + emit_build_info, + example_name, }) => { let project_dir = resolve_project_dir(project_dir, &top_level_project_dir); if platformio { @@ -87,6 +89,8 @@ pub async fn async_main() { symbol_analysis, no_timestamp, output_dir, + emit_build_info, + example_name, ) .await } diff --git a/crates/fbuild-cli/src/daemon_client.rs b/crates/fbuild-cli/src/daemon_client.rs index eecb32ef..5c0284fd 100644 --- a/crates/fbuild-cli/src/daemon_client.rs +++ b/crates/fbuild-cli/src/daemon_client.rs @@ -51,12 +51,23 @@ pub struct BuildRequest { /// Export a tooling-friendly artifact bundle to this directory after build. #[serde(skip_serializing_if = "Option::is_none")] pub output_dir: Option, + /// When true, request a PIO-compatible `build_info.json` (FastLED/fbuild#297). + #[serde(default, skip_serializing_if = "is_false")] + pub emit_build_info: bool, + /// Optional example name for `build_info_.json`. + #[serde(skip_serializing_if = "Option::is_none")] + pub example_name: Option, /// Snapshot of all `PLATFORMIO_*` env vars from the caller's environment. /// The daemon does not inherit caller env vars, so they are forwarded here. #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub pio_env: BTreeMap, } +#[allow(clippy::trivially_copy_pass_by_ref)] +fn is_false(b: &bool) -> bool { + !*b +} + #[derive(Debug, Serialize)] pub struct DeployRequest { pub project_dir: String, diff --git a/crates/fbuild-cli/src/mcp/tools.rs b/crates/fbuild-cli/src/mcp/tools.rs index 47b4141b..717b06bd 100644 --- a/crates/fbuild-cli/src/mcp/tools.rs +++ b/crates/fbuild-cli/src/mcp/tools.rs @@ -118,6 +118,8 @@ pub(super) async fn execute_tool( .ok() .filter(|s| !s.is_empty()), output_dir: None, + emit_build_info: false, + example_name: None, pio_env: crate::daemon_client::capture_pio_env(), }; diff --git a/crates/fbuild-daemon/src/handlers/emulator/select.rs b/crates/fbuild-daemon/src/handlers/emulator/select.rs index a755ed60..3e2bb0f0 100644 --- a/crates/fbuild-daemon/src/handlers/emulator/select.rs +++ b/crates/fbuild-daemon/src/handlers/emulator/select.rs @@ -278,6 +278,8 @@ pub async fn test_emu( Vec::new() }, watch_set_cache: Some(std::sync::Arc::clone(&ctx.watch_set_cache) as std::sync::Arc<_>), + emit_build_info: false, + example_name: None, }; let p = platform; diff --git a/crates/fbuild-daemon/src/handlers/emulator/tests_process.rs b/crates/fbuild-daemon/src/handlers/emulator/tests_process.rs index 675fa78e..7f52aa97 100644 --- a/crates/fbuild-daemon/src/handlers/emulator/tests_process.rs +++ b/crates/fbuild-daemon/src/handlers/emulator/tests_process.rs @@ -133,6 +133,8 @@ fn run_real_esp32s3_fixture_in_qemu() { "-DARDUINO_USB_CDC_ON_BOOT=0".to_string(), ], watch_set_cache: None, + emit_build_info: false, + example_name: None, }; let orchestrator = fbuild_build::esp32::orchestrator::Esp32Orchestrator; diff --git a/crates/fbuild-daemon/src/handlers/operations/build.rs b/crates/fbuild-daemon/src/handlers/operations/build.rs index 7c69b3bc..7da39758 100644 --- a/crates/fbuild-daemon/src/handlers/operations/build.rs +++ b/crates/fbuild-daemon/src/handlers/operations/build.rs @@ -127,6 +127,8 @@ pub async fn build( pio_env: req.pio_env.clone(), extra_build_flags: Vec::new(), watch_set_cache: Some(Arc::clone(&ctx.watch_set_cache) as Arc<_>), + emit_build_info: req.emit_build_info, + example_name: req.example_name.clone(), }; let project_dir_desc = req.project_dir.clone(); @@ -323,6 +325,8 @@ pub async fn build( pio_env: req.pio_env, extra_build_flags: Vec::new(), watch_set_cache: Some(Arc::clone(&ctx.watch_set_cache) as Arc<_>), + emit_build_info: req.emit_build_info, + example_name: req.example_name, }; let result = tokio::task::spawn_blocking(move || { diff --git a/crates/fbuild-daemon/src/handlers/operations/deploy.rs b/crates/fbuild-daemon/src/handlers/operations/deploy.rs index cdb07032..9d3ffb16 100644 --- a/crates/fbuild-daemon/src/handlers/operations/deploy.rs +++ b/crates/fbuild-daemon/src/handlers/operations/deploy.rs @@ -171,6 +171,8 @@ pub async fn deploy( Vec::new() }, watch_set_cache: Some(Arc::clone(&ctx.watch_set_cache) as Arc<_>), + emit_build_info: false, + example_name: None, }; let build_result = { diff --git a/crates/fbuild-daemon/src/models.rs b/crates/fbuild-daemon/src/models.rs index c73adbd8..9f968a38 100644 --- a/crates/fbuild-daemon/src/models.rs +++ b/crates/fbuild-daemon/src/models.rs @@ -44,6 +44,15 @@ pub struct BuildRequest { /// Export a tooling-friendly artifact bundle to this directory after build. #[serde(default, skip_serializing_if = "Option::is_none")] pub output_dir: Option, + /// When true, emit `/.build/pio//build_info[_].json` + /// after a successful link. PlatformIO-compatible blob consumed by + /// FastLED's size/symbol CI scripts. See FastLED/fbuild#297. + #[serde(default)] + pub emit_build_info: bool, + /// Optional example name for `build_info_.json`. When omitted, + /// the orchestrator falls back to the project directory's basename. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub example_name: Option, /// Snapshot of `PLATFORMIO_*` env vars from the CLI caller's environment. /// /// The daemon does not inherit caller env vars, so the CLI forwards them