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
5 changes: 5 additions & 0 deletions crates/fbuild-build-engine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,11 @@ pub struct BuildParams {
/// actually emitted. This unblocks per-symbol bloat analysis on
/// over-budget builds. See FastLED/fbuild#594.
pub bloat_analysis: bool,
/// The CLI caller's PATH, forwarded per request so bare-name tool
/// spawns (esptool, python, objcopy) resolve against the caller's
/// environment instead of the daemon's spawn-time PATH
/// (FastLED/fbuild#1219). `None` = legacy behavior (daemon env).
pub caller_path: Option<String>,
}

/// Trait for platform-specific build orchestrators.
Expand Down
26 changes: 25 additions & 1 deletion crates/fbuild-build-engine/src/linker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,30 @@ impl LinkerBase {
output_format: &str,
remove_sections: &[String],
tool_label: &str,
) -> Result<PathBuf> {
Self::objcopy_firmware_with_env(
objcopy_path,
elf_path,
output_dir,
output_format,
remove_sections,
tool_label,
None,
)
.await
}

/// As [`Self::objcopy_firmware`] but with explicit child env vars — used
/// to run a bare-name objcopy under the CLI caller's PATH instead of the
/// daemon's spawn-time PATH (FastLED/fbuild#1219). `None` = daemon env.
pub async fn objcopy_firmware_with_env(
objcopy_path: &Path,
elf_path: &Path,
output_dir: &Path,
output_format: &str,
remove_sections: &[String],
tool_label: &str,
env: Option<&[(&str, &str)]>,
) -> Result<PathBuf> {
use fbuild_core::subprocess::run_command;

Expand Down Expand Up @@ -494,7 +518,7 @@ impl LinkerBase {
let result = run_command(
&args_ref,
None,
None,
env,
Some(std::time::Duration::from_secs(60)),
)
.await?;
Expand Down
10 changes: 7 additions & 3 deletions crates/fbuild-build-engine/src/pipeline/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,13 @@ impl BuildContext {
let pio_overrides = fbuild_config::PioEnvOverrides::from_map(params.pio_env.clone());
let config =
fbuild_config::PlatformIOConfig::from_path_with_overrides(&ini_path, pio_overrides)?;
let overlay =
crate::script_runtime::resolve_extra_script_overlay(project_dir, env_name, &config)
.await?;
let overlay = crate::script_runtime::resolve_extra_script_overlay_with_path(
project_dir,
env_name,
&config,
params.caller_path.as_deref(),
)
.await?;
if let Some(p) = perf.as_mut() {
p.record("config-parse", t0.elapsed());
}
Expand Down
31 changes: 28 additions & 3 deletions crates/fbuild-build-engine/src/script_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,19 @@ pub async fn resolve_extra_script_overlay(
project_dir: &Path,
env_name: &str,
config: &fbuild_config::PlatformIOConfig,
) -> fbuild_core::Result<BuildOverlay> {
resolve_extra_script_overlay_with_path(project_dir, env_name, config, None).await
}

/// As [`resolve_extra_script_overlay`] but resolving (and running) the
/// Python interpreter against the CLI caller's PATH instead of the
/// daemon's spawn-time PATH (FastLED/fbuild#1219). `None` = legacy
/// behavior (daemon env).
pub async fn resolve_extra_script_overlay_with_path(
project_dir: &Path,
env_name: &str,
config: &fbuild_config::PlatformIOConfig,
caller_path: Option<&str>,
) -> fbuild_core::Result<BuildOverlay> {
let extra_scripts = config.get_extra_scripts(env_name)?;
if extra_scripts.is_empty() {
Expand All @@ -61,7 +74,7 @@ pub async fn resolve_extra_script_overlay(
.to_string(),
};

let python = find_python().await.ok_or_else(|| {
let python = find_python_with_path(caller_path).await.ok_or_else(|| {
fbuild_core::FbuildError::BuildFailed(
"extra_scripts detected but no Python interpreter was found; \
install Python or use --platformio"
Expand Down Expand Up @@ -116,10 +129,14 @@ pub async fn resolve_extra_script_overlay(
// — config-time evaluation, never legitimately long. Bound to 60s
// so a buggy or hostile script cannot wedge the daemon's build
// pipeline indefinitely.
// FastLED/fbuild#1219: run the interpreter under the caller's PATH
// too, so the script's own resolution (imports, subprocesses) sees
// the same environment the probe did.
let env: Option<Vec<(&str, &str)>> = caller_path.map(|p| vec![("PATH", p)]);
let output = fbuild_core::subprocess::run_command(
&argv,
Some(project_dir),
None,
env.as_deref(),
Some(std::time::Duration::from_secs(60)),
)
.await
Expand Down Expand Up @@ -299,12 +316,20 @@ fn libs_to_flags(
}

pub async fn find_python() -> Option<Vec<String>> {
find_python_with_path(None).await
}

/// As [`find_python`] but probing against the CLI caller's PATH instead of
/// the daemon's spawn-time PATH (FastLED/fbuild#1219). The caller PATH
/// *replaces* the inherited one for the probe; `None` = legacy behavior.
pub async fn find_python_with_path(caller_path: Option<&str>) -> Option<Vec<String>> {
let candidates: &[&[&str]] = if cfg!(windows) {
&[&["python"], &["py", "-3"]]
} else {
&[&["python3"], &["python"]]
};

let env: Option<Vec<(&str, &str)>> = caller_path.map(|p| vec![("PATH", p)]);
for candidate in candidates {
let mut argv: Vec<&str> = candidate.to_vec();
argv.push("--version");
Expand All @@ -313,7 +338,7 @@ pub async fn find_python() -> Option<Vec<String>> {
if let Ok(output) = fbuild_core::subprocess::run_command(
&argv,
None,
None,
env.as_deref(),
Some(std::time::Duration::from_secs(5)),
)
.await
Expand Down
41 changes: 41 additions & 0 deletions crates/fbuild-build-engine/src/script_runtime_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -578,3 +578,44 @@ env.Append(CPPDEFINES=[\"LFS_OK\"])
"{overlay:?}"
);
}

// ---- caller-PATH threading (FastLED/fbuild#1219) ----------------------

/// Load-bearing regression test for FastLED/fbuild#1219: the caller PATH
/// passed to `find_python_with_path` must REPLACE the inherited process
/// PATH for the probe. An empty temp dir as PATH must make resolution
/// fail even though this process's own PATH can resolve python.
#[tokio::test]
async fn test_find_python_with_path_replaces_inherited_path() {
// Gate on python being resolvable at all — machines without python
// can't distinguish "override worked" from "python missing".
if find_python().await.is_none() {
return;
}
// The `py` launcher installs to the Windows directory, which is on the
// OS fallback exe-search path that `Command::new` consults even when
// the child PATH is overridden — an empty PATH cannot hide it, and
// that is exe resolution, not a PATH leak. Skip on such machines; the
// strict assertion still runs on Unix CI and py-less Windows.
#[cfg(windows)]
{
let windir = std::env::var("WINDIR").unwrap_or_else(|_| r"C:\Windows".to_string());
if std::path::Path::new(&windir).join("py.exe").exists() {
return;
}
}
let empty = tempfile::tempdir().unwrap();
let bogus_path = empty.path().to_string_lossy().to_string();
assert!(
find_python_with_path(Some(&bogus_path)).await.is_none(),
"an empty-dir PATH override must make python resolution fail — \
the probe leaked the daemon-process PATH"
);
}

/// `find_python_with_path(None)` is the legacy path: identical result to
/// `find_python()` (both resolve against the inherited process env).
#[tokio::test]
async fn test_find_python_with_path_none_matches_find_python() {
assert_eq!(find_python_with_path(None).await, find_python().await);
}
24 changes: 23 additions & 1 deletion crates/fbuild-build-esp/src/esp32/esp32_linker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ pub struct Esp32Linker {
/// falls back to an `esptool` on PATH. See FastLED/fbuild#954.
esptool_bin: Option<PathBuf>,
verbose: bool,
/// The CLI caller's PATH, so the bare-`esptool` fallback resolves
/// against the caller's environment instead of the daemon's
/// spawn-time PATH (FastLED/fbuild#1219). `None` = daemon env.
caller_path: Option<String>,
}

impl Esp32Linker {
Expand Down Expand Up @@ -189,9 +193,18 @@ impl Esp32Linker {
max_ram,
esptool_bin,
verbose,
caller_path: None,
}
}

/// Builder: forward the CLI caller's PATH to the esptool `elf2image`
/// spawn so the bare-name fallback resolves against the caller's
/// environment (FastLED/fbuild#1219).
pub fn with_caller_path(mut self, caller_path: Option<String>) -> Self {
self.caller_path = caller_path;
self
}

/// Build all linker flags: SDK flags + profile-specific flags.
fn linker_flags(&self) -> Vec<String> {
let mut flags = Vec::new();
Expand Down Expand Up @@ -472,7 +485,16 @@ impl Linker for Esp32Linker {

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

match run_command(&args, None, None, Some(std::time::Duration::from_secs(60))).await {
// FastLED/fbuild#1219: resolve/run esptool under the caller's PATH.
let env: Option<Vec<(&str, &str)>> = self.caller_path.as_deref().map(|p| vec![("PATH", p)]);
match run_command(
&args,
None,
env.as_deref(),
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 Down
11 changes: 8 additions & 3 deletions crates/fbuild-build-esp/src/esp32/orchestrator/boot_artifacts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,13 @@ pub(super) async fn prepare_boot_artifacts(
mcu_config: &Esp32McuConfig,
flash_freq: &str,
esptool_bin: Option<&Path>,
caller_path: Option<&str>,
perf: &mut crate::perf_log::PerfTimer,
) -> Result<()> {
// FastLED/fbuild#1219: bare-name tool spawns below (`esptool` fallback,
// python interpreter) resolve against the CLI caller's PATH when it was
// forwarded; `None` keeps the daemon's spawn-time env.
let spawn_env: Option<Vec<(&str, &str)>> = caller_path.map(|p| vec![("PATH", p)]);
let boot_artifacts_started = Instant::now();
perf.checkpoint("boot-artifacts-start");
// SDK directory selector matching the chip's ROM revision (e.g. `esp32p4_es`
Expand Down Expand Up @@ -94,7 +99,7 @@ pub(super) async fn prepare_boot_artifacts(
match fbuild_core::subprocess::run_command(
&args,
None,
None,
spawn_env.as_deref(),
Some(std::time::Duration::from_secs(60)),
)
.await
Expand Down Expand Up @@ -144,7 +149,7 @@ pub(super) async fn prepare_boot_artifacts(
// `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()
let python = crate::script_runtime::find_python_with_path(caller_path)
.await
.unwrap_or_else(|| vec!["python".to_string()]);
let mut args: Vec<&str> = python.iter().map(|s| s.as_str()).collect();
Expand All @@ -157,7 +162,7 @@ pub(super) async fn prepare_boot_artifacts(
match fbuild_core::subprocess::run_command(
&args,
None,
None,
spawn_env.as_deref(),
Some(std::time::Duration::from_secs(10)),
)
.await
Expand Down
4 changes: 3 additions & 1 deletion crates/fbuild-build-esp/src/esp32/orchestrator/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -814,7 +814,8 @@ impl BuildOrchestrator for Esp32Orchestrator {
ctx.board.max_ram,
esptool_bin.clone().map(|path| path.into_path_buf()),
params.verbose,
);
)
.with_caller_path(params.caller_path.clone());

let link_result = {
let _g = perf.phase("link-convert-size");
Expand Down Expand Up @@ -842,6 +843,7 @@ impl BuildOrchestrator for Esp32Orchestrator {
&mcu_config,
&flash_freq,
esptool_bin.as_ref().map(|path| path.as_path()),
params.caller_path.as_deref(),
&mut perf,
)
.await?;
Expand Down
24 changes: 23 additions & 1 deletion crates/fbuild-build-esp/src/esp8266/esp8266_linker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ pub struct Esp8266Linker {
max_flash: Option<u64>,
max_ram: Option<u64>,
verbose: bool,
/// The CLI caller's PATH, so the bare-`esptool` spawn resolves against
/// the caller's environment instead of the daemon's spawn-time PATH
/// (FastLED/fbuild#1219). `None` = daemon env.
caller_path: Option<String>,
}

impl Esp8266Linker {
Expand Down Expand Up @@ -76,8 +80,17 @@ impl Esp8266Linker {
max_flash,
max_ram,
verbose,
caller_path: None,
}
}

/// Builder: forward the CLI caller's PATH to the esptool `elf2image`
/// spawn so the bare-name invocation resolves against the caller's
/// environment (FastLED/fbuild#1219).
pub fn with_caller_path(mut self, caller_path: Option<String>) -> Self {
self.caller_path = caller_path;
self
}
}

impl Esp8266Linker {
Expand Down Expand Up @@ -275,7 +288,16 @@ impl Linker for Esp8266Linker {
&bin_str,
];

match run_command(&args, None, None, Some(std::time::Duration::from_secs(30))).await {
// FastLED/fbuild#1219: resolve/run esptool under the caller's PATH.
let env: Option<Vec<(&str, &str)>> = self.caller_path.as_deref().map(|p| vec![("PATH", p)]);
match run_command(
&args,
None,
env.as_deref(),
Some(std::time::Duration::from_secs(30)),
)
.await
{
Ok(result) if result.success() => {
// Verify the output file was actually created with content.
// ESP8266 esptool may create segmented files (firmware.bin-0x00000.bin)
Expand Down
3 changes: 2 additions & 1 deletion crates/fbuild-build-esp/src/esp8266/orchestrator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,8 @@ impl BuildOrchestrator for Esp8266Orchestrator {
ctx.board.max_flash,
ctx.board.max_ram,
params.verbose,
);
)
.with_caller_path(params.caller_path.clone());

// 8. Build LibraryBuildEnv for project-as-library compilation
let gcc_path = toolchain.get_gcc_path();
Expand Down
Loading
Loading