Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions crates/fbuild-build-engine/src/build_fingerprint/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@ pub struct BinArtifactCache {
pub flash_mode: String,
pub flash_freq: String,
pub flash_size: String,
/// Which esptool produced the cached bin (FastLED/fbuild#1238): empty
/// for a provisioned absolute-path esptool or when no caller PATH was
/// forwarded (pre-#1236 behavior, so old records stay valid); otherwise
/// a short hash of the caller PATH the bare-name spawn resolved
/// against. `serde(default)` deserializes old records as `""`, which
/// mismatches any caller-PATH fingerprint — invalidation in the
/// correct direction only.
#[serde(default)]
pub esptool_fingerprint: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down
70 changes: 70 additions & 0 deletions crates/fbuild-build-esp/src/esp32/esp32_linker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,9 +259,34 @@ impl Esp32Linker {
flash_mode: self.flash_mode.clone(),
flash_freq: self.flash_freq.clone(),
flash_size: flash_size.to_string(),
esptool_fingerprint: self.esptool_fingerprint(),
})
}

/// Key the bin cache on which esptool a bare-name spawn would resolve
/// (FastLED/fbuild#1238). A provisioned absolute-path esptool (or no
/// forwarded caller PATH) fingerprints as `""` — exactly the pre-#1236
/// behavior, so existing caches stay valid. A bare-name spawn under a
/// caller PATH fingerprints that PATH, so two requests whose PATHs
/// resolve different esptool binaries never share a `firmware.bin`.
fn esptool_fingerprint(&self) -> String {
if self.esptool_bin.is_some() {
return String::new();
}
match self.caller_path.as_deref() {
None => String::new(),
Some(path) => {
use sha2::{Digest, Sha256};
let digest = Sha256::digest(path.as_bytes());
let hex = digest
.iter()
.map(|b| format!("{b:02x}"))
.collect::<String>();
hex[..16].to_string()
}
}
}

fn can_reuse_bin(&self, elf_path: &Path, output_dir: &Path, flash_size: &str) -> bool {
let bin_out = output_dir.join("firmware.bin");
if !bin_out.exists() {
Expand Down Expand Up @@ -594,6 +619,51 @@ mod tests {
assert_eq!(linker.max_ram, Some(327680));
}

/// FastLED/fbuild#1238: seed a reusable bin cache written by `linker`,
/// returning the elf path whose stamp the cache records.
fn seed_bin_cache(linker: &Esp32Linker, dir: &Path) -> PathBuf {
let elf = dir.join("firmware.elf");
std::fs::write(&elf, b"elf").unwrap();
std::fs::write(dir.join("firmware.bin"), b"bin").unwrap();
let cache = linker.current_bin_cache(&elf, "4MB").unwrap();
save_json(&linker.bin_cache_path(dir), &cache).unwrap();
elf
}

#[test]
fn bare_name_esptool_bin_reuse_is_keyed_by_caller_path() {
let dir = tempfile::tempdir().unwrap();
// Bare-name esptool (no provisioned path) resolved under PATH_A.
let writer = test_linker("esp32c6").with_caller_path(Some("PATH_A".into()));
assert!(writer.esptool_bin.is_none(), "test premise: bare-name spawn");
let elf = seed_bin_cache(&writer, dir.path());

// Same caller PATH → same esptool → reuse.
assert!(writer.can_reuse_bin(&elf, dir.path(), "4MB"));
// Different caller PATH may resolve a different esptool → no reuse.
let other = test_linker("esp32c6").with_caller_path(Some("PATH_B".into()));
assert!(!other.can_reuse_bin(&elf, dir.path(), "4MB"));
// No caller PATH (daemon env) must not reuse a caller-PATH bin.
let legacy = test_linker("esp32c6");
assert!(!legacy.can_reuse_bin(&elf, dir.path(), "4MB"));
}

#[test]
fn absolute_esptool_bin_reuse_ignores_caller_path() {
let dir = tempfile::tempdir().unwrap();
// Provisioned absolute esptool: caller PATH is irrelevant to which
// tool runs, so the fingerprint stays empty and caching keeps
// working across requests with different PATHs.
let mut writer = test_linker("esp32c6").with_caller_path(Some("PATH_A".into()));
writer.esptool_bin = Some(PathBuf::from("/tools/esptool"));
assert_eq!(writer.esptool_fingerprint(), "");
let elf = seed_bin_cache(&writer, dir.path());

let mut reader = test_linker("esp32c6").with_caller_path(Some("PATH_B".into()));
reader.esptool_bin = Some(PathBuf::from("/tools/esptool"));
assert!(reader.can_reuse_bin(&elf, dir.path(), "4MB"));
}

#[test]
fn test_flash_size_uses_board_max_flash_for_elf2image_and_cache() {
let config = get_mcu_config("esp32c6").unwrap();
Expand Down
Loading