diff --git a/crates/fbuild-build/src/avr/orchestrator.rs b/crates/fbuild-build/src/avr/orchestrator.rs index b12f6892..a97ca7c1 100644 --- a/crates/fbuild-build/src/avr/orchestrator.rs +++ b/crates/fbuild-build/src/avr/orchestrator.rs @@ -19,13 +19,12 @@ use fbuild_core::{Platform, Result}; use serde::Serialize; use crate::build_fingerprint::{ - hash_watch_set_stamps_cached, save_json, stable_hash_json, FastPathInputs, - PersistedBuildFingerprint, BUILD_FINGERPRINT_VERSION, + expected_fast_path_artifacts, stable_hash_json, FastPathCheckInputs, FastPathContract, + FastPathPersistInputs, BUILD_FINGERPRINT_VERSION, }; -use crate::compile_database::{CompileDatabase, TargetArchitecture}; +use crate::compile_database::TargetArchitecture; use crate::compiler::Compiler as _; use crate::pipeline; -use crate::zccache::FingerprintWatch; use crate::{BuildOrchestrator, BuildParams, BuildResult, SourceScanner}; use super::avr_compiler::AvrCompiler; @@ -67,33 +66,6 @@ fn profile_label(profile: fbuild_core::BuildProfile) -> &'static str { } } -fn build_fingerprint_path(build_dir: &Path) -> PathBuf { - build_dir.join("build_fingerprint.json") -} - -/// Build the watch set for the AVR fast-path check. -/// -/// Covers the project directory (sketch + local `lib/`) and the -/// resolved libraries directory if present. Mirrors the ESP32 -/// orchestrator's policy — any directory that can produce a source -/// file consumed by the build must be watched, or we risk reusing -/// stale artifacts. -fn collect_fast_path_watches(build_dir: &Path, project_dir: &Path) -> Vec { - let mut watches = Vec::new(); - if let Some(watch) = - crate::build_fingerprint::fast_path_watch("project", build_dir, project_dir) - { - watches.push(watch); - } - let resolved_libs_dir = build_dir.join("libs"); - if let Some(watch) = - crate::build_fingerprint::fast_path_watch("dep_libs", build_dir, &resolved_libs_dir) - { - watches.push(watch); - } - watches -} - impl BuildOrchestrator for AvrOrchestrator { fn platform(&self) -> Platform { Platform::AtmelAvr @@ -150,7 +122,6 @@ impl BuildOrchestrator for AvrOrchestrator { // This lives here rather than before `ensure_installed` so the hashed // toolchain/framework paths reflect the real install location. let build_dir = &ctx.build_dir; - let fingerprint_path = build_fingerprint_path(build_dir); let metadata_hash = stable_hash_json(&AvrFingerprintMetadata { version: BUILD_FINGERPRINT_VERSION, env_name: params.env_name.clone(), @@ -168,9 +139,15 @@ impl BuildOrchestrator for AvrOrchestrator { max_flash: ctx.board.max_flash, max_ram: ctx.board.max_ram, })?; - let fingerprint_watches = { + let (fast_elf, [fast_hex], fast_compile_db) = + expected_fast_path_artifacts(build_dir, ¶ms.project_dir, ["firmware.hex"]); + let fast_path = { let _g = perf.phase("fp-watches-collect"); - collect_fast_path_watches(build_dir, ¶ms.project_dir) + FastPathContract::for_project_outputs( + build_dir, + ¶ms.project_dir, + [fast_elf.clone(), fast_hex.clone(), fast_compile_db.clone()], + ) }; if !params.compiledb_only @@ -178,21 +155,13 @@ impl BuildOrchestrator for AvrOrchestrator { && params.symbol_analysis_path.is_none() { let _fast_path_phase = perf.phase("fast-path-check"); - let fast_elf = build_dir.join("firmware.elf"); - let fast_hex = build_dir.join("firmware.hex"); - let fast_compile_db = - CompileDatabase::expected_output_path(build_dir, ¶ms.project_dir); - let required_artifacts = [fast_elf.clone(), fast_hex.clone(), fast_compile_db.clone()]; - let inputs = FastPathInputs { - fingerprint_path: &fingerprint_path, + let inputs = FastPathCheckInputs { metadata_hash: &metadata_hash, - watches: &fingerprint_watches, - required_artifacts: &required_artifacts, extra_artifact_ok: None, watch_set_cache: params.watch_set_cache.as_deref(), compiler_cache: compiler_cache.as_deref(), }; - if let Some(hit) = crate::build_fingerprint::fast_path_check(&inputs)? { + if let Some(hit) = crate::build_fingerprint::fast_path_check(&fast_path, &inputs)? { ctx.build_log .push("No-op fingerprint matched; reusing existing AVR artifacts.".to_string()); let elapsed = start.elapsed().as_secs_f64(); @@ -315,35 +284,15 @@ impl BuildOrchestrator for AvrOrchestrator { && !params.symbol_analysis && params.symbol_analysis_path.is_none() { - let persisted_fingerprint = PersistedBuildFingerprint { - version: BUILD_FINGERPRINT_VERSION, - metadata_hash: metadata_hash.clone(), - file_set_hash: match hash_watch_set_stamps_cached( - &fingerprint_watches, - params.watch_set_cache.as_deref(), - ) { - Ok(hash) => Some(hash), - Err(e) => { - tracing::warn!("failed to hash watched inputs for fingerprint save: {}", e); - None - } + crate::build_fingerprint::persist_fast_path_success( + &fast_path, + &FastPathPersistInputs { + metadata_hash: &metadata_hash, + size_info: build_result.size_info.clone(), + watch_set_cache: params.watch_set_cache.as_deref(), + compiler_cache: compiler_cache.as_deref(), }, - size_info: build_result.size_info.clone(), - }; - if let Err(e) = save_json(&fingerprint_path, &persisted_fingerprint) { - tracing::warn!("failed to write build fingerprint: {}", e); - } - if let Some(ref zcc) = compiler_cache { - for watch in &fingerprint_watches { - if let Err(e) = crate::zccache::mark_fingerprint_success(zcc, watch) { - tracing::warn!( - "failed to mark zccache fingerprint success for {}: {}", - watch.root.display(), - e - ); - } - } - } + ); } Ok(build_result) diff --git a/crates/fbuild-build/src/build_fingerprint/README.md b/crates/fbuild-build/src/build_fingerprint/README.md index 40cb498d..14c49419 100644 --- a/crates/fbuild-build/src/build_fingerprint/README.md +++ b/crates/fbuild-build/src/build_fingerprint/README.md @@ -11,9 +11,9 @@ nothing relevant has changed. primitives (`hash_watch_set`, `hash_watch_set_stamps`, `hash_watch_set_stamps_cached`), and the `WatchSetStampCache` trait the daemon implements for cross-invocation memoisation. -- **`fast_path.rs`** -- Shared fast-path check extracted from the - ESP32 orchestrator. Takes a `FastPathInputs` (metadata hash, - watches, required artifacts, optional zccache + stamp-cache) and - returns `Some(FastPathHit)` when the caller can skip the pipeline - entirely. Used by ESP32 + AVR today; Teensy / RP2040 / STM32 will - follow. +- **`fast_path.rs`** -- Shared warm-build cache contract. Orchestrators + declare required outputs through `FastPathContract`, then use + `fast_path_check` for reuse decisions and + `persist_fast_path_success` to write `build_fingerprint.json` and + mark zccache watch roots. Used by ESP32, AVR, Teensy, RP2040, SAM, + NRF52, and Renesas. diff --git a/crates/fbuild-build/src/build_fingerprint/fast_path.rs b/crates/fbuild-build/src/build_fingerprint/fast_path.rs index be1a3064..0d40b323 100644 --- a/crates/fbuild-build/src/build_fingerprint/fast_path.rs +++ b/crates/fbuild-build/src/build_fingerprint/fast_path.rs @@ -28,9 +28,10 @@ use std::path::{Path, PathBuf}; use fbuild_core::{Result, SizeInfo}; use super::{ - hash_watch_set_stamps_cached, load_json, PersistedBuildFingerprint, WatchSetStampCache, - BUILD_FINGERPRINT_VERSION, + hash_watch_set_stamps_cached, load_json, save_json, PersistedBuildFingerprint, + WatchSetStampCache, BUILD_FINGERPRINT_VERSION, }; +use crate::compile_database::CompileDatabase; use crate::zccache::{self, FingerprintWatch}; /// File extensions considered source inputs for the watch-set fingerprint. @@ -65,18 +66,8 @@ pub const FAST_PATH_EXCLUDES: &[&str] = &[ /// Build a default [`FingerprintWatch`] for a directory using the /// shared fast-path extension / exclude lists. /// -/// Returns `None` if `root` does not exist, which lets callers skip -/// optional paths (e.g. a resolved-library tree that hasn't been -/// populated yet) without filtering in a second pass. -pub fn fast_path_watch( - cache_name: &str, - build_dir: &Path, - root: &Path, -) -> Option { - if !root.exists() { - return None; - } - Some(FingerprintWatch { +pub fn fast_path_watch(cache_name: &str, build_dir: &Path, root: &Path) -> FingerprintWatch { + FingerprintWatch { cache_file: build_dir.join(format!(".{}.zccache_fp.json", cache_name)), root: root.to_path_buf(), extensions: FAST_PATH_EXTENSIONS @@ -87,25 +78,115 @@ pub fn fast_path_watch( .iter() .map(|exclude| (*exclude).to_string()) .collect(), - }) + } +} + +/// Build the standard warm-build artifact set for a project. +/// +/// Returns: +/// - `firmware.elf` +/// - platform-specific output artifacts under `build_dir` +/// - the effective compile database path for `project_dir` +pub fn expected_fast_path_artifacts( + build_dir: &Path, + project_dir: &Path, + output_names: [&str; N], +) -> (PathBuf, [PathBuf; N], PathBuf) { + ( + build_dir.join("firmware.elf"), + output_names.map(|name| build_dir.join(name)), + CompileDatabase::expected_output_path(build_dir, project_dir), + ) +} + +/// Shared declaration of the artifacts and watched roots that make up +/// a warm-build cache hit for one orchestrator invocation. +/// +/// Orchestrators own the platform-specific metadata hash and named +/// output paths, but the shared layer owns: +/// - the persisted fingerprint path convention +/// - the default watch roots (`project` + resolved `libs`) +/// - the list of artifacts that must exist on a fast-path hit +#[derive(Debug, Clone)] +pub struct FastPathContract { + build_dir: PathBuf, + fingerprint_path: PathBuf, + watches: Vec, + required_artifacts: Vec, +} + +impl FastPathContract { + /// Create an empty contract rooted at `build_dir`. + pub fn new(build_dir: &Path) -> Self { + Self { + build_dir: build_dir.to_path_buf(), + fingerprint_path: build_dir.join("build_fingerprint.json"), + watches: Vec::new(), + required_artifacts: Vec::new(), + } + } + + /// Standard project contract: watch the project tree plus + /// resolved libraries, and require the provided outputs. + pub fn for_project_outputs( + build_dir: &Path, + project_dir: &Path, + required_artifacts: I, + ) -> Self + where + I: IntoIterator, + { + let mut contract = Self::new(build_dir); + contract.add_default_watches(project_dir); + contract.add_required_artifacts(required_artifacts); + contract + } + + /// Add a watched root. Missing roots are still recorded so the + /// fingerprint encodes that absence until the directory appears. + pub fn add_watch_root(&mut self, cache_name: &str, root: &Path) -> &mut Self { + self.watches + .push(fast_path_watch(cache_name, &self.build_dir, root)); + self + } + + /// Add the shared project + resolved-library watch roots. + pub fn add_default_watches(&mut self, project_dir: &Path) -> &mut Self { + self.add_watch_root("project", project_dir); + self.add_watch_root("dep_libs", &self.build_dir.join("libs")); + self + } + + /// Declare outputs that must exist for a warm-build reuse hit. + pub fn add_required_artifacts(&mut self, artifacts: I) -> &mut Self + where + I: IntoIterator, + { + self.required_artifacts.extend(artifacts); + self + } + + pub fn fingerprint_path(&self) -> &Path { + &self.fingerprint_path + } + + pub fn watches(&self) -> &[FingerprintWatch] { + &self.watches + } + + pub fn required_artifacts(&self) -> &[PathBuf] { + &self.required_artifacts + } } /// Inputs to [`fast_path_check`]. /// -/// Bundled in a struct so callers don't accumulate 8-argument calls -/// and so field additions stay source-compatible. All lifetimes tie -/// to the orchestrator's own call frame. -pub struct FastPathInputs<'a> { - /// Location of the persisted `build_fingerprint.json`. - pub fingerprint_path: &'a Path, +/// Bundled in a struct so callers don't accumulate several arguments +/// that vary per invocation while the contract stays fixed. +pub struct FastPathCheckInputs<'a> { /// Current build's metadata hash (board, profile, flash params, …). /// A mismatch against the persisted value forces a full rebuild. pub metadata_hash: &'a str, - /// Directories walked to form the watch-set fingerprint. - pub watches: &'a [FingerprintWatch], - /// Files that MUST exist on disk for a cache hit to be valid - /// (ELF, firmware binary, compile_commands.json, …). - pub required_artifacts: &'a [PathBuf], /// Optional extra "is current?" callback run after the artifact /// existence check. ESP32 uses this to require that /// `compile_commands.json` also has the PlatformIO-style project-root @@ -123,6 +204,23 @@ pub struct FastPathInputs<'a> { pub compiler_cache: Option<&'a Path>, } +/// Back-compat alias for earlier call sites/tests that imported the +/// original name before the shared contract existed. +pub type FastPathInputs<'a> = FastPathCheckInputs<'a>; + +/// Inputs to [`persist_fast_path_success`]. +pub struct FastPathPersistInputs<'a> { + /// Current build's metadata hash (board, profile, flash params, …). + pub metadata_hash: &'a str, + /// Size info to persist alongside the fingerprint for fast-path reuse. + pub size_info: Option, + /// Optional daemon-scoped memo for the watch-set traversal. + pub watch_set_cache: Option<&'a dyn WatchSetStampCache>, + /// Discovered zccache binary, if any. When present each watch root is + /// marked successful after the fingerprint is persisted. + pub compiler_cache: Option<&'a Path>, +} + /// Payload returned by a successful [`fast_path_check`]. /// /// Gives the caller what it needs to assemble a `BuildResult` without @@ -152,11 +250,14 @@ pub struct FastPathHit { /// /// The helper itself logs parse/hash warnings via `tracing::warn!` /// but never panics. -pub fn fast_path_check(inputs: &FastPathInputs<'_>) -> Result> { +pub fn fast_path_check( + contract: &FastPathContract, + inputs: &FastPathCheckInputs<'_>, +) -> Result> { // Load the persisted fingerprint. A parse error falls back to a // full build (matches the pre-extraction ESP32 behaviour). let persisted: Option = - match load_json::(inputs.fingerprint_path) { + match load_json::(contract.fingerprint_path()) { Ok(value) => value, Err(e) => { tracing::warn!("ignoring invalid build fingerprint: {}", e); @@ -176,7 +277,7 @@ pub fn fast_path_check(inputs: &FastPathInputs<'_>) -> Result) -> Result) -> Result) { + let persisted_fingerprint = PersistedBuildFingerprint { + version: BUILD_FINGERPRINT_VERSION, + metadata_hash: inputs.metadata_hash.to_string(), + file_set_hash: match hash_watch_set_stamps_cached( + contract.watches(), + inputs.watch_set_cache, + ) { + Ok(hash) => Some(hash), + Err(e) => { + tracing::warn!("failed to hash watched inputs for fingerprint save: {}", e); + None + } + }, + size_info: inputs.size_info.clone(), + }; + if let Err(e) = save_json(contract.fingerprint_path(), &persisted_fingerprint) { + tracing::warn!("failed to write build fingerprint: {}", e); + } + if let Some(zcc) = inputs.compiler_cache { + for watch in contract.watches() { + if let Err(e) = crate::zccache::mark_fingerprint_success(zcc, watch) { + tracing::warn!( + "failed to mark zccache fingerprint success for {}: {}", + watch.root.display(), + e + ); + } + } + } +} + /// zccache-powered fingerprint check with graceful fallback. fn check_with_zccache( zcc: &Path, @@ -301,10 +436,9 @@ mod tests { struct Fixture { _tmp: tempfile::TempDir, - fingerprint_path: PathBuf, required_artifact: PathBuf, src_root: PathBuf, - watch: FingerprintWatch, + contract: FastPathContract, } impl Fixture { @@ -317,32 +451,31 @@ mod tests { let main = src_root.join("main.cpp"); fs::write(&main, "int main() { return 0; }\n").unwrap(); - let fingerprint_path = build_dir.join("build_fingerprint.json"); let required_artifact = build_dir.join("firmware.elf"); fs::write(&required_artifact, b"elf-bytes").unwrap(); - let watch = super::fast_path_watch("project", &build_dir, &src_root) - .expect("watch present — src_root exists"); + let mut contract = FastPathContract::new(&build_dir); + contract.add_watch_root("project", &src_root); + contract.add_required_artifacts([required_artifact.clone()]); Self { _tmp: tmp, - fingerprint_path, required_artifact, src_root, - watch, + contract, } } fn write_fingerprint(&self, metadata_hash: &str) -> String { let file_set_hash = - super::super::hash_watch_set_stamps(std::slice::from_ref(&self.watch)).unwrap(); + super::super::hash_watch_set_stamps(self.contract.watches()).unwrap(); let fp = PersistedBuildFingerprint { version: BUILD_FINGERPRINT_VERSION, metadata_hash: metadata_hash.to_string(), file_set_hash: Some(file_set_hash.clone()), size_info: None, }; - super::super::save_json(&self.fingerprint_path, &fp).unwrap(); + super::super::save_json(self.contract.fingerprint_path(), &fp).unwrap(); file_set_hash } } @@ -353,26 +486,22 @@ mod tests { fx.write_fingerprint("meta-abc"); let cache: Arc = Arc::new(RecordingCache::default()); - let required = vec![fx.required_artifact.clone()]; - let watches = vec![fx.watch.clone()]; - let inputs = FastPathInputs { - fingerprint_path: &fx.fingerprint_path, + let inputs = FastPathCheckInputs { metadata_hash: "meta-abc", - watches: &watches, - required_artifacts: &required, extra_artifact_ok: None, watch_set_cache: Some(cache.as_ref()), compiler_cache: None, }; - let hit = fast_path_check(&inputs).expect("check must not error on happy path"); + let hit = + fast_path_check(&fx.contract, &inputs).expect("check must not error on happy path"); assert!(hit.is_some(), "expected fast-path hit"); // Second call should populate the memoised watch-set cache so // a third call skips the walk entirely (can't observe the // skip directly without instrumentation, but the cache must // hold an entry). - let _ = fast_path_check(&inputs).unwrap(); + let _ = fast_path_check(&fx.contract, &inputs).unwrap(); let recorded = cache.entries.lock().unwrap().len(); assert_eq!(recorded, 1, "watch-set cache should record one entry"); } @@ -381,18 +510,13 @@ mod tests { fn fast_path_misses_when_fingerprint_absent() { let fx = Fixture::new(); // Deliberately do NOT write the fingerprint file. - let required = vec![fx.required_artifact.clone()]; - let watches = vec![fx.watch.clone()]; - let inputs = FastPathInputs { - fingerprint_path: &fx.fingerprint_path, + let inputs = FastPathCheckInputs { metadata_hash: "meta-abc", - watches: &watches, - required_artifacts: &required, extra_artifact_ok: None, watch_set_cache: None, compiler_cache: None, }; - let hit = fast_path_check(&inputs).unwrap(); + let hit = fast_path_check(&fx.contract, &inputs).unwrap(); assert!(hit.is_none(), "missing fingerprint must force a full build"); } @@ -409,18 +533,13 @@ mod tests { ) .unwrap(); - let required = vec![fx.required_artifact.clone()]; - let watches = vec![fx.watch.clone()]; - let inputs = FastPathInputs { - fingerprint_path: &fx.fingerprint_path, + let inputs = FastPathCheckInputs { metadata_hash: "meta-abc", - watches: &watches, - required_artifacts: &required, extra_artifact_ok: None, watch_set_cache: None, compiler_cache: None, }; - let hit = fast_path_check(&inputs).unwrap(); + let hit = fast_path_check(&fx.contract, &inputs).unwrap(); assert!( hit.is_none(), "changed source file must invalidate fast path" @@ -432,18 +551,13 @@ mod tests { let fx = Fixture::new(); fx.write_fingerprint("meta-abc"); - let required = vec![fx.required_artifact.clone()]; - let watches = vec![fx.watch.clone()]; - let inputs = FastPathInputs { - fingerprint_path: &fx.fingerprint_path, + let inputs = FastPathCheckInputs { metadata_hash: "meta-xyz", - watches: &watches, - required_artifacts: &required, extra_artifact_ok: None, watch_set_cache: None, compiler_cache: None, }; - let hit = fast_path_check(&inputs).unwrap(); + let hit = fast_path_check(&fx.contract, &inputs).unwrap(); assert!(hit.is_none(), "metadata hash mismatch must invalidate"); } @@ -453,18 +567,13 @@ mod tests { fx.write_fingerprint("meta-abc"); fs::remove_file(&fx.required_artifact).unwrap(); - let required = vec![fx.required_artifact.clone()]; - let watches = vec![fx.watch.clone()]; - let inputs = FastPathInputs { - fingerprint_path: &fx.fingerprint_path, + let inputs = FastPathCheckInputs { metadata_hash: "meta-abc", - watches: &watches, - required_artifacts: &required, extra_artifact_ok: None, watch_set_cache: None, compiler_cache: None, }; - let hit = fast_path_check(&inputs).unwrap(); + let hit = fast_path_check(&fx.contract, &inputs).unwrap(); assert!(hit.is_none(), "missing artifact must invalidate"); } @@ -473,22 +582,93 @@ mod tests { let fx = Fixture::new(); fx.write_fingerprint("meta-abc"); - let required = vec![fx.required_artifact.clone()]; - let watches = vec![fx.watch.clone()]; let always_stale = || false; - let inputs = FastPathInputs { - fingerprint_path: &fx.fingerprint_path, + let inputs = FastPathCheckInputs { metadata_hash: "meta-abc", - watches: &watches, - required_artifacts: &required, extra_artifact_ok: Some(&always_stale), watch_set_cache: None, compiler_cache: None, }; - let hit = fast_path_check(&inputs).unwrap(); + let hit = fast_path_check(&fx.contract, &inputs).unwrap(); assert!( hit.is_none(), "extra_artifact_ok returning false must invalidate" ); } + + #[test] + fn contract_uses_shared_path_and_default_watches() { + let tmp = tempfile::TempDir::new().unwrap(); + let build_dir = tmp.path().join("build"); + let project_dir = tmp.path().join("project"); + fs::create_dir_all(&build_dir).unwrap(); + fs::create_dir_all(&project_dir).unwrap(); + let artifact = build_dir.join("firmware.elf"); + + let contract = + FastPathContract::for_project_outputs(&build_dir, &project_dir, [artifact.clone()]); + + assert_eq!( + contract.fingerprint_path(), + build_dir.join("build_fingerprint.json") + ); + assert_eq!(contract.required_artifacts(), &[artifact]); + assert_eq!(contract.watches().len(), 2); + assert_eq!(contract.watches()[0].root, project_dir); + assert_eq!(contract.watches()[1].root, build_dir.join("libs")); + } + + #[test] + fn expected_fast_path_artifacts_follow_compile_db_location() { + let tmp = tempfile::TempDir::new().unwrap(); + let build_dir = tmp.path().join("build"); + let app_project = tmp.path().join("app"); + let lib_project = tmp.path().join("libproj"); + fs::create_dir_all(&build_dir).unwrap(); + fs::create_dir_all(&app_project).unwrap(); + fs::create_dir_all(&lib_project).unwrap(); + fs::write(lib_project.join("library.json"), r#"{"name":"libproj"}"#).unwrap(); + + let (app_elf, [app_hex], app_compile_db) = + expected_fast_path_artifacts(&build_dir, &app_project, ["firmware.hex"]); + let (lib_elf, [lib_hex], lib_compile_db) = + expected_fast_path_artifacts(&build_dir, &lib_project, ["firmware.hex"]); + + assert_eq!(app_elf, build_dir.join("firmware.elf")); + assert_eq!(app_hex, build_dir.join("firmware.hex")); + assert_eq!(lib_elf, build_dir.join("firmware.elf")); + assert_eq!(lib_hex, build_dir.join("firmware.hex")); + assert_eq!(app_compile_db, app_project.join("compile_commands.json")); + assert_eq!(lib_compile_db, build_dir.join("compile_commands.json")); + } + + #[test] + fn persist_success_writes_fingerprint_for_contract() { + let fx = Fixture::new(); + + persist_fast_path_success( + &fx.contract, + &FastPathPersistInputs { + metadata_hash: "meta-abc", + size_info: None, + watch_set_cache: None, + compiler_cache: None, + }, + ); + + let hit = fast_path_check( + &fx.contract, + &FastPathCheckInputs { + metadata_hash: "meta-abc", + extra_artifact_ok: None, + watch_set_cache: None, + compiler_cache: None, + }, + ) + .unwrap(); + assert!( + hit.is_some(), + "persisted contract should produce a fast-path hit" + ); + } } diff --git a/crates/fbuild-build/src/build_fingerprint/mod.rs b/crates/fbuild-build/src/build_fingerprint/mod.rs index ffc46296..c6077a6e 100644 --- a/crates/fbuild-build/src/build_fingerprint/mod.rs +++ b/crates/fbuild-build/src/build_fingerprint/mod.rs @@ -2,7 +2,10 @@ pub mod fast_path; -pub use fast_path::{fast_path_check, fast_path_watch, FastPathHit, FastPathInputs}; +pub use fast_path::{ + expected_fast_path_artifacts, fast_path_check, fast_path_watch, persist_fast_path_success, + FastPathCheckInputs, FastPathContract, FastPathHit, FastPathInputs, FastPathPersistInputs, +}; use std::collections::HashMap; use std::path::{Path, PathBuf}; diff --git a/crates/fbuild-build/src/esp32/orchestrator.rs b/crates/fbuild-build/src/esp32/orchestrator.rs index 863ab803..2b092419 100644 --- a/crates/fbuild-build/src/esp32/orchestrator.rs +++ b/crates/fbuild-build/src/esp32/orchestrator.rs @@ -26,12 +26,11 @@ use fbuild_packages::Framework; use serde::Serialize; use crate::build_fingerprint::{ - hash_watch_set_stamps_cached, save_json, stable_hash_json, PersistedBuildFingerprint, - BUILD_FINGERPRINT_VERSION, + expected_fast_path_artifacts, stable_hash_json, FastPathCheckInputs, FastPathContract, + FastPathPersistInputs, BUILD_FINGERPRINT_VERSION, }; use crate::flag_overlay::LanguageExtraFlags; use crate::linker::LinkerScripts; -use crate::zccache::FingerprintWatch; use crate::compiler::Compiler as _; use crate::{BuildOrchestrator, BuildParams, BuildResult, SourceScanner}; @@ -130,10 +129,6 @@ fn record_failed_framework_lib(marker_path: &Path, signature: &str, error: &str) let _ = std::fs::write(marker_path, format!("{signature}\n{error}\n")); } -fn build_fingerprint_path(build_dir: &Path) -> PathBuf { - build_dir.join("build_fingerprint.json") -} - fn profile_label(profile: fbuild_core::BuildProfile) -> &'static str { match profile { fbuild_core::BuildProfile::Release => "release", @@ -141,22 +136,6 @@ fn profile_label(profile: fbuild_core::BuildProfile) -> &'static str { } } -fn collect_fast_path_watches(build_dir: &Path, project_dir: &Path) -> Vec { - let mut watches = Vec::new(); - if let Some(watch) = - crate::build_fingerprint::fast_path_watch("project", build_dir, project_dir) - { - watches.push(watch); - } - let resolved_libs_dir = build_dir.join("libs"); - if let Some(watch) = - crate::build_fingerprint::fast_path_watch("dep_libs", build_dir, &resolved_libs_dir) - { - watches.push(watch); - } - watches -} - fn compile_db_is_current(build_dir: &Path, project_dir: &Path) -> bool { let build_copy = build_dir.join("compile_commands.json"); if !build_copy.exists() { @@ -165,19 +144,6 @@ fn compile_db_is_current(build_dir: &Path, project_dir: &Path) -> bool { crate::compile_database::CompileDatabase::expected_output_path(build_dir, project_dir).exists() } -fn expected_fast_path_artifacts( - build_dir: &Path, - project_dir: &Path, -) -> (PathBuf, PathBuf, PathBuf, PathBuf, PathBuf) { - ( - build_dir.join("firmware.elf"), - build_dir.join("firmware.bin"), - build_dir.join("bootloader.bin"), - build_dir.join("partitions.bin"), - crate::compile_database::CompileDatabase::expected_output_path(build_dir, project_dir), - ) -} - impl BuildOrchestrator for Esp32Orchestrator { fn platform(&self) -> Platform { Platform::Espressif32 @@ -278,10 +244,31 @@ impl BuildOrchestrator for Esp32Orchestrator { max_flash: ctx.board.max_flash, max_ram: ctx.board.max_ram, })?; - let fingerprint_path = build_fingerprint_path(build_dir); - let fingerprint_watches = { + let (fast_elf, [fast_bin, fast_boot, fast_parts, fast_app0], fast_compile_db) = + expected_fast_path_artifacts( + build_dir, + ¶ms.project_dir, + [ + "firmware.bin", + "bootloader.bin", + "partitions.bin", + "boot_app0.bin", + ], + ); + let fast_path = { let _g = perf.phase("fp-watches-collect"); - collect_fast_path_watches(build_dir, ¶ms.project_dir) + FastPathContract::for_project_outputs( + build_dir, + ¶ms.project_dir, + [ + fast_elf.clone(), + fast_bin.clone(), + fast_boot.clone(), + fast_parts.clone(), + fast_app0.clone(), + fast_compile_db.clone(), + ], + ) }; if !params.compiledb_only @@ -289,29 +276,17 @@ impl BuildOrchestrator for Esp32Orchestrator { && params.symbol_analysis_path.is_none() { let _fast_path_phase = perf.phase("fast-path-check"); - let (fast_elf, fast_bin, fast_boot, fast_parts, fast_compile_db) = - expected_fast_path_artifacts(build_dir, ¶ms.project_dir); - let required_artifacts = [ - fast_elf.clone(), - fast_bin.clone(), - fast_boot.clone(), - fast_parts.clone(), - fast_compile_db.clone(), - ]; // ESP32 also requires the project-root copy of compile_commands.json // to be in sync with the build-dir copy. That's platform-specific, // so it rides on the shared helper via `extra_artifact_ok`. let compile_db_fresh = || compile_db_is_current(build_dir, ¶ms.project_dir); - let inputs = crate::build_fingerprint::FastPathInputs { - fingerprint_path: &fingerprint_path, + let inputs = FastPathCheckInputs { metadata_hash: &metadata_hash, - watches: &fingerprint_watches, - required_artifacts: &required_artifacts, extra_artifact_ok: Some(&compile_db_fresh), watch_set_cache: params.watch_set_cache.as_deref(), compiler_cache: compiler_cache.as_deref(), }; - if let Some(hit) = crate::build_fingerprint::fast_path_check(&inputs)? { + if let Some(hit) = crate::build_fingerprint::fast_path_check(&fast_path, &inputs)? { ctx.build_log.push( "No-op fingerprint matched; reusing existing ESP32 artifacts.".to_string(), ); @@ -1275,34 +1250,25 @@ impl BuildOrchestrator for Esp32Orchestrator { // 15. Size reporting + result assembly let fingerprint_started = Instant::now(); perf.checkpoint("fingerprint-save-start"); - let persisted_fingerprint = PersistedBuildFingerprint { - version: BUILD_FINGERPRINT_VERSION, - metadata_hash: metadata_hash.clone(), - file_set_hash: match hash_watch_set_stamps_cached( - &fingerprint_watches, - params.watch_set_cache.as_deref(), - ) { - Ok(hash) => Some(hash), - Err(e) => { - tracing::warn!("failed to hash watched inputs for fingerprint save: {}", e); - None - } - }, - size_info: link_result.size_info.clone(), - }; - if let Err(e) = save_json(&fingerprint_path, &persisted_fingerprint) { - tracing::warn!("failed to write build fingerprint: {}", e); - } - if let Some(ref zcc) = compiler_cache { - for watch in &fingerprint_watches { - if let Err(e) = crate::zccache::mark_fingerprint_success(zcc, watch) { - tracing::warn!( - "failed to mark zccache fingerprint success for {}: {}", - watch.root.display(), - e - ); - } - } + let fast_path_ready = fast_path + .required_artifacts() + .iter() + .all(|path| path.exists()) + && compile_db_is_current(build_dir, ¶ms.project_dir); + if fast_path_ready { + crate::build_fingerprint::persist_fast_path_success( + &fast_path, + &FastPathPersistInputs { + metadata_hash: &metadata_hash, + size_info: link_result.size_info.clone(), + watch_set_cache: params.watch_set_cache.as_deref(), + compiler_cache: compiler_cache.as_deref(), + }, + ); + } else { + tracing::warn!( + "skipping ESP32 fast-path persistence because final artifacts are incomplete" + ); } perf.record("fingerprint-save", fingerprint_started.elapsed()); perf.checkpoint("fingerprint-save-finish"); diff --git a/crates/fbuild-build/src/nrf52/orchestrator.rs b/crates/fbuild-build/src/nrf52/orchestrator.rs index 35bcc631..b206684c 100644 --- a/crates/fbuild-build/src/nrf52/orchestrator.rs +++ b/crates/fbuild-build/src/nrf52/orchestrator.rs @@ -12,19 +12,18 @@ //! 9. Link (with linker script) //! 10. Convert to hex + report size -use std::path::{Path, PathBuf}; +use std::path::Path; use std::time::Instant; use fbuild_core::{Platform, Result}; use serde::Serialize; use crate::build_fingerprint::{ - hash_watch_set_stamps_cached, save_json, stable_hash_json, FastPathInputs, - PersistedBuildFingerprint, BUILD_FINGERPRINT_VERSION, + expected_fast_path_artifacts, stable_hash_json, FastPathCheckInputs, FastPathContract, + FastPathPersistInputs, BUILD_FINGERPRINT_VERSION, }; -use crate::compile_database::{CompileDatabase, TargetArchitecture}; +use crate::compile_database::TargetArchitecture; use crate::pipeline; -use crate::zccache::FingerprintWatch; use crate::{BuildOrchestrator, BuildParams, BuildResult, SourceScanner}; use super::nrf52_compiler::Nrf52Compiler; @@ -58,37 +57,6 @@ fn profile_label(profile: fbuild_core::BuildProfile) -> &'static str { } } -fn build_fingerprint_path(build_dir: &Path) -> PathBuf { - build_dir.join("build_fingerprint.json") -} - -fn collect_fast_path_watches(build_dir: &Path, project_dir: &Path) -> Vec { - let mut watches = Vec::new(); - if let Some(watch) = - crate::build_fingerprint::fast_path_watch("project", build_dir, project_dir) - { - watches.push(watch); - } - let resolved_libs_dir = build_dir.join("libs"); - if let Some(watch) = - crate::build_fingerprint::fast_path_watch("dep_libs", build_dir, &resolved_libs_dir) - { - watches.push(watch); - } - watches -} - -fn expected_fast_path_artifacts( - build_dir: &Path, - project_dir: &Path, -) -> (PathBuf, PathBuf, PathBuf) { - ( - build_dir.join("firmware.elf"), - build_dir.join("firmware.hex"), - CompileDatabase::expected_output_path(build_dir, project_dir), - ) -} - impl BuildOrchestrator for Nrf52Orchestrator { fn platform(&self) -> Platform { Platform::NordicNrf52 @@ -124,7 +92,6 @@ impl BuildOrchestrator for Nrf52Orchestrator { .ldscript .as_deref() .unwrap_or("nrf52840_s140_v6.ld"); - let fingerprint_path = build_fingerprint_path(build_dir); let metadata_hash = stable_hash_json(&Nrf52FingerprintMetadata { version: BUILD_FINGERPRINT_VERSION, env_name: params.env_name.clone(), @@ -141,25 +108,25 @@ impl BuildOrchestrator for Nrf52Orchestrator { max_flash: ctx.board.max_flash, max_ram: ctx.board.max_ram, })?; - let fingerprint_watches = collect_fast_path_watches(build_dir, ¶ms.project_dir); + let (fast_elf, [fast_hex], fast_compile_db) = + expected_fast_path_artifacts(build_dir, ¶ms.project_dir, ["firmware.hex"]); + let fast_path = FastPathContract::for_project_outputs( + build_dir, + ¶ms.project_dir, + [fast_elf.clone(), fast_hex.clone(), fast_compile_db.clone()], + ); if !params.compiledb_only && !params.symbol_analysis && params.symbol_analysis_path.is_none() { - let (fast_elf, fast_hex, fast_compile_db) = - expected_fast_path_artifacts(build_dir, ¶ms.project_dir); - let required_artifacts = [fast_elf.clone(), fast_hex.clone(), fast_compile_db.clone()]; - let inputs = FastPathInputs { - fingerprint_path: &fingerprint_path, + let inputs = FastPathCheckInputs { metadata_hash: &metadata_hash, - watches: &fingerprint_watches, - required_artifacts: &required_artifacts, extra_artifact_ok: None, watch_set_cache: params.watch_set_cache.as_deref(), compiler_cache: compiler_cache.as_deref(), }; - if let Some(hit) = crate::build_fingerprint::fast_path_check(&inputs)? { + if let Some(hit) = crate::build_fingerprint::fast_path_check(&fast_path, &inputs)? { ctx.build_log.push( "No-op fingerprint matched; reusing existing NRF52 artifacts.".to_string(), ); @@ -371,35 +338,15 @@ impl BuildOrchestrator for Nrf52Orchestrator { && !params.symbol_analysis && params.symbol_analysis_path.is_none() { - let persisted_fingerprint = PersistedBuildFingerprint { - version: BUILD_FINGERPRINT_VERSION, - metadata_hash: metadata_hash.clone(), - file_set_hash: match hash_watch_set_stamps_cached( - &fingerprint_watches, - params.watch_set_cache.as_deref(), - ) { - Ok(hash) => Some(hash), - Err(e) => { - tracing::warn!("failed to hash watched inputs for fingerprint save: {}", e); - None - } + crate::build_fingerprint::persist_fast_path_success( + &fast_path, + &FastPathPersistInputs { + metadata_hash: &metadata_hash, + size_info: build_result.size_info.clone(), + watch_set_cache: params.watch_set_cache.as_deref(), + compiler_cache: compiler_cache.as_deref(), }, - size_info: build_result.size_info.clone(), - }; - if let Err(e) = save_json(&fingerprint_path, &persisted_fingerprint) { - tracing::warn!("failed to write build fingerprint: {}", e); - } - if let Some(ref zcc) = compiler_cache { - for watch in &fingerprint_watches { - if let Err(e) = crate::zccache::mark_fingerprint_success(zcc, watch) { - tracing::warn!( - "failed to mark zccache fingerprint success for {}: {}", - watch.root.display(), - e - ); - } - } - } + ); } Ok(build_result) @@ -427,7 +374,7 @@ mod tests { } #[test] - fn test_collect_fast_path_watches_includes_project_and_resolved_libs() { + fn test_fast_path_contract_includes_project_and_resolved_libs() { let tmp = tempfile::TempDir::new().unwrap(); let build_dir = tmp.path().join("build"); let project_dir = tmp.path().join("project"); @@ -435,29 +382,14 @@ mod tests { std::fs::create_dir_all(&libs_dir).unwrap(); std::fs::create_dir_all(&project_dir).unwrap(); - let watches = collect_fast_path_watches(&build_dir, &project_dir); - - assert_eq!(watches.len(), 2); - assert_eq!(watches[0].root, project_dir); - assert_eq!(watches[1].root, libs_dir); - } - - #[test] - fn test_expected_fast_path_artifacts_include_compile_db() { - let tmp = tempfile::TempDir::new().unwrap(); - let build_dir = tmp.path().join("build"); - let project_dir = tmp.path().join("project"); - - let (elf, hex, compile_db) = expected_fast_path_artifacts(&build_dir, &project_dir); - - assert_eq!(elf, build_dir.join("firmware.elf")); - assert_eq!(hex, build_dir.join("firmware.hex")); - assert_eq!( - compile_db, - crate::compile_database::CompileDatabase::expected_output_path( - &build_dir, - &project_dir - ) + let contract = FastPathContract::for_project_outputs( + &build_dir, + &project_dir, + Vec::::new(), ); + + assert_eq!(contract.watches().len(), 2); + assert_eq!(contract.watches()[0].root, project_dir); + assert_eq!(contract.watches()[1].root, libs_dir); } } diff --git a/crates/fbuild-build/src/renesas/orchestrator.rs b/crates/fbuild-build/src/renesas/orchestrator.rs index f312151b..b88c0481 100644 --- a/crates/fbuild-build/src/renesas/orchestrator.rs +++ b/crates/fbuild-build/src/renesas/orchestrator.rs @@ -19,13 +19,11 @@ use fbuild_core::{Platform, Result}; use serde::Serialize; use crate::build_fingerprint::{ - hash_watch_set_stamps_cached, save_json, stable_hash_json, FastPathInputs, - PersistedBuildFingerprint, BUILD_FINGERPRINT_VERSION, + expected_fast_path_artifacts, stable_hash_json, FastPathCheckInputs, FastPathContract, + FastPathPersistInputs, BUILD_FINGERPRINT_VERSION, }; -use crate::compile_database::CompileDatabase; use crate::compile_database::TargetArchitecture; use crate::pipeline; -use crate::zccache::FingerprintWatch; use crate::{BuildOrchestrator, BuildParams, BuildResult, SourceScanner}; use super::renesas_compiler::RenesasCompiler; @@ -60,37 +58,6 @@ fn profile_label(profile: fbuild_core::BuildProfile) -> &'static str { } } -fn build_fingerprint_path(build_dir: &Path) -> PathBuf { - build_dir.join("build_fingerprint.json") -} - -fn collect_fast_path_watches(build_dir: &Path, project_dir: &Path) -> Vec { - let mut watches = Vec::new(); - if let Some(watch) = - crate::build_fingerprint::fast_path_watch("project", build_dir, project_dir) - { - watches.push(watch); - } - let resolved_libs_dir = build_dir.join("libs"); - if let Some(watch) = - crate::build_fingerprint::fast_path_watch("dep_libs", build_dir, &resolved_libs_dir) - { - watches.push(watch); - } - watches -} - -fn expected_fast_path_artifacts( - build_dir: &Path, - project_dir: &Path, -) -> (PathBuf, PathBuf, PathBuf) { - ( - build_dir.join("firmware.elf"), - build_dir.join("firmware.bin"), - CompileDatabase::expected_output_path(build_dir, project_dir), - ) -} - impl BuildOrchestrator for RenesasOrchestrator { fn platform(&self) -> Platform { Platform::RenesasRa @@ -124,7 +91,6 @@ impl BuildOrchestrator for RenesasOrchestrator { let core_dir = framework.get_core_dir(&ctx.board.core); let variant_dir = framework.get_variant_dir(&ctx.board.variant); let build_dir = &ctx.build_dir; - let fingerprint_path = build_fingerprint_path(build_dir); let metadata_hash = stable_hash_json(&RenesasFingerprintMetadata { version: BUILD_FINGERPRINT_VERSION, env_name: params.env_name.clone(), @@ -142,25 +108,25 @@ impl BuildOrchestrator for RenesasOrchestrator { max_flash: ctx.board.max_flash, max_ram: ctx.board.max_ram, })?; - let fingerprint_watches = collect_fast_path_watches(build_dir, ¶ms.project_dir); + let (fast_elf, [fast_bin], fast_compile_db) = + expected_fast_path_artifacts(build_dir, ¶ms.project_dir, ["firmware.bin"]); + let fast_path = FastPathContract::for_project_outputs( + build_dir, + ¶ms.project_dir, + [fast_elf.clone(), fast_bin.clone(), fast_compile_db.clone()], + ); if !params.compiledb_only && !params.symbol_analysis && params.symbol_analysis_path.is_none() { - let (fast_elf, fast_bin, fast_compile_db) = - expected_fast_path_artifacts(build_dir, ¶ms.project_dir); - let required_artifacts = [fast_elf.clone(), fast_bin.clone(), fast_compile_db.clone()]; - let inputs = FastPathInputs { - fingerprint_path: &fingerprint_path, + let inputs = FastPathCheckInputs { metadata_hash: &metadata_hash, - watches: &fingerprint_watches, - required_artifacts: &required_artifacts, extra_artifact_ok: None, watch_set_cache: params.watch_set_cache.as_deref(), compiler_cache: compiler_cache.as_deref(), }; - if let Some(hit) = crate::build_fingerprint::fast_path_check(&inputs)? { + if let Some(hit) = crate::build_fingerprint::fast_path_check(&fast_path, &inputs)? { ctx.build_log.push( "No-op fingerprint matched; reusing existing Renesas artifacts.".to_string(), ); @@ -290,35 +256,15 @@ impl BuildOrchestrator for RenesasOrchestrator { && !params.symbol_analysis && params.symbol_analysis_path.is_none() { - let persisted_fingerprint = PersistedBuildFingerprint { - version: BUILD_FINGERPRINT_VERSION, - metadata_hash: metadata_hash.clone(), - file_set_hash: match hash_watch_set_stamps_cached( - &fingerprint_watches, - params.watch_set_cache.as_deref(), - ) { - Ok(hash) => Some(hash), - Err(e) => { - tracing::warn!("failed to hash watched inputs for fingerprint save: {}", e); - None - } + crate::build_fingerprint::persist_fast_path_success( + &fast_path, + &FastPathPersistInputs { + metadata_hash: &metadata_hash, + size_info: build_result.size_info.clone(), + watch_set_cache: params.watch_set_cache.as_deref(), + compiler_cache: compiler_cache.as_deref(), }, - size_info: build_result.size_info.clone(), - }; - if let Err(e) = save_json(&fingerprint_path, &persisted_fingerprint) { - tracing::warn!("failed to write build fingerprint: {}", e); - } - if let Some(ref zcc) = compiler_cache { - for watch in &fingerprint_watches { - if let Err(e) = crate::zccache::mark_fingerprint_success(zcc, watch) { - tracing::warn!( - "failed to mark zccache fingerprint success for {}: {}", - watch.root.display(), - e - ); - } - } - } + ); } Ok(build_result) @@ -428,33 +374,17 @@ mod tests { } #[test] - fn test_collect_fast_path_watches_skips_missing_dep_libs() { + fn test_fast_path_contract_preserves_missing_dep_libs() { let tmp = tempfile::TempDir::new().unwrap(); let build_dir = tmp.path().join("build"); let project_dir = tmp.path().join("project"); std::fs::create_dir_all(&build_dir).unwrap(); std::fs::create_dir_all(&project_dir).unwrap(); - let watches = collect_fast_path_watches(&build_dir, &project_dir); - assert_eq!(watches.len(), 1); - assert_eq!(watches[0].root, project_dir); - } - - #[test] - fn test_expected_fast_path_artifacts_follow_compile_db_location() { - let tmp = tempfile::TempDir::new().unwrap(); - let build_dir = tmp.path().join("build"); - let app_project = tmp.path().join("app"); - let lib_project = tmp.path().join("libproj"); - std::fs::create_dir_all(&build_dir).unwrap(); - std::fs::create_dir_all(&app_project).unwrap(); - std::fs::create_dir_all(&lib_project).unwrap(); - std::fs::write(lib_project.join("library.json"), r#"{"name":"libproj"}"#).unwrap(); - - let (_, _, app_compile_db) = expected_fast_path_artifacts(&build_dir, &app_project); - let (_, _, lib_compile_db) = expected_fast_path_artifacts(&build_dir, &lib_project); - - assert_eq!(app_compile_db, app_project.join("compile_commands.json")); - assert_eq!(lib_compile_db, build_dir.join("compile_commands.json")); + let contract = + FastPathContract::for_project_outputs(&build_dir, &project_dir, Vec::::new()); + assert_eq!(contract.watches().len(), 2); + assert_eq!(contract.watches()[0].root, project_dir); + assert_eq!(contract.watches()[1].root, build_dir.join("libs")); } } diff --git a/crates/fbuild-build/src/rp2040/orchestrator.rs b/crates/fbuild-build/src/rp2040/orchestrator.rs index 2933987a..b58eca7e 100644 --- a/crates/fbuild-build/src/rp2040/orchestrator.rs +++ b/crates/fbuild-build/src/rp2040/orchestrator.rs @@ -20,14 +20,13 @@ use fbuild_core::{Platform, Result}; use serde::Serialize; use crate::build_fingerprint::{ - hash_watch_set_stamps_cached, save_json, stable_hash_json, FastPathInputs, - PersistedBuildFingerprint, BUILD_FINGERPRINT_VERSION, + expected_fast_path_artifacts, stable_hash_json, FastPathCheckInputs, FastPathContract, + FastPathPersistInputs, BUILD_FINGERPRINT_VERSION, }; -use crate::compile_database::{CompileDatabase, TargetArchitecture}; +use crate::compile_database::TargetArchitecture; use crate::compiler::Compiler as _; use crate::generic_arm::{ArmCompiler, ArmLinker}; use crate::pipeline; -use crate::zccache::FingerprintWatch; use crate::{BuildOrchestrator, BuildParams, BuildResult, SourceScanner}; /// RP2040 platform build orchestrator. @@ -58,37 +57,6 @@ fn profile_label(profile: fbuild_core::BuildProfile) -> &'static str { } } -fn build_fingerprint_path(build_dir: &Path) -> PathBuf { - build_dir.join("build_fingerprint.json") -} - -fn collect_fast_path_watches(build_dir: &Path, project_dir: &Path) -> Vec { - let mut watches = Vec::new(); - if let Some(watch) = - crate::build_fingerprint::fast_path_watch("project", build_dir, project_dir) - { - watches.push(watch); - } - let resolved_libs_dir = build_dir.join("libs"); - if let Some(watch) = - crate::build_fingerprint::fast_path_watch("dep_libs", build_dir, &resolved_libs_dir) - { - watches.push(watch); - } - watches -} - -fn expected_fast_path_artifacts( - build_dir: &Path, - project_dir: &Path, -) -> (PathBuf, PathBuf, PathBuf) { - ( - build_dir.join("firmware.elf"), - build_dir.join("firmware.bin"), - CompileDatabase::expected_output_path(build_dir, project_dir), - ) -} - impl BuildOrchestrator for Rp2040Orchestrator { fn platform(&self) -> Platform { Platform::RaspberryPi @@ -128,7 +96,6 @@ impl BuildOrchestrator for Rp2040Orchestrator { &board_id, ); let build_dir = ctx.build_dir.clone(); - let fingerprint_path = build_fingerprint_path(&build_dir); let metadata_hash = stable_hash_json(&Rp2040FingerprintMetadata { version: BUILD_FINGERPRINT_VERSION, env_name: params.env_name.clone(), @@ -145,25 +112,25 @@ impl BuildOrchestrator for Rp2040Orchestrator { max_flash: ctx.board.max_flash, max_ram: ctx.board.max_ram, })?; - let fingerprint_watches = collect_fast_path_watches(&build_dir, ¶ms.project_dir); + let (fast_elf, [fast_bin], fast_compile_db) = + expected_fast_path_artifacts(&build_dir, ¶ms.project_dir, ["firmware.bin"]); + let fast_path = FastPathContract::for_project_outputs( + &build_dir, + ¶ms.project_dir, + [fast_elf.clone(), fast_bin.clone(), fast_compile_db.clone()], + ); if !params.compiledb_only && !params.symbol_analysis && params.symbol_analysis_path.is_none() { - let (fast_elf, fast_bin, fast_compile_db) = - expected_fast_path_artifacts(&build_dir, ¶ms.project_dir); - let required_artifacts = [fast_elf.clone(), fast_bin.clone(), fast_compile_db.clone()]; - let inputs = FastPathInputs { - fingerprint_path: &fingerprint_path, + let inputs = FastPathCheckInputs { metadata_hash: &metadata_hash, - watches: &fingerprint_watches, - required_artifacts: &required_artifacts, extra_artifact_ok: None, watch_set_cache: params.watch_set_cache.as_deref(), compiler_cache: compiler_cache.as_deref(), }; - if let Some(hit) = crate::build_fingerprint::fast_path_check(&inputs)? { + if let Some(hit) = crate::build_fingerprint::fast_path_check(&fast_path, &inputs)? { ctx.build_log.push( "No-op fingerprint matched; reusing existing RP2040 artifacts.".to_string(), ); @@ -351,37 +318,15 @@ impl BuildOrchestrator for Rp2040Orchestrator { && !params.symbol_analysis && params.symbol_analysis_path.is_none() { - let persisted_fingerprint_watches = - collect_fast_path_watches(&build_dir, ¶ms.project_dir); - let persisted_fingerprint = PersistedBuildFingerprint { - version: BUILD_FINGERPRINT_VERSION, - metadata_hash: metadata_hash.clone(), - file_set_hash: match hash_watch_set_stamps_cached( - &persisted_fingerprint_watches, - params.watch_set_cache.as_deref(), - ) { - Ok(hash) => Some(hash), - Err(e) => { - tracing::warn!("failed to hash watched inputs for fingerprint save: {}", e); - None - } + crate::build_fingerprint::persist_fast_path_success( + &fast_path, + &FastPathPersistInputs { + metadata_hash: &metadata_hash, + size_info: build_result.size_info.clone(), + watch_set_cache: params.watch_set_cache.as_deref(), + compiler_cache: compiler_cache.as_deref(), }, - size_info: build_result.size_info.clone(), - }; - if let Err(e) = save_json(&fingerprint_path, &persisted_fingerprint) { - tracing::warn!("failed to write build fingerprint: {}", e); - } - if let Some(ref zcc) = compiler_cache { - for watch in &persisted_fingerprint_watches { - if let Err(e) = crate::zccache::mark_fingerprint_success(zcc, watch) { - tracing::warn!( - "failed to mark zccache fingerprint success for {}: {}", - watch.root.display(), - e - ); - } - } - } + ); } Ok(build_result) @@ -900,20 +845,22 @@ mod tests { } #[test] - fn test_collect_fast_path_watches_skips_missing_dep_libs() { + fn test_fast_path_contract_preserves_missing_dep_libs() { let tmp = tempfile::TempDir::new().unwrap(); let build_dir = tmp.path().join("build"); let project_dir = tmp.path().join("project"); std::fs::create_dir_all(&build_dir).unwrap(); std::fs::create_dir_all(&project_dir).unwrap(); - let watches = collect_fast_path_watches(&build_dir, &project_dir); - assert_eq!(watches.len(), 1); - assert_eq!(watches[0].root, project_dir); + let contract = + FastPathContract::for_project_outputs(&build_dir, &project_dir, Vec::::new()); + assert_eq!(contract.watches().len(), 2); + assert_eq!(contract.watches()[0].root, project_dir); + assert_eq!(contract.watches()[1].root, build_dir.join("libs")); } #[test] - fn test_collect_fast_path_watches_includes_dep_libs_when_present() { + fn test_fast_path_contract_includes_dep_libs_when_present() { let tmp = tempfile::TempDir::new().unwrap(); let build_dir = tmp.path().join("build"); let project_dir = tmp.path().join("project"); @@ -921,27 +868,10 @@ mod tests { std::fs::create_dir_all(&dep_libs_dir).unwrap(); std::fs::create_dir_all(&project_dir).unwrap(); - let watches = collect_fast_path_watches(&build_dir, &project_dir); - assert_eq!(watches.len(), 2); - assert_eq!(watches[0].root, project_dir); - assert_eq!(watches[1].root, dep_libs_dir); - } - - #[test] - fn test_expected_fast_path_artifacts_follow_compile_db_location() { - let tmp = tempfile::TempDir::new().unwrap(); - let build_dir = tmp.path().join("build"); - let app_project = tmp.path().join("app"); - let lib_project = tmp.path().join("libproj"); - std::fs::create_dir_all(&build_dir).unwrap(); - std::fs::create_dir_all(&app_project).unwrap(); - std::fs::create_dir_all(&lib_project).unwrap(); - std::fs::write(lib_project.join("library.json"), r#"{"name":"libproj"}"#).unwrap(); - - let (_, _, app_compile_db) = expected_fast_path_artifacts(&build_dir, &app_project); - let (_, _, lib_compile_db) = expected_fast_path_artifacts(&build_dir, &lib_project); - - assert_eq!(app_compile_db, app_project.join("compile_commands.json")); - assert_eq!(lib_compile_db, build_dir.join("compile_commands.json")); + let contract = + FastPathContract::for_project_outputs(&build_dir, &project_dir, Vec::::new()); + assert_eq!(contract.watches().len(), 2); + assert_eq!(contract.watches()[0].root, project_dir); + assert_eq!(contract.watches()[1].root, dep_libs_dir); } } diff --git a/crates/fbuild-build/src/sam/orchestrator.rs b/crates/fbuild-build/src/sam/orchestrator.rs index fe28598a..bfcdbdbc 100644 --- a/crates/fbuild-build/src/sam/orchestrator.rs +++ b/crates/fbuild-build/src/sam/orchestrator.rs @@ -23,12 +23,11 @@ use fbuild_core::{Platform, Result}; use serde::Serialize; use crate::build_fingerprint::{ - hash_watch_set_stamps_cached, save_json, stable_hash_json, FastPathInputs, - PersistedBuildFingerprint, BUILD_FINGERPRINT_VERSION, + expected_fast_path_artifacts, stable_hash_json, FastPathCheckInputs, FastPathContract, + FastPathPersistInputs, BUILD_FINGERPRINT_VERSION, }; use crate::compile_database::TargetArchitecture; use crate::pipeline; -use crate::zccache::FingerprintWatch; use crate::{BuildOrchestrator, BuildParams, BuildResult, SourceScanner}; use super::sam_compiler::SamCompiler; @@ -69,37 +68,6 @@ fn profile_label(profile: fbuild_core::BuildProfile) -> &'static str { } } -fn build_fingerprint_path(build_dir: &Path) -> PathBuf { - build_dir.join("build_fingerprint.json") -} - -fn collect_fast_path_watches(build_dir: &Path, project_dir: &Path) -> Vec { - let mut watches = Vec::new(); - if let Some(watch) = - crate::build_fingerprint::fast_path_watch("project", build_dir, project_dir) - { - watches.push(watch); - } - let resolved_libs_dir = build_dir.join("libs"); - if let Some(watch) = - crate::build_fingerprint::fast_path_watch("dep_libs", build_dir, &resolved_libs_dir) - { - watches.push(watch); - } - watches -} - -fn expected_fast_path_artifacts( - build_dir: &Path, - project_dir: &Path, -) -> (PathBuf, PathBuf, PathBuf) { - ( - build_dir.join("firmware.elf"), - build_dir.join("firmware.bin"), - crate::compile_database::CompileDatabase::expected_output_path(build_dir, project_dir), - ) -} - impl BuildOrchestrator for SamOrchestrator { fn platform(&self) -> Platform { Platform::AtmelSam @@ -133,7 +101,6 @@ impl BuildOrchestrator for SamOrchestrator { }; let build_dir = &ctx.build_dir; - let fingerprint_path = build_fingerprint_path(build_dir); let metadata_hash = stable_hash_json(&SamFingerprintMetadata { version: BUILD_FINGERPRINT_VERSION, env_name: params.env_name.clone(), @@ -151,25 +118,25 @@ impl BuildOrchestrator for SamOrchestrator { max_flash: ctx.board.max_flash, max_ram: ctx.board.max_ram, })?; - let fingerprint_watches = collect_fast_path_watches(build_dir, ¶ms.project_dir); + let (fast_elf, [fast_bin], fast_compile_db) = + expected_fast_path_artifacts(build_dir, ¶ms.project_dir, ["firmware.bin"]); + let fast_path = FastPathContract::for_project_outputs( + build_dir, + ¶ms.project_dir, + [fast_elf.clone(), fast_bin.clone(), fast_compile_db.clone()], + ); if !params.compiledb_only && !params.symbol_analysis && params.symbol_analysis_path.is_none() { - let (fast_elf, fast_bin, fast_compile_db) = - expected_fast_path_artifacts(build_dir, ¶ms.project_dir); - let required_artifacts = [fast_elf.clone(), fast_bin.clone(), fast_compile_db.clone()]; - let inputs = FastPathInputs { - fingerprint_path: &fingerprint_path, + let inputs = FastPathCheckInputs { metadata_hash: &metadata_hash, - watches: &fingerprint_watches, - required_artifacts: &required_artifacts, extra_artifact_ok: None, watch_set_cache: params.watch_set_cache.as_deref(), compiler_cache: compiler_cache.as_deref(), }; - if let Some(hit) = crate::build_fingerprint::fast_path_check(&inputs)? { + if let Some(hit) = crate::build_fingerprint::fast_path_check(&fast_path, &inputs)? { ctx.build_log .push("No-op fingerprint matched; reusing existing SAM artifacts.".to_string()); let elapsed = start.elapsed().as_secs_f64(); @@ -316,35 +283,15 @@ impl BuildOrchestrator for SamOrchestrator { && !params.symbol_analysis && params.symbol_analysis_path.is_none() { - let persisted_fingerprint = PersistedBuildFingerprint { - version: BUILD_FINGERPRINT_VERSION, - metadata_hash: metadata_hash.clone(), - file_set_hash: match hash_watch_set_stamps_cached( - &fingerprint_watches, - params.watch_set_cache.as_deref(), - ) { - Ok(hash) => Some(hash), - Err(e) => { - tracing::warn!("failed to hash watched inputs for fingerprint save: {}", e); - None - } + crate::build_fingerprint::persist_fast_path_success( + &fast_path, + &FastPathPersistInputs { + metadata_hash: &metadata_hash, + size_info: build_result.size_info.clone(), + watch_set_cache: params.watch_set_cache.as_deref(), + compiler_cache: compiler_cache.as_deref(), }, - size_info: build_result.size_info.clone(), - }; - if let Err(e) = save_json(&fingerprint_path, &persisted_fingerprint) { - tracing::warn!("failed to write build fingerprint: {}", e); - } - if let Some(ref zcc) = compiler_cache { - for watch in &fingerprint_watches { - if let Err(e) = crate::zccache::mark_fingerprint_success(zcc, watch) { - tracing::warn!( - "failed to mark zccache fingerprint success for {}: {}", - watch.root.display(), - e - ); - } - } - } + ); } Ok(build_result) @@ -449,39 +396,17 @@ mod tests { } #[test] - fn test_collect_fast_path_watches_skips_missing_dep_libs() { + fn test_fast_path_contract_preserves_missing_dep_libs() { let tmp = tempfile::TempDir::new().unwrap(); let build_dir = tmp.path().join("build"); let project_dir = tmp.path().join("project"); std::fs::create_dir_all(&build_dir).unwrap(); std::fs::create_dir_all(&project_dir).unwrap(); - let watches = collect_fast_path_watches(&build_dir, &project_dir); - assert_eq!(watches.len(), 1); - assert_eq!(watches[0].root, project_dir); - } - - #[test] - fn test_expected_fast_path_artifacts_follow_compile_db_location() { - let tmp = tempfile::TempDir::new().unwrap(); - let build_dir = tmp.path().join("build"); - let app_project = tmp.path().join("app"); - let lib_project = tmp.path().join("libproj"); - std::fs::create_dir_all(&build_dir).unwrap(); - std::fs::create_dir_all(&app_project).unwrap(); - std::fs::create_dir_all(&lib_project).unwrap(); - std::fs::write(lib_project.join("library.json"), r#"{"name":"libproj"}"#).unwrap(); - - let (app_elf, app_bin, app_compile_db) = - expected_fast_path_artifacts(&build_dir, &app_project); - let (lib_elf, lib_bin, lib_compile_db) = - expected_fast_path_artifacts(&build_dir, &lib_project); - - assert_eq!(app_elf, build_dir.join("firmware.elf")); - assert_eq!(app_bin, build_dir.join("firmware.bin")); - assert_eq!(app_compile_db, app_project.join("compile_commands.json")); - assert_eq!(lib_elf, build_dir.join("firmware.elf")); - assert_eq!(lib_bin, build_dir.join("firmware.bin")); - assert_eq!(lib_compile_db, build_dir.join("compile_commands.json")); + let contract = + FastPathContract::for_project_outputs(&build_dir, &project_dir, Vec::::new()); + assert_eq!(contract.watches().len(), 2); + assert_eq!(contract.watches()[0].root, project_dir); + assert_eq!(contract.watches()[1].root, build_dir.join("libs")); } } diff --git a/crates/fbuild-build/src/teensy/orchestrator.rs b/crates/fbuild-build/src/teensy/orchestrator.rs index 28689614..788069ec 100644 --- a/crates/fbuild-build/src/teensy/orchestrator.rs +++ b/crates/fbuild-build/src/teensy/orchestrator.rs @@ -22,13 +22,12 @@ use serde::Serialize; use walkdir::{DirEntry, WalkDir}; use crate::build_fingerprint::{ - hash_watch_set_stamps_cached, save_json, stable_hash_json, FastPathInputs, - PersistedBuildFingerprint, BUILD_FINGERPRINT_VERSION, + expected_fast_path_artifacts, stable_hash_json, FastPathCheckInputs, FastPathContract, + FastPathPersistInputs, BUILD_FINGERPRINT_VERSION, }; -use crate::compile_database::{CompileDatabase, TargetArchitecture}; +use crate::compile_database::TargetArchitecture; use crate::compiler::Compiler as _; use crate::pipeline; -use crate::zccache::FingerprintWatch; use crate::{BuildOrchestrator, BuildParams, BuildResult, SourceScanner}; use super::teensy_compiler::TeensyCompiler; @@ -61,37 +60,6 @@ fn profile_label(profile: fbuild_core::BuildProfile) -> &'static str { } } -fn build_fingerprint_path(build_dir: &Path) -> PathBuf { - build_dir.join("build_fingerprint.json") -} - -fn collect_fast_path_watches(build_dir: &Path, project_dir: &Path) -> Vec { - let mut watches = Vec::new(); - if let Some(watch) = - crate::build_fingerprint::fast_path_watch("project", build_dir, project_dir) - { - watches.push(watch); - } - let resolved_libs_dir = build_dir.join("libs"); - if let Some(watch) = - crate::build_fingerprint::fast_path_watch("dep_libs", build_dir, &resolved_libs_dir) - { - watches.push(watch); - } - watches -} - -fn expected_fast_path_artifacts( - build_dir: &Path, - project_dir: &Path, -) -> (PathBuf, PathBuf, PathBuf) { - ( - build_dir.join("firmware.elf"), - build_dir.join("firmware.hex"), - CompileDatabase::expected_output_path(build_dir, project_dir), - ) -} - fn resolve_teensy_framework_library_sources( framework: &fbuild_packages::library::TeensyCores, project_dir: &Path, @@ -338,7 +306,6 @@ impl BuildOrchestrator for TeensyOrchestrator { let core_dir = framework.get_core_dir(&ctx.board.core); let build_dir = &ctx.build_dir; - let fingerprint_path = build_fingerprint_path(build_dir); let metadata_hash = stable_hash_json(&TeensyFingerprintMetadata { version: BUILD_FINGERPRINT_VERSION, env_name: params.env_name.clone(), @@ -354,25 +321,25 @@ impl BuildOrchestrator for TeensyOrchestrator { max_flash: ctx.board.max_flash, max_ram: ctx.board.max_ram, })?; - let fingerprint_watches = collect_fast_path_watches(build_dir, ¶ms.project_dir); + let (fast_elf, [fast_hex], fast_compile_db) = + expected_fast_path_artifacts(build_dir, ¶ms.project_dir, ["firmware.hex"]); + let fast_path = FastPathContract::for_project_outputs( + build_dir, + ¶ms.project_dir, + [fast_elf.clone(), fast_hex.clone(), fast_compile_db.clone()], + ); if !params.compiledb_only && !params.symbol_analysis && params.symbol_analysis_path.is_none() { - let (fast_elf, fast_hex, fast_compile_db) = - expected_fast_path_artifacts(build_dir, ¶ms.project_dir); - let required_artifacts = [fast_elf.clone(), fast_hex.clone(), fast_compile_db.clone()]; - let inputs = FastPathInputs { - fingerprint_path: &fingerprint_path, + let inputs = FastPathCheckInputs { metadata_hash: &metadata_hash, - watches: &fingerprint_watches, - required_artifacts: &required_artifacts, extra_artifact_ok: None, watch_set_cache: params.watch_set_cache.as_deref(), compiler_cache: compiler_cache.as_deref(), }; - if let Some(hit) = crate::build_fingerprint::fast_path_check(&inputs)? { + if let Some(hit) = crate::build_fingerprint::fast_path_check(&fast_path, &inputs)? { ctx.build_log.push( "No-op fingerprint matched; reusing existing Teensy artifacts.".to_string(), ); @@ -508,35 +475,15 @@ impl BuildOrchestrator for TeensyOrchestrator { && !params.symbol_analysis && params.symbol_analysis_path.is_none() { - let persisted_fingerprint = PersistedBuildFingerprint { - version: BUILD_FINGERPRINT_VERSION, - metadata_hash: metadata_hash.clone(), - file_set_hash: match hash_watch_set_stamps_cached( - &fingerprint_watches, - params.watch_set_cache.as_deref(), - ) { - Ok(hash) => Some(hash), - Err(e) => { - tracing::warn!("failed to hash watched inputs for fingerprint save: {}", e); - None - } + crate::build_fingerprint::persist_fast_path_success( + &fast_path, + &FastPathPersistInputs { + metadata_hash: &metadata_hash, + size_info: build_result.size_info.clone(), + watch_set_cache: params.watch_set_cache.as_deref(), + compiler_cache: compiler_cache.as_deref(), }, - size_info: build_result.size_info.clone(), - }; - if let Err(e) = save_json(&fingerprint_path, &persisted_fingerprint) { - tracing::warn!("failed to write build fingerprint: {}", e); - } - if let Some(ref zcc) = compiler_cache { - for watch in &fingerprint_watches { - if let Err(e) = crate::zccache::mark_fingerprint_success(zcc, watch) { - tracing::warn!( - "failed to mark zccache fingerprint success for {}: {}", - watch.root.display(), - e - ); - } - } - } + ); } Ok(build_result) @@ -587,34 +534,18 @@ mod tests { } #[test] - fn test_collect_fast_path_watches_skips_missing_dep_libs() { + fn test_fast_path_contract_preserves_missing_dep_libs() { let tmp = tempfile::TempDir::new().unwrap(); let build_dir = tmp.path().join("build"); let project_dir = tmp.path().join("project"); std::fs::create_dir_all(&build_dir).unwrap(); std::fs::create_dir_all(&project_dir).unwrap(); - let watches = collect_fast_path_watches(&build_dir, &project_dir); - assert_eq!(watches.len(), 1); - assert_eq!(watches[0].root, project_dir); - } - - #[test] - fn test_expected_fast_path_artifacts_follow_compile_db_location() { - let tmp = tempfile::TempDir::new().unwrap(); - let build_dir = tmp.path().join("build"); - let app_project = tmp.path().join("app"); - let lib_project = tmp.path().join("libproj"); - std::fs::create_dir_all(&build_dir).unwrap(); - std::fs::create_dir_all(&app_project).unwrap(); - std::fs::create_dir_all(&lib_project).unwrap(); - std::fs::write(lib_project.join("library.json"), r#"{"name":"libproj"}"#).unwrap(); - - let (_, _, app_compile_db) = expected_fast_path_artifacts(&build_dir, &app_project); - let (_, _, lib_compile_db) = expected_fast_path_artifacts(&build_dir, &lib_project); - - assert_eq!(app_compile_db, app_project.join("compile_commands.json")); - assert_eq!(lib_compile_db, build_dir.join("compile_commands.json")); + let contract = + FastPathContract::for_project_outputs(&build_dir, &project_dir, Vec::::new()); + assert_eq!(contract.watches().len(), 2); + assert_eq!(contract.watches()[0].root, project_dir); + assert_eq!(contract.watches()[1].root, build_dir.join("libs")); } #[test]