Skip to content

daemon resolves a relative project_dir against its OWN cwd — builds/deploys the wrong project and reports success #1415

Description

@zackees

Found while profiling for #1414. This is a correctness bug, not a performance one: fbuild builds a different project than the one you are standing in, reports success, and prints artifact paths that do not exist.

Reproduction

Two sibling project directories, projA and projB, same platformio.ini. Build A, then immediately build B while A's daemon is still alive:

$ cd projA && fbuild build
build succeeded in 7.0s

$ cd ../projB && pwd
.../projB
$ ls -a
.  ..  lib  platformio.ini  src          # no .fbuild

$ fbuild build
No-op fingerprint matched; reusing existing ESP32 artifacts.
Flash: 343.34KB / 16.00MB (2.1%)
Artifact: ./.fbuild/build/esp32_esp32_esp32s3_opi/release/firmware.elf (6.14MB)
build succeeded in 0.5s

$ find . -name '*.o' | wc -l
0                                        # still no .fbuild directory

projB was never built. The daemon built projA again, took its fast path, and reported projA's sizes and artifacts as ./.fbuild/... relative to a directory that has no such file.

Cause

The CLI sends the project as a relative path plus a caller_cwd:

// crates/fbuild-cli/src/cli/build.rs:98
let (caller_pid, caller_cwd) = daemon_client::caller_info();

The daemon resolves every other client path against caller_cwd via resolve_client_pathbuild_dir_override, symbol_analysis_path, output_dir. The project itself is the one that is not:

// crates/fbuild-daemon/src/handlers/operations/build.rs:148
let project_dir = PathBuf::from(&req.project_dir);

So "." resolves against the daemon's cwd. The daemon inherits its cwd from whichever CLI invocation spawned it (--spawner-cwd=...), so every request that arrives while that daemon is alive is interpreted relative to the first project.

ps during the repro above:

fbuild-daemon --spawner-cwd=/…/projA

and the daemon log identifies the project by the unresolved string:

INFO fbuild_daemon::handlers::operations::build: waiting for project lock on .

The project lock is therefore also taken on "." — two different projects share one lock, and neither is the path the user asked for.

Why it usually hides

The daemon self-evicts after ~30s idle. Work on one project at a time, or leave a gap between projects, and each build gets a daemon whose cwd happens to match — which is why this has not shown up in CI or normal single-project use. It reproduces reliably when a second project is built within the idle window.

Impact

  • fbuild build in project B silently produces nothing and claims success.
  • Worse for deploy: deploy.rs sends caller_cwd the same way, so a deploy from project B flashes project A's firmware while reporting project B's build as successful. That is a wrong-firmware-on-hardware bug.
  • Any tool that builds several sketches in a loop (FastLED's example sweeps, Design Studio for FastLED) is in the failure window by construction.

Fix sketch

Route req.project_dir through the same resolve_client_path(p, req.caller_cwd.as_deref(), ...) the other client paths use, and log the resolved absolute path rather than the raw string so the lock and the log name the real project. A regression test should assert that a request carrying project_dir: "." plus a caller_cwd pointing elsewhere builds the caller_cwd project, with the daemon's own cwd set to a third directory.

Worth checking deploy, monitor, and the emulator handlers for the same unresolved project_dir before closing.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions