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
71 changes: 59 additions & 12 deletions crates/fbuild-build/src/esp32/esp32_linker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,46 @@ pub fn f_flash_to_esptool_freq(f_flash: Option<&str>, default_freq: &str) -> Str
}
}

/// Build the argv for an esptool `elf2image` invocation.
///
/// When esptool was provisioned, the standalone binary is invoked directly;
/// otherwise it falls back to an `esptool` on PATH. Shared by the firmware
/// conversion path here and the bootloader conversion path in
/// `orchestrator::boot_artifacts` so both honor the same provisioned tool
/// (FastLED/fbuild#954). The `--chip` flag is a global option and therefore
/// precedes the `elf2image` subcommand, matching the esptool v4/v5 CLI.
#[allow(clippy::too_many_arguments)]
pub(crate) fn esptool_elf2image_argv(
esptool_bin: Option<&Path>,
chip: &str,
flash_mode: &str,
flash_freq: &str,
flash_size: &str,
elf: &str,
out_bin: &str,
) -> Vec<String> {
let mut argv: Vec<String> = Vec::new();
match esptool_bin {
Some(bin) => argv.push(bin.to_string_lossy().to_string()),
None => argv.push("esptool".to_string()),
}
argv.extend([
"--chip".to_string(),
chip.to_string(),
"elf2image".to_string(),
"--flash-mode".to_string(),
flash_mode.to_string(),
"--flash-freq".to_string(),
flash_freq.to_string(),
"--flash-size".to_string(),
flash_size.to_string(),
elf.to_string(),
"-o".to_string(),
out_bin.to_string(),
]);
argv
}

/// ESP32-specific linker using RISC-V or Xtensa GCC as the link driver.
pub struct Esp32Linker {
gcc_path: PathBuf,
Expand All @@ -72,6 +112,9 @@ pub struct Esp32Linker {
flash_freq: String,
max_flash: Option<u64>,
max_ram: Option<u64>,
/// Path to the provisioned standalone esptool binary, if available. `None`
/// falls back to an `esptool` on PATH. See FastLED/fbuild#954.
esptool_bin: Option<PathBuf>,
verbose: bool,
}

Expand All @@ -91,6 +134,7 @@ impl Esp32Linker {
flash_freq: &str,
max_flash: Option<u64>,
max_ram: Option<u64>,
esptool_bin: Option<PathBuf>,
verbose: bool,
) -> Self {
let flash_mode = flash_mode.unwrap_or_else(|| mcu_config.default_flash_mode().to_string());
Expand All @@ -108,6 +152,7 @@ impl Esp32Linker {
flash_freq: flash_freq.to_string(),
max_flash,
max_ram,
esptool_bin,
verbose,
}
}
Expand Down Expand Up @@ -367,25 +412,22 @@ impl Linker for Esp32Linker {
}
// Determine flash size from max_flash config (bytes → human-readable).
// elf2image doesn't support "detect" — needs an explicit size.
let args = [
"esptool",
"--chip",
// Prefer the provisioned standalone esptool binary; fall back to an
// `esptool` on PATH (FastLED/fbuild#954).
let argv = esptool_elf2image_argv(
self.esptool_bin.as_deref(),
chip,
"elf2image",
"--flash-mode",
&self.flash_mode,
"--flash-freq",
&self.flash_freq,
"--flash-size",
&flash_size,
&elf_str,
"-o",
&bin_str,
];
);
let args: Vec<&str> = argv.iter().map(|s| s.as_str()).collect();

tracing::info!("elf2image: {}", args.join(" "));
tracing::info!("elf2image: {}", argv.join(" "));

match run_command(&args, None, None, Some(std::time::Duration::from_secs(30))).await {
match run_command(&args, None, None, Some(std::time::Duration::from_secs(60))).await {
Ok(result) if result.success() => {
let cache = self.current_bin_cache(&elf_out, &flash_size)?;
if let Err(e) = save_json(&self.bin_cache_path(output_dir), &cache) {
Expand All @@ -400,7 +442,8 @@ impl Linker for Esp32Linker {
))),
Err(e) => Err(fbuild_core::FbuildError::BuildFailed(format!(
"esptool not found — cannot convert firmware.elf to firmware.bin.\n\
Install with: pip install esptool\nError: {}",
fbuild normally provisions esptool automatically; if provisioning \
failed, install with: pip install esptool\nError: {}",
e
))),
}
Expand Down Expand Up @@ -475,6 +518,7 @@ mod tests {
"80m",
Some(3145728),
Some(327680),
None,
false,
)
}
Expand Down Expand Up @@ -504,6 +548,7 @@ mod tests {
"80m",
Some(4 * 1024 * 1024),
Some(327680),
None,
false,
);
let tmp = tempfile::TempDir::new().unwrap();
Expand Down Expand Up @@ -568,6 +613,7 @@ mod tests {
"80m",
Some(3145728),
Some(327680),
None,
false,
);
let flags = linker.linker_flags();
Expand Down Expand Up @@ -611,6 +657,7 @@ mod tests {
"80m",
Some(3145728),
Some(327680),
None,
false,
);
let flags = linker.linker_flags();
Expand Down
19 changes: 9 additions & 10 deletions crates/fbuild-build/src/esp32/orchestrator/boot_artifacts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ use super::super::mcu_config::Esp32McuConfig;
/// 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.
#[allow(clippy::too_many_arguments)]
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,
flash_freq: &str,
esptool_bin: Option<&Path>,
perf: &mut crate::perf_log::PerfTimer,
) -> Result<()> {
let boot_artifacts_started = Instant::now();
Expand Down Expand Up @@ -76,26 +78,23 @@ pub(super) async fn prepare_boot_artifacts(
board.max_flash,
mcu_config.default_flash_size(),
);
let args = [
"esptool",
"--chip",
// Prefer the provisioned standalone esptool binary; fall back to an
// `esptool` on PATH (FastLED/fbuild#954).
let argv = crate::esp32::esp32_linker::esptool_elf2image_argv(
esptool_bin,
&board.mcu,
"elf2image",
"--flash-mode",
boot_flash_mode,
"--flash-freq",
flash_freq,
"--flash-size",
flash_size,
&boot_elf_str,
"-o",
&boot_dst_str,
];
);
let args: Vec<&str> = argv.iter().map(|s| s.as_str()).collect();
match fbuild_core::subprocess::run_command(
&args,
None,
None,
Some(std::time::Duration::from_secs(30)),
Some(std::time::Duration::from_secs(60)),
)
.await
{
Expand Down
4 changes: 3 additions & 1 deletion crates/fbuild-build/src/esp32/orchestrator/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ impl BuildOrchestrator for Esp32Orchestrator {
// packages can honor consumer-pinned URLs (FastLED/fbuild#672).
let env_config = ctx.config.get_env_config(&params.env_name).ok();
let _resolve_phase = perf.phase("pioarduino-resolve");
let (toolchain, framework) = resolve_pioarduino_packages(
let (toolchain, framework, esptool_bin) = resolve_pioarduino_packages(
&params.project_dir,
&ctx.board.mcu,
&mcu_config,
Expand Down Expand Up @@ -793,6 +793,7 @@ impl BuildOrchestrator for Esp32Orchestrator {
&flash_freq,
ctx.board.max_flash,
ctx.board.max_ram,
esptool_bin.clone(),
params.verbose,
);

Expand Down Expand Up @@ -821,6 +822,7 @@ impl BuildOrchestrator for Esp32Orchestrator {
&ctx.board,
&mcu_config,
&flash_freq,
esptool_bin.as_deref(),
&mut perf,
)
.await?;
Expand Down
61 changes: 55 additions & 6 deletions crates/fbuild-build/src/esp32/orchestrator/packages.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Package resolution for pioarduino (platform.json, framework, toolchain).

use std::collections::HashMap;
use std::path::Path;
use std::path::{Path, PathBuf};

use fbuild_core::Result;

Expand All @@ -23,6 +23,7 @@ pub(super) async fn resolve_pioarduino_packages(
) -> Result<(
fbuild_packages::toolchain::Esp32Toolchain,
fbuild_packages::library::Esp32Framework,
Option<PathBuf>,
)> {
// Ensure pioarduino platform (contains platform.json with metadata URLs).
// Honor `platform_packages = platform-espressif32@<URL>#<sha>` from the env
Expand Down Expand Up @@ -107,10 +108,20 @@ pub(super) async fn resolve_pioarduino_packages(
}
Ok::<(), fbuild_core::FbuildError>(())
};
// Both futures run to completion; surface BOTH errors if both fail so the
// framework/SDK-libs failure (often the more actionable one) isn't dropped
// in favor of the toolchain error (CodeRabbit review on #967).
let (toolchain_res, framework_res) = tokio::join!(toolchain_fut, framework_fut);
// Provision the managed `tool-esptoolpy` package CONCURRENTLY with the
// toolchain + framework. esptool converts firmware.elf → firmware.bin at
// link time (FastLED/fbuild#954); provisioning it removes the pristine-
// machine "esptool not found — pip install esptool" failure. Best-effort:
// a resolution miss logs a warning and returns `None`, and the linker
// falls back to an `esptool` on PATH.
let esptool_fut = resolve_esptool(&platform, project_dir);

// The three futures run to completion; surface BOTH the toolchain and
// framework errors if both fail so the framework/SDK-libs failure (often
// the more actionable one) isn't dropped in favor of the toolchain error
// (CodeRabbit review on #967).
let (toolchain_res, framework_res, esptool_py) =
tokio::join!(toolchain_fut, framework_fut, esptool_fut);
match (toolchain_res, framework_res) {
(Err(tc), Err(fw)) => {
return Err(fbuild_core::FbuildError::BuildFailed(format!(
Expand All @@ -121,7 +132,45 @@ pub(super) async fn resolve_pioarduino_packages(
(Ok(_), Ok(_)) => {}
}

Ok((toolchain, framework))
Ok((toolchain, framework, esptool_py))
}

/// Provision the managed `tool-esptoolpy` package (the tasmota PyInstaller
/// standalone binary) from `platform.json` and return the path to the
/// `esptool` executable.
///
/// Best-effort: any failure (missing `platform.json` entry, unsupported host,
/// network error) is logged at warn level and yields `None`, so the linker's
/// existing "esptool on PATH" fallback still applies. See FastLED/fbuild#954.
async fn resolve_esptool(
platform: &fbuild_packages::library::Esp32Platform,
project_dir: &Path,
) -> Option<PathBuf> {
let url = match platform.get_package_url("tool-esptoolpy") {
Ok(url) => url,
Err(e) => {
tracing::warn!(
"could not resolve tool-esptoolpy from platform.json: {} \
(falling back to esptool on PATH)",
e
);
return None;
}
};
let esptool = fbuild_packages::library::Esptool::from_metadata_url(project_dir, &url);
match esptool.ensure_installed().await {
Ok(path) => {
tracing::info!("provisioned esptool at {}", path.display());
Some(path)
}
Err(e) => {
tracing::warn!(
"could not provision esptool: {} (falling back to esptool on PATH)",
e
);
None
}
}
}

/// Provision toolchain-* packages listed in `platform.json` other than the
Expand Down
Loading
Loading