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
1 change: 1 addition & 0 deletions crates/fbuild-build-esp/src/esp32/orchestrator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ mod job_pool;
mod local_libs;
mod packages;

pub use packages::resolve_deploy_esptool;
pub(crate) use packages::{downloadable_lib_deps, provision_esp32};

#[cfg(test)]
Expand Down
47 changes: 47 additions & 0 deletions crates/fbuild-build-esp/src/esp32/orchestrator/packages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,53 @@ pub(super) async fn resolve_pioarduino_packages(
Ok((toolchain, framework, esptool_py, platform_info))
}

/// Resolve the esptool executable a deploy should spawn: the same one the
/// build uses (`FBUILD_ESPTOOL_PATH` first, then the provisioned
/// `tool-esptoolpy` package from the env's `platform.json`), so deploy works
/// from a shell with no esptool on PATH and runs the build's esptool version
/// (FastLED/fbuild#1616).
///
/// `Ok(None)` means no usable resolved esptool; the deployer then falls back
/// to a bare `esptool` PATH lookup. That includes the v4 `esptool.py` source
/// package older pioarduino pins provision: the deployer speaks v5's
/// hyphenated CLI (`write-flash`, `default-reset`), which v4 rejects.
pub async fn resolve_deploy_esptool(
project_dir: &Path,
env_config: Option<&HashMap<String, String>>,
) -> Result<Option<NormalizedPath>> {
// An explicit override is the user's choice and is used as-is, whatever
// its file name (v5 still ships `esptool.py` entry points).
if let Some(path) = fbuild_packages::library::esptool_path_override()? {
return Ok(Some(path));
}
let Some(platform) = pioarduino_platform(project_dir, env_config, true).await? else {
return Ok(None);
};
fbuild_packages::Package::ensure_installed(&platform).await?;
// The file-name check only applies to the package fbuild provisioned,
// where `esptool.py` means the v4 source package.
Ok(resolve_esptool(&platform, project_dir).await?.filter(|path| {
let usable = speaks_v5_cli(path);
if !usable {
tracing::info!(
"resolved esptool {} is the v4 source package; deploy uses an `esptool` on PATH",
path.display()
);
}
usable
}))
}

/// Whether `esptool` speaks the v5 hyphenated CLI the deployer emits. The v4
/// source package installs an `esptool.py` entry point (the same signal
/// `esp32_linker` uses to pick underscore flags for `elf2image`).
pub(super) fn speaks_v5_cli(esptool: &Path) -> bool {
!esptool
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.starts_with("esptool.py"))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/// Provision what [`resolve_pioarduino_packages`] installs — platform,
/// MCU-primary toolchain, framework, SDK libs and esptool — one report row
/// each, for `fbuild install` (FastLED/fbuild#1433). Check and dry-run modes
Expand Down
19 changes: 19 additions & 0 deletions crates/fbuild-build-esp/src/esp32/orchestrator/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,3 +301,22 @@ fn effective_define_flags_match_compiler_overlay_order() {
);
assert!(!defines.contains_key("VALUE"));
}

/// FastLED/fbuild#1616: deploy reuses the build's esptool only when it speaks
/// the v5 hyphenated CLI the deployer emits; the v4 `esptool.py` source
/// package falls back to an `esptool` on PATH.
#[test]
fn deploy_esptool_requires_v5_cli() {
use super::packages::speaks_v5_cli;
use std::path::Path;
assert!(speaks_v5_cli(Path::new(
"/cache/tasmota-esptool/5.1.0/esptool-linux-amd64/esptool"
)));
assert!(speaks_v5_cli(Path::new("C:/cache/esptool/esptool.exe")));
assert!(!speaks_v5_cli(Path::new(
"/cache/tool-esptoolpy/fbuild-tools/esptool/bin/esptool.py"
)));
assert!(!speaks_v5_cli(Path::new(
"C:/cache/fbuild-tools/esptool/Scripts/esptool.py.exe"
)));
}
27 changes: 25 additions & 2 deletions crates/fbuild-daemon/src/handlers/operations/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -611,8 +611,13 @@ pub async fn deploy(
&esptool_params,
false,
)
// esptool is spawned as a bare name; resolve it against
// the requesting CLI's PATH, not the daemon's (#1234).
// Spawn the esptool the build resolved (FBUILD_ESPTOOL_PATH,
// then the provisioned package), not whatever `esptool` is on
// PATH (#1616).
.with_esptool_path(resolve_deploy_esptool(&deploy_project, env_config).await)
// Without a resolved esptool it is spawned as a bare name;
// resolve that against the requesting CLI's PATH, not the
// daemon's (#1234).
.with_caller_path(deploy_caller_path.clone());
let deployer = if let Some(baud) = baud_override {
deployer.with_baud_rate(&baud.to_string())
Expand Down Expand Up @@ -1440,6 +1445,24 @@ fn deployer_caller_path(req: &DeployRequest) -> Option<String> {
req.caller_path.clone()
}

/// The esptool the build resolved for this env (FastLED/fbuild#1616), or
/// `None` to keep the bare `esptool` PATH lookup. A resolution failure is
/// logged rather than failing the deploy: an `esptool` on PATH still works.
async fn resolve_deploy_esptool(
project_dir: &std::path::Path,
env_config: &std::collections::HashMap<String, String>,
) -> Option<fbuild_core::path::NormalizedPath> {
match fbuild_build::esp32::orchestrator::resolve_deploy_esptool(project_dir, Some(env_config))
.await
{
Ok(path) => path,
Err(e) => {
tracing::warn!("could not resolve the build's esptool ({e}); using `esptool` on PATH");
None
}
}
}

fn deploy_error_response(
request_id: String,
error: &fbuild_core::FbuildError,
Expand Down
38 changes: 28 additions & 10 deletions crates/fbuild-deploy/src/esp32/deployer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

use std::path::Path;

use fbuild_core::path::NormalizedPath;

use fbuild_core::Result;
use fbuild_core::subprocess::run_command;

Expand Down Expand Up @@ -65,11 +67,16 @@ pub struct Esp32Deployer {
/// Feature-gated — see `use_native_verify`.
#[cfg(feature = "espflash-native")]
pub(super) use_native_write: bool,
/// The requesting CLI's PATH. `esptool` is spawned as a bare name
/// (see [`Esp32Deployer::find_esptool`]), so without this it would
/// resolve against the long-lived daemon's boot-time PATH
/// The requesting CLI's PATH. Without a resolved `esptool_path`,
/// `esptool` is spawned as a bare name (see
/// [`Esp32Deployer::find_esptool`]), so without this it would resolve
/// against the long-lived daemon's boot-time PATH
/// (FastLED/fbuild#1234). `None` keeps legacy daemon-env behavior.
pub(super) caller_path: Option<String>,
/// The esptool the build resolved (`FBUILD_ESPTOOL_PATH` or the
/// provisioned `tool-esptoolpy` package). `None` falls back to a bare
/// `esptool` PATH lookup (FastLED/fbuild#1616).
pub(super) esptool_path: Option<NormalizedPath>,
}

#[cfg(feature = "espflash-native")]
Expand Down Expand Up @@ -152,6 +159,7 @@ impl Esp32Deployer {
#[cfg(feature = "espflash-native")]
use_native_write: false,
caller_path: None,
esptool_path: None,
}
}

Expand Down Expand Up @@ -240,11 +248,21 @@ impl Esp32Deployer {
self
}

/// Find the esptool executable.
///
/// Uses standalone `esptool` command (available when esptool is pip-installed).
pub(super) fn find_esptool() -> Vec<String> {
vec!["esptool".to_string()]
/// Use the esptool the build resolved instead of a bare `esptool`
/// PATH lookup (FastLED/fbuild#1616). `None` keeps the PATH lookup.
#[must_use]
pub fn with_esptool_path(mut self, esptool_path: Option<NormalizedPath>) -> Self {
self.esptool_path = esptool_path;
self
}

/// The esptool program: the resolved path when one was supplied,
/// otherwise the bare `esptool` command found on PATH.
pub(super) fn find_esptool(&self) -> Vec<String> {
match &self.esptool_path {
Some(path) => vec![path.to_string_lossy().into_owned()],
None => vec!["esptool".to_string()],
}
}

/// Build the `esptool verify-flash` command line that this deployer
Expand All @@ -262,7 +280,7 @@ impl Esp32Deployer {
let bootloader_path = build_dir.join("bootloader.bin");
let partitions_path = build_dir.join("partitions.bin");

let mut args = Self::find_esptool();
let mut args = self.find_esptool();
args.extend([
"--chip".to_string(),
self.chip.clone(),
Expand Down Expand Up @@ -654,7 +672,7 @@ impl Esp32Deployer {
let bootloader_path = build_dir.join("bootloader.bin");
let partitions_path = build_dir.join("partitions.bin");

let mut args = Self::find_esptool();
let mut args = self.find_esptool();
args.extend([
"--chip".to_string(),
self.chip.clone(),
Expand Down
57 changes: 56 additions & 1 deletion crates/fbuild-deploy/src/esp32/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ fn with_caller_path_stores_the_requesting_cli_path() {

// The esptool program is a bare name, so the deploy-time spawn gets
// the [("PATH", ...)] overlay from the stored caller PATH.
let esptool = Esp32Deployer::find_esptool();
let esptool = deployer.find_esptool();
assert_eq!(
fbuild_core::subprocess::bare_name_path_overlay(
&esptool[0],
Expand All @@ -75,6 +75,61 @@ fn with_caller_path_stores_the_requesting_cli_path() {
);
}

/// FastLED/fbuild#1616: a resolved esptool path (the one the build
/// provisioned, or `FBUILD_ESPTOOL_PATH`) becomes argv[0] of both the
/// verify-flash and write-flash invocations, and an absolute path does not
/// get the caller-PATH overlay meant for bare names.
#[test]
fn with_esptool_path_is_argv0_for_verify_and_write() {
let tmp = tempfile::TempDir::new().unwrap();
let fw = tmp.path().join("firmware.bin");
std::fs::write(&fw, b"FIRM").unwrap();
let esptool = tmp.path().join("tools").join("esptool");

let params = test_esptool_params();
let deployer = Esp32Deployer::new(
"esp32c6", "460800", "0x0", "0x8000", "0x10000", &params, false,
)
.with_caller_path(Some("/opt/tools/bin".to_string()))
.with_esptool_path(Some(fbuild_core::path::NormalizedPath::from(
esptool.clone(),
)));

let expected = esptool.to_string_lossy().to_string();
let verify = deployer.build_verify_flash_args(&fw, "COM13");
assert_eq!(verify[0], expected);
assert!(verify.contains(&"verify-flash".to_string()));
let write = deployer.build_write_flash_args(&fw, "COM13", None);
assert_eq!(write[0], expected);
assert!(write.contains(&"write-flash".to_string()));

assert_eq!(
fbuild_core::subprocess::bare_name_path_overlay(&write[0], deployer.caller_path.as_deref()),
None
);
}

/// FastLED/fbuild#1616: with no resolved path the deployer keeps the bare
/// `esptool` PATH lookup (and so the #1234 caller-PATH overlay).
#[test]
fn without_esptool_path_falls_back_to_bare_esptool() {
let tmp = tempfile::TempDir::new().unwrap();
let fw = tmp.path().join("firmware.bin");
std::fs::write(&fw, b"FIRM").unwrap();

let params = test_esptool_params();
let deployer = Esp32Deployer::new(
"esp32c6", "460800", "0x0", "0x8000", "0x10000", &params, false,
)
.with_esptool_path(None);

assert_eq!(deployer.build_verify_flash_args(&fw, "COM13")[0], "esptool");
assert_eq!(
deployer.build_write_flash_args(&fw, "COM13", None)[0],
"esptool"
);
}

#[test]
fn qemu_flash_size_resolution_accepts_supported_sizes() {
let mut board = fbuild_test_support::board_for_test("esp32-s3-devkitc-1");
Expand Down
Loading