Skip to content
Merged
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
119 changes: 111 additions & 8 deletions crates/fbuild-build/src/esp32/orchestrator/boot_artifacts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -127,19 +131,29 @@ 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();
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,
Expand Down Expand Up @@ -177,3 +191,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<std::path::PathBuf> {
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);
}
}
1 change: 1 addition & 0 deletions crates/fbuild-build/src/esp32/orchestrator/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,7 @@ impl BuildOrchestrator for Esp32Orchestrator {
// 14. Prepare boot artifacts for deployment / emulation
prepare_boot_artifacts(
build_dir,
&params.project_dir,
&framework,
&ctx.board,
&mcu_config,
Expand Down
2 changes: 1 addition & 1 deletion crates/fbuild-build/src/script_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ fn libs_to_flags(
Ok(flags)
}

async fn find_python() -> Option<Vec<String>> {
pub(crate) async fn find_python() -> Option<Vec<String>> {
let candidates: &[&[&str]] = if cfg!(windows) {
&[&["python"], &["py", "-3"]]
} else {
Expand Down
Loading