From f3b11be31b741aa6983a22d89024c5e3a17b1943 Mon Sep 17 00:00:00 2001 From: zackees Date: Thu, 2 Jul 2026 17:31:49 -0700 Subject: [PATCH 1/2] fix(build): resolve board_build.partitions CSV project-relative first MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PlatformIO semantics: an explicitly configured partitions CSV is a project-relative path; the framework tools/partitions dir serves built-in names. fbuild only looked in the framework dir, so custom tables (NightDriverStrip's config/partitions_custom_noota.csv) were silently ignored — wrong partition layout on flash, and the missing partitions.bin also blocked ESP32 fast-path fingerprint persistence (#951/#957). An explicit CSV that resolves nowhere is now a hard error instead of a warn-and-continue. Closes #955 Part of #942 Co-Authored-By: Claude Fable 5 --- .../src/esp32/orchestrator/boot_artifacts.rs | 101 +++++++++++++++++- .../src/esp32/orchestrator/build.rs | 1 + 2 files changed, 100 insertions(+), 2 deletions(-) diff --git a/crates/fbuild-build/src/esp32/orchestrator/boot_artifacts.rs b/crates/fbuild-build/src/esp32/orchestrator/boot_artifacts.rs index 2203438b..3ccc5244 100644 --- a/crates/fbuild-build/src/esp32/orchestrator/boot_artifacts.rs +++ b/crates/fbuild-build/src/esp32/orchestrator/boot_artifacts.rs @@ -9,9 +9,13 @@ use super::super::mcu_config::Esp32McuConfig; /// Stage boot/partition/boot_app0 binaries into `build_dir`. Logs warnings on /// missing inputs but does not error — the linker output remains usable for -/// in-tree flows even when the boot artifacts cannot be produced. +/// in-tree flows even when the boot artifacts cannot be produced. The one +/// exception: an explicitly configured `board_build.partitions` CSV that +/// resolves nowhere is a hard error (FastLED/fbuild#955) — silently flashing +/// a different partition table is worse than failing. pub(super) async fn prepare_boot_artifacts( build_dir: &Path, + project_dir: &Path, framework: &fbuild_packages::library::Esp32Framework, board: &fbuild_config::BoardConfig, mcu_config: &Esp32McuConfig, @@ -127,7 +131,11 @@ pub(super) async fn prepare_boot_artifacts( } else { // Generate partitions.bin from CSV using gen_esp32part.py let partitions_name = board.partitions.as_deref().unwrap_or("default.csv"); - let parts_csv = framework.get_partitions_csv(partitions_name); + let parts_csv = resolve_partitions_csv( + project_dir, + framework.get_partitions_csv(partitions_name), + board.partitions.as_deref(), + )?; let gen_tool = framework.get_gen_esp32part(); if parts_csv.exists() && gen_tool.exists() { let gen_tool_str = gen_tool.to_string_lossy(); @@ -177,3 +185,92 @@ pub(super) async fn prepare_boot_artifacts( perf.checkpoint("boot-artifacts-finish"); Ok(()) } + +/// Resolve the partitions CSV with PlatformIO semantics +/// (FastLED/fbuild#955): an explicitly configured `board_build.partitions` +/// path is project-relative first; the framework `tools/partitions/` +/// directory serves built-in names (`default.csv`, `huge_app.csv`, ...). +/// An explicit CSV that resolves nowhere is a hard error. When nothing was +/// configured, the framework default is returned as-is and the caller's +/// existence check keeps the historical warn-and-continue behavior. +fn resolve_partitions_csv( + project_dir: &Path, + framework_candidate: std::path::PathBuf, + configured: Option<&str>, +) -> Result { + let Some(name) = configured else { + return Ok(framework_candidate); + }; + let project_candidate = project_dir.join(name); + if project_candidate.exists() { + return Ok(project_candidate); + } + if framework_candidate.exists() { + return Ok(framework_candidate); + } + Err(fbuild_core::FbuildError::BuildFailed(format!( + "board_build.partitions = {} not found: checked {} and {}", + name, + project_candidate.display(), + framework_candidate.display() + ))) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn explicit_partitions_csv_resolves_project_relative_first() { + let tmp = tempfile::TempDir::new().unwrap(); + let project = tmp.path(); + std::fs::create_dir_all(project.join("config")).unwrap(); + std::fs::write(project.join("config/custom.csv"), "csv").unwrap(); + + let resolved = resolve_partitions_csv( + project, + project.join("framework/tools/partitions/config/custom.csv"), + Some("config/custom.csv"), + ) + .unwrap(); + assert_eq!(resolved, project.join("config/custom.csv")); + } + + #[test] + fn explicit_builtin_name_falls_back_to_framework_dir() { + let tmp = tempfile::TempDir::new().unwrap(); + let project = tmp.path(); + let fw_dir = project.join("framework/tools/partitions"); + std::fs::create_dir_all(&fw_dir).unwrap(); + std::fs::write(fw_dir.join("huge_app.csv"), "csv").unwrap(); + + let resolved = + resolve_partitions_csv(project, fw_dir.join("huge_app.csv"), Some("huge_app.csv")) + .unwrap(); + assert_eq!(resolved, fw_dir.join("huge_app.csv")); + } + + #[test] + fn explicit_partitions_csv_missing_everywhere_is_a_hard_error() { + let tmp = tempfile::TempDir::new().unwrap(); + let project = tmp.path(); + + let err = resolve_partitions_csv( + project, + project.join("framework/tools/partitions/nope.csv"), + Some("nope.csv"), + ) + .unwrap_err(); + assert!(err + .to_string() + .contains("board_build.partitions = nope.csv")); + } + + #[test] + fn unconfigured_default_keeps_warn_and_continue_semantics() { + let tmp = tempfile::TempDir::new().unwrap(); + let missing_default = tmp.path().join("framework/tools/partitions/default.csv"); + let resolved = resolve_partitions_csv(tmp.path(), missing_default.clone(), None).unwrap(); + assert_eq!(resolved, missing_default); + } +} diff --git a/crates/fbuild-build/src/esp32/orchestrator/build.rs b/crates/fbuild-build/src/esp32/orchestrator/build.rs index 6af6780e..626aa72e 100644 --- a/crates/fbuild-build/src/esp32/orchestrator/build.rs +++ b/crates/fbuild-build/src/esp32/orchestrator/build.rs @@ -808,6 +808,7 @@ impl BuildOrchestrator for Esp32Orchestrator { // 14. Prepare boot artifacts for deployment / emulation prepare_boot_artifacts( build_dir, + ¶ms.project_dir, &framework, &ctx.board, &mcu_config, From 6703d7e54e15b01f3816225b839a601d452870c6 Mon Sep 17 00:00:00 2001 From: zackees Date: Thu, 2 Jul 2026 17:49:51 -0700 Subject: [PATCH 2/2] fix(build): resolve python3 for gen_esp32part.py (bare 'python' is absent on modern distros) Co-Authored-By: Claude Fable 5 --- .../src/esp32/orchestrator/boot_artifacts.rs | 18 ++++++++++++------ crates/fbuild-build/src/script_runtime.rs | 2 +- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/crates/fbuild-build/src/esp32/orchestrator/boot_artifacts.rs b/crates/fbuild-build/src/esp32/orchestrator/boot_artifacts.rs index 3ccc5244..2f900494 100644 --- a/crates/fbuild-build/src/esp32/orchestrator/boot_artifacts.rs +++ b/crates/fbuild-build/src/esp32/orchestrator/boot_artifacts.rs @@ -141,13 +141,19 @@ pub(super) async fn prepare_boot_artifacts( let gen_tool_str = gen_tool.to_string_lossy(); let parts_csv_str = parts_csv.to_string_lossy(); let parts_dst_str = parts_dst.to_string_lossy(); - let args = [ - "python", - &gen_tool_str, + // `python` doesn't exist on modern distros (ubuntu 24.04 ships + // only `python3`); resolve the interpreter the same way the + // extra_scripts runtime does. + let python = crate::script_runtime::find_python() + .await + .unwrap_or_else(|| vec!["python".to_string()]); + let mut args: Vec<&str> = python.iter().map(|s| s.as_str()).collect(); + args.extend([ + gen_tool_str.as_ref(), "-q", - &parts_csv_str, - &parts_dst_str, - ]; + parts_csv_str.as_ref(), + parts_dst_str.as_ref(), + ]); match fbuild_core::subprocess::run_command( &args, None, diff --git a/crates/fbuild-build/src/script_runtime.rs b/crates/fbuild-build/src/script_runtime.rs index aa67f7ff..743d85bc 100644 --- a/crates/fbuild-build/src/script_runtime.rs +++ b/crates/fbuild-build/src/script_runtime.rs @@ -298,7 +298,7 @@ fn libs_to_flags( Ok(flags) } -async fn find_python() -> Option> { +pub(crate) async fn find_python() -> Option> { let candidates: &[&[&str]] = if cfg!(windows) { &[&["python"], &["py", "-3"]] } else {