|
13 | 13 | //! example) stay out of the compile set. See FastLED/fbuild#205. |
14 | 14 |
|
15 | 15 | use std::path::{Path, PathBuf}; |
| 16 | +use std::sync::OnceLock; |
16 | 17 |
|
| 18 | +use fbuild_library_select::cache::{resolve_cached, CacheKeyInputs}; |
17 | 19 | use fbuild_library_select::resolve as resolve_library_selection; |
18 | 20 | use fbuild_packages::library::FrameworkLibrary; |
19 | 21 | use walkdir::{DirEntry, WalkDir}; |
| 22 | +use zccache_artifact::KvStore; |
20 | 23 |
|
21 | 24 | /// Resolve framework library source files needed by a project. |
22 | 25 | pub fn resolve_framework_library_sources( |
@@ -56,6 +59,123 @@ pub fn resolve_framework_library_sources_from_libraries( |
56 | 59 | selection.source_files |
57 | 60 | } |
58 | 61 |
|
| 62 | +/// Cached counterpart to [`resolve_framework_library_sources`]. |
| 63 | +/// |
| 64 | +/// Routes the same `(libraries, project_dir, src_dir)` resolution through |
| 65 | +/// `fbuild_library_select::cache::resolve_cached` using the supplied |
| 66 | +/// `KvStore`. On a backend failure (open, read, write) we log a warning and |
| 67 | +/// fall back to the uncached `resolve(...)` so a degraded cache can never |
| 68 | +/// poison a build — same philosophy as the corrupt-entry handling already |
| 69 | +/// inside `cache.rs`. |
| 70 | +pub fn resolve_framework_library_sources_cached( |
| 71 | + libraries: &[FrameworkLibrary], |
| 72 | + project_dir: &Path, |
| 73 | + src_dir: &Path, |
| 74 | + key_inputs: &CacheKeyInputs<'_>, |
| 75 | + store: &KvStore, |
| 76 | +) -> Vec<PathBuf> { |
| 77 | + let (sources, _hit) = resolve_framework_library_sources_cached_with_hit( |
| 78 | + libraries, |
| 79 | + project_dir, |
| 80 | + src_dir, |
| 81 | + key_inputs, |
| 82 | + store, |
| 83 | + ); |
| 84 | + sources |
| 85 | +} |
| 86 | + |
| 87 | +/// Internal helper that returns `(sources, from_cache)` so tests can assert |
| 88 | +/// hit/miss without the public API surfacing that bit. The hit flag is |
| 89 | +/// `false` whenever the cache backend errored and we fell back to the |
| 90 | +/// uncached resolver. |
| 91 | +pub(crate) fn resolve_framework_library_sources_cached_with_hit( |
| 92 | + libraries: &[FrameworkLibrary], |
| 93 | + project_dir: &Path, |
| 94 | + src_dir: &Path, |
| 95 | + key_inputs: &CacheKeyInputs<'_>, |
| 96 | + store: &KvStore, |
| 97 | +) -> (Vec<PathBuf>, bool) { |
| 98 | + let roots = framework_include_scan_roots(project_dir, src_dir); |
| 99 | + if libraries.is_empty() { |
| 100 | + return (Vec::new(), false); |
| 101 | + } |
| 102 | + |
| 103 | + let seeds = collect_project_seeds(&roots); |
| 104 | + let search_paths: Vec<PathBuf> = roots.clone(); |
| 105 | + |
| 106 | + match resolve_cached(&seeds, &search_paths, libraries, key_inputs, store) { |
| 107 | + Ok(cached) => { |
| 108 | + for name in &cached.selection.required_libraries { |
| 109 | + if let Some(lib) = libraries.iter().find(|l| &l.name == name) { |
| 110 | + tracing::info!( |
| 111 | + "selected framework library '{}': {} source files", |
| 112 | + lib.name, |
| 113 | + lib.source_files.len() |
| 114 | + ); |
| 115 | + } |
| 116 | + } |
| 117 | + tracing::info!( |
| 118 | + cache = if cached.from_cache { "hit" } else { "miss" }, |
| 119 | + key = %cached.key.to_hex(), |
| 120 | + "library-select cache: {}", |
| 121 | + if cached.from_cache { "hit" } else { "miss" } |
| 122 | + ); |
| 123 | + (cached.selection.source_files, cached.from_cache) |
| 124 | + } |
| 125 | + Err(err) => { |
| 126 | + tracing::warn!( |
| 127 | + error = %err, |
| 128 | + "library-select cache backend error; falling back to uncached resolve" |
| 129 | + ); |
| 130 | + ( |
| 131 | + resolve_framework_library_sources_from_libraries(libraries, &roots), |
| 132 | + false, |
| 133 | + ) |
| 134 | + } |
| 135 | + } |
| 136 | +} |
| 137 | + |
| 138 | +/// Process-shared `KvStore` for the library-selection cache. |
| 139 | +/// |
| 140 | +/// Opens lazily on first call and caches the handle for the rest of the |
| 141 | +/// process. Returns `None` on open failure — callers must skip caching |
| 142 | +/// (and route through the uncached resolver) rather than crash. |
| 143 | +pub fn library_select_kv_store() -> Option<&'static KvStore> { |
| 144 | + static STORE: OnceLock<Option<KvStore>> = OnceLock::new(); |
| 145 | + STORE |
| 146 | + .get_or_init(|| { |
| 147 | + let dir = library_select_cache_dir(); |
| 148 | + match KvStore::open(&dir) { |
| 149 | + Ok(store) => { |
| 150 | + tracing::info!( |
| 151 | + path = %dir.display(), |
| 152 | + "library-select cache: opened KvStore" |
| 153 | + ); |
| 154 | + Some(store) |
| 155 | + } |
| 156 | + Err(err) => { |
| 157 | + tracing::warn!( |
| 158 | + path = %dir.display(), |
| 159 | + error = %err, |
| 160 | + "library-select cache: failed to open KvStore; \ |
| 161 | + resolution will run uncached" |
| 162 | + ); |
| 163 | + None |
| 164 | + } |
| 165 | + } |
| 166 | + }) |
| 167 | + .as_ref() |
| 168 | +} |
| 169 | + |
| 170 | +/// Filesystem location of the library-selection KvStore. |
| 171 | +/// |
| 172 | +/// Routes through `fbuild_paths::get_cache_root()` so the cache obeys the |
| 173 | +/// dev/prod isolation contract (`FBUILD_DEV_MODE=1` → `~/.fbuild/dev/cache`) |
| 174 | +/// and any `FBUILD_CACHE_DIR` override. |
| 175 | +fn library_select_cache_dir() -> PathBuf { |
| 176 | + fbuild_paths::get_cache_root().join("library-selection") |
| 177 | +} |
| 178 | + |
59 | 179 | /// Project directories to scan for `#include` directives and local headers. |
60 | 180 | pub fn framework_include_scan_roots(project_dir: &Path, src_dir: &Path) -> Vec<PathBuf> { |
61 | 181 | let mut roots = Vec::new(); |
@@ -331,4 +451,54 @@ mod tests { |
331 | 451 |
|
332 | 452 | assert_eq!(sources, vec![spi_dir.join("SPI.cpp")]); |
333 | 453 | } |
| 454 | + |
| 455 | + #[test] |
| 456 | + fn cached_resolution_round_trips_through_kvstore() { |
| 457 | + let tmp = tempfile::TempDir::new().unwrap(); |
| 458 | + let project_dir = tmp.path().join("project"); |
| 459 | + let src_dir = project_dir.join("src"); |
| 460 | + std::fs::create_dir_all(&src_dir).unwrap(); |
| 461 | + std::fs::write(src_dir.join("main.cpp"), "#include <SPI.h>\n").unwrap(); |
| 462 | + |
| 463 | + let spi_dir = tmp.path().join("framework").join("libraries").join("SPI"); |
| 464 | + std::fs::create_dir_all(&spi_dir).unwrap(); |
| 465 | + std::fs::write(spi_dir.join("SPI.h"), "").unwrap(); |
| 466 | + std::fs::write(spi_dir.join("SPI.cpp"), "").unwrap(); |
| 467 | + |
| 468 | + let libraries = vec![FrameworkLibrary { |
| 469 | + name: "SPI".to_string(), |
| 470 | + dir: spi_dir.clone(), |
| 471 | + include_dirs: vec![spi_dir.clone()], |
| 472 | + source_files: vec![spi_dir.join("SPI.cpp")], |
| 473 | + }]; |
| 474 | + |
| 475 | + let framework_root = tmp.path().join("framework"); |
| 476 | + let key_inputs = CacheKeyInputs { |
| 477 | + toolchain_triple: "test-arm-none-eabi", |
| 478 | + framework_install_path: &framework_root, |
| 479 | + framework_version: "0.0.0-test", |
| 480 | + }; |
| 481 | + |
| 482 | + let kv = KvStore::open(tmp.path().join("kv")).unwrap(); |
| 483 | + |
| 484 | + let (first, hit_first) = resolve_framework_library_sources_cached_with_hit( |
| 485 | + &libraries, |
| 486 | + &project_dir, |
| 487 | + &src_dir, |
| 488 | + &key_inputs, |
| 489 | + &kv, |
| 490 | + ); |
| 491 | + assert!(!hit_first, "first call must miss the cache"); |
| 492 | + assert_eq!(first, vec![spi_dir.join("SPI.cpp")]); |
| 493 | + |
| 494 | + let (second, hit_second) = resolve_framework_library_sources_cached_with_hit( |
| 495 | + &libraries, |
| 496 | + &project_dir, |
| 497 | + &src_dir, |
| 498 | + &key_inputs, |
| 499 | + &kv, |
| 500 | + ); |
| 501 | + assert!(hit_second, "second call must hit the cache"); |
| 502 | + assert_eq!(first, second, "cache hit must yield identical sources"); |
| 503 | + } |
334 | 504 | } |
0 commit comments