Split out of #1217, which was the esptool-specific symptom. This is the underlying class.
Problem
Tool resolution for bare-name spawns is bound to whichever environment happened to spawn the daemon, and that daemon then lives for up to 12 hours. Two builds with byte-identical inputs can resolve different toolchains depending only on which shell started the daemon and when.
The binding happens once, at spawn — crates/fbuild-cli/src/daemon_client.rs:1038:
cmd.env_clear().envs(baseline);
if let Some(path) = launcher_path(std::env::vars_os()) {
cmd.env("PATH", path);
}
// then re-adds only vars starting with FBUILD_
launcher_path (daemon_client.rs:1006) correctly forwards the launching CLI's PATH. But builds execute inside the daemon, so every subsequent CLI invocation that connects to an already-running daemon inherits the first launcher's PATH, not its own. Nothing re-syncs it per request.
The window is long — crates/fbuild-daemon/src/context.rs:108:
/// Fallback idle timeout: daemon shuts down after 12 hours regardless.
pub const IDLE_TIMEOUT: Duration = Duration::from_secs(43200);
What actually resolves from PATH inside the daemon
Not a corner case — these are all bare-name spawns with no absolute path:
| Site |
Spawns |
Notes |
fbuild-build-esp/src/esp32/esp32_linker.rs:73 |
esptool |
fallback when provisioning returns None — the #1217 outage |
fbuild-build-esp/src/esp8266/esp8266_linker.rs:263 |
esptool |
unconditional; no provisioned path attempted at all |
fbuild-build-engine/src/script_runtime.rs:303-305 |
python3 / python / py -3 |
find_python() probes candidates in PATH order |
fbuild-build-esp/src/esp32/orchestrator/boot_artifacts.rs:149 |
python |
fallback |
fbuild-build-mcu/src/ch32v/ch32v_linker.rs:238 |
objcopy |
|
find_python() is the one I'd weight highest. Which interpreter runs PlatformIO build scripts — a project venv's or the system's — currently depends on daemon spawn lineage. That is a silent correctness hazard, not just an availability one: the build succeeds, it just ran under an interpreter nobody selected.
Why this is worth fixing beyond #1217
#1217's fix (PR #1218) makes esptool provisioning succeed, so the PATH fallback stops being reached for ESP32. It does not make the fallback correct. Any future URL-shape change, a provisioning 404, an unsupported host tag — all still land on a PATH lookup against a possibly-stale environment, and fail minutes into a build with a misleading message.
It also makes builds non-reproducible in a way that is genuinely hard to debug: the same command, same repo, same inputs, different result, with the only variable being whether a daemon from this morning is still alive.
Suggested direction
- Forward the caller's PATH (or resolved tool paths) per request, not only at spawn. The build request already carries project context; the caller's execution environment is arguably part of it.
- Failing that, detect drift — if a request arrives whose caller PATH differs from the daemon's, either re-exec, refuse with a clear message, or at minimum log it loudly. Silent divergence is the worst of the three.
- Consider whether bare-name spawns should exist at all in the build path. Every one of them is a place where the daemon's environment silently becomes part of the build inputs.
Repro sketch
- Start a daemon from a shell without a project venv on PATH (any
fbuild invocation does this).
- Activate the venv in a second shell; confirm
which esptool / which python3 resolve inside it.
- Run a build from the second shell.
- The daemon still resolves the first shell's PATH; the venv tools are invisible. Kill the daemon and re-run to see the result change with no input change.
This is the mechanism behind the FastLED CI outage in #1217: GitHub Actions installs esptool into the project venv (esptool>=5.0.2), but the daemon's PATH did not include it.
Split out of #1217, which was the esptool-specific symptom. This is the underlying class.
Problem
Tool resolution for bare-name spawns is bound to whichever environment happened to spawn the daemon, and that daemon then lives for up to 12 hours. Two builds with byte-identical inputs can resolve different toolchains depending only on which shell started the daemon and when.
The binding happens once, at spawn —
crates/fbuild-cli/src/daemon_client.rs:1038:launcher_path(daemon_client.rs:1006) correctly forwards the launching CLI's PATH. But builds execute inside the daemon, so every subsequent CLI invocation that connects to an already-running daemon inherits the first launcher's PATH, not its own. Nothing re-syncs it per request.The window is long —
crates/fbuild-daemon/src/context.rs:108:What actually resolves from PATH inside the daemon
Not a corner case — these are all bare-name spawns with no absolute path:
fbuild-build-esp/src/esp32/esp32_linker.rs:73esptoolNone— the #1217 outagefbuild-build-esp/src/esp8266/esp8266_linker.rs:263esptoolfbuild-build-engine/src/script_runtime.rs:303-305python3/python/py -3find_python()probes candidates in PATH orderfbuild-build-esp/src/esp32/orchestrator/boot_artifacts.rs:149pythonfbuild-build-mcu/src/ch32v/ch32v_linker.rs:238objcopyfind_python()is the one I'd weight highest. Which interpreter runs PlatformIO build scripts — a project venv's or the system's — currently depends on daemon spawn lineage. That is a silent correctness hazard, not just an availability one: the build succeeds, it just ran under an interpreter nobody selected.Why this is worth fixing beyond #1217
#1217's fix (PR #1218) makes esptool provisioning succeed, so the PATH fallback stops being reached for ESP32. It does not make the fallback correct. Any future URL-shape change, a provisioning 404, an unsupported host tag — all still land on a PATH lookup against a possibly-stale environment, and fail minutes into a build with a misleading message.
It also makes builds non-reproducible in a way that is genuinely hard to debug: the same command, same repo, same inputs, different result, with the only variable being whether a daemon from this morning is still alive.
Suggested direction
Repro sketch
fbuildinvocation does this).which esptool/which python3resolve inside it.This is the mechanism behind the FastLED CI outage in #1217: GitHub Actions installs
esptoolinto the project venv (esptool>=5.0.2), but the daemon's PATH did not include it.