Skip to content

fix(esp32): surface daemon errors + provision platform.json helper toolchains - #402

Merged
zackees merged 2 commits into
mainfrom
fix/esp32-diagnostics-401
Jun 3, 2026
Merged

fix(esp32): surface daemon errors + provision platform.json helper toolchains#402
zackees merged 2 commits into
mainfrom
fix/esp32-diagnostics-401

Conversation

@zackees

@zackeeszackees commented Jun 3, 2026

Copy link
Copy Markdown
Member

Closes#401.

Summary

Two complementary fixes for ESP toolchain handling raised in #401:

Daemon streaming surfaces a usable error (was: stream error: error decoding response body)

The streaming /api/build handler runs the build in a spawned task that emits a terminal NDJSON result event when it finishes. If that task panicked or returned early, no terminal event was emitted, the response body stream closed mid-frame, and the CLI surfaced reqwest's underlying stream error: error decoding response body — an opaque message that gave the user no actionable diagnostic.

  • New StreamTerminationGuard in fbuild-daemon wraps the spawn body. On Drop without an explicit mark_completed(), it enqueues a fallback terminal result event pointing the user at ~/.fbuild/daemon/daemon.log. The happy path calls mark_completed() after the real result event so no duplicate is emitted.
  • fbuild-cli's build_streaming wraps the reqwest chunk error with an actionable hint instead of the raw message.

ESP32 platform.json helper toolchains are now provisioned

Esp32Platform::get_toolchain_metadata_url previously returned only the MCU-primary toolchain (toolchain-xtensa-esp-elf OR toolchain-riscv32-esp). pioarduino platform-espressif32@54.03.20 lists both on the same platform.json — the RISC-V toolchain is needed on ESP32-S3 because the chip has a RISC-V ULP coprocessor.

  • New Esp32Platform::enumerate_packages() returns (name, version_url) for every entry in packages, sorted by name.
  • resolve_pioarduino_packages now opportunistically resolves every additional toolchain-* listed in platform.json after the primary one. Resolution failures log a warning but don't fail the build, because not every sketch references every helper (most FastLED sketches don't compile ULP code).
  • Version-compatibility policy: fbuild trusts platform.json's pinned metadata URL (option b from the issue), matching PlatformIO behavior and avoiding ad-hoc version-compatibility decisions.

Tests added

esp32_platform (5 new unit tests, all green locally):

  • test_get_toolchain_metadata_url_xtensa_pioarduino_54_03_20 — primary Xtensa URL resolves.
  • test_get_riscv_helper_toolchain_for_esp32s3_pioarduino_54_03_20 — RISC-V helper URL is discoverable on an ESP32-S3 build.
  • test_enumerate_packages_returns_all_entries — sorted full list including helpers + esptoolpy.
  • test_enumerate_packages_errors_when_section_missing — clean error on malformed manifest.
  • Plus the existing pre-fixture tests still pass.

fbuild-daemon::handlers::operations::build (2 new unit tests):

  • termination_guard_emits_fallback_on_drop_when_not_completed — Drop enqueues a fallback result event with an actionable message.
  • termination_guard_silent_on_drop_when_completedmark_completed() disarms the guard so no duplicate is emitted.

All four touched crates lint clean (soldr cargo clippy --workspace --all-targets -- -D warnings).

Acceptance criteria mapping (from #401)

  • ✅ Better diagnostics on the streaming build path — guard + CLI message wrap.
  • ✅ Audit pioarduino platform.json package consumption — enumerate_packages + opportunistic provisioning.
  • ✅ Version-compatibility policy — trust platform.json pinned versions; documented in code.
  • ✅ Regression coverage — unit tests against canned platform.json 54.03.20 fragment + Drop-impl tests.
  • Out of scope for this PR: end-to-end integration test that downloads pioarduino 54.03.20 and runs a real ESP32-S3 build with ULP code (would need network + ~500 MB toolchain download in CI). The unit tests cover the resolution logic; the integration test fixture (tests/platform/esp32s3/) continues to use the stock platform = espressif32 channel as before.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added support for provisioning additional helper toolchain packages for ESP32 builds.
  • Bug Fixes

    • Improved error messages when daemon connection is lost mid-build, including guidance to check logs.
    • Enhanced build streaming to ensure completion events are always delivered, even if the background task fails unexpectedly.
  • Tests

    • Expanded test coverage for package resolution and build operations.

…olchains
Addresses fbuild#401.
Daemon streaming (fbuild-daemon, fbuild-cli):
- StreamTerminationGuard wraps the spawned build task with a Drop impl
that enqueues a fallback terminal `result` NDJSON event if the task
panics or returns without marking completion. Without this, the body
stream closes mid-frame and the CLI reports the opaque reqwest error
`stream error: error decoding response body` with no clue what failed.
- CLI now wraps the underlying stream error with an actionable message
pointing at ~/.fbuild/daemon/daemon.log instead of bubbling the raw
reqwest text.
ESP32 platform.json package enumeration (fbuild-packages, fbuild-build):
- Esp32Platform::enumerate_packages() returns (name, version_url) pairs
for every entry in the `packages` section, sorted by name.
- The pioarduino orchestrator now opportunistically provisions every
toolchain-* helper listed in platform.json that isn't the MCU-primary
toolchain. Resolution failures are warned, never fatal, because not
every build references every helper (e.g. only sketches with ULP code
on ESP32-S3 actually need toolchain-riscv32-esp).
- Version-compatibility policy: we always trust platform.json's pinned
metadata URL rather than reuse a newer cached toolchain. This matches
PlatformIO behavior and avoids ad-hoc version-compatibility decisions.
Tests:
- esp32_platform: 5 new unit tests against a pioarduino 54.03.20
packages-section fixture, verifying both the MCU-primary Xtensa URL
and the RISC-V helper URL for ESP32-S3 are resolvable, plus that
enumerate_packages returns the full sorted list and errors cleanly
when `packages` is missing.
- build handler: 2 new tests for StreamTerminationGuard — Drop emits a
fallback `result` event when not marked completed, and stays silent
after mark_completed().
@coderabbitai

coderabbitaiBot commented Jun 3, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@zackees, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 42 minutes and 9 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 711e92b2-1020-4d22-acf6-77e4dc44245a

📥 Commits

Reviewing files that changed from the base of the PR and between 9620ae4 and 7b48748.

📒 Files selected for processing (3)
  • crates/fbuild-build/src/esp32/orchestrator/packages.rs
  • crates/fbuild-cli/src/daemon_client.rs
  • crates/fbuild-packages/src/library/esp32_platform.rs
📝 Walkthrough

Walkthrough

This PR provisions helper ESP toolchains discovered in platform.json and hardens the streaming build path. The orchestrator now enumerates all platform packages, opportunistically caches secondary toolchains (e.g., RISC-V for ESP32-S3), and guarantees terminal result events even on daemon crashes. Client errors now surface actionable messages instead of stream decode failures.

Changes

ESP Toolchain Resolution and Build Streaming

Layer / File(s)Summary
Platform package enumeration
crates/fbuild-packages/src/library/esp32_platform.rs
Esp32Platform::enumerate_packages reads platform.json, extracts all packages as (name, version_url) pairs, sorts deterministically, and errors when packages are missing. Tests cover Xtensa/RISC-V toolchain URLs and edge cases.
Helper toolchain provisioning in orchestrator
crates/fbuild-build/src/esp32/orchestrator/packages.rs
resolve_pioarduino_packages calls provision_helper_toolchains to discover and synchronously cache all non-primary toolchain-* packages. Resolution failures log at warn/info but never fail the caller, ensuring secondary toolchains (e.g., RISC-V helper) are cached when available.
StreamTerminationGuard for guaranteed result events
crates/fbuild-daemon/src/handlers/operations/build.rs
Introduces StreamTerminationGuard to ensure a terminal NDJSON "result" event is sent even if the streaming task panics. The guard is armed at spawn, marked completed after the final successful result event, and emits a fallback failure on drop if not completed. Unit tests verify both paths.
Improved streaming error diagnostics
crates/fbuild-cli/src/daemon_client.rs
DaemonClient::build_streaming maps mid-stream chunk errors to "lost connection to daemon mid-build" with guidance to check daemon logs, replacing the opaque "stream error: error decoding response body" message.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • #401: fbuild ESP toolchain resolution: surface better diagnostic + provision all platform.json packages — This PR directly implements the acceptance criteria: enumeration of all packages, provisioning of helper toolchains, guaranteed terminal result events on streaming builds, and improved error messages for streaming failures.

Poem

🐰 A toolkit finds helpers hiding in the JSON deep,
Packages gathered, caches to keep,
When demons crash mid-flight, no silent end—
A guard ensures the final message sends,
Errors bloom clear: not stream-decode despair! 🛠️

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe PR title accurately summarizes the two main changes: surfacing daemon errors and provisioning helper toolchains from platform.json for ESP32.
Linked Issues check✅ PassedAll linked issue #401 requirements are addressed: daemon error diagnostics improved, enumerate_packages implemented, helper toolchain provisioning added, streaming guard ensures terminal events, and comprehensive unit tests provided.
Out of Scope Changes check✅ PassedAll changes directly address issue #401 requirements. The PR explicitly notes end-to-end integration testing is out-of-scope due to large network requirements.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/esp32-diagnostics-401

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

coderabbitai[bot]
coderabbitaiBot previously requested changes Jun 3, 2026

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
crates/fbuild-packages/src/library/esp32_platform.rs (1)

119-152: 💤 Low value

Optional: factor out the shared platform.json read/parse.

enumerate_packages duplicates the read-string + serde_json::from_str + get("packages") boilerplate already present in get_package_url (Lines 83-108). A small private helper returning the parsed packages object (or the full Value) would remove the duplication and keep error messages consistent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/fbuild-packages/src/library/esp32_platform.rs` around lines 119 - 152,
Both enumerate_packages and get_package_url duplicate the platform.json
read/parse/lookup logic; create a small private helper (e.g.,
parse_platform_json or load_platform_packages) that reads platform.json,
deserializes to serde_json::Value (or returns the packages object), and returns
Result<serde_json::Map<String, Value>, FbuildError> so both enumerate_packages
and get_package_url call it; update enumerate_packages to call the new helper to
obtain the packages map and then map/sort entries, and update get_package_url to
use the same helper to fetch a package's version/url, preserving existing error
messages.
crates/fbuild-build/src/esp32/orchestrator/packages.rs (2)

94-95: 💤 Low value

Hoist Cache::new out of the loop.

fbuild_packages::Cache::new(project_dir) is reconstructed on every iteration but only depends on project_dir. Create it once before the loop.

♻️ Proposed change
+ let cache = fbuild_packages::Cache::new(project_dir);
for (name, metadata_url) in entries {
if name == primary || !name.starts_with("toolchain-") {
continue;
}
- let cache = fbuild_packages::Cache::new(project_dir);
let cache_dir = cache.toolchains_dir().join(&name);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/fbuild-build/src/esp32/orchestrator/packages.rs` around lines 94 - 95,
Hoist the creation of the package cache so it is not reconstructed each loop:
move the call to fbuild_packages::Cache::new(project_dir) out of the loop and
store it in a local variable (e.g. let cache =
fbuild_packages::Cache::new(project_dir)) before iterating; inside the loop
continue to call cache.toolchains_dir().join(&name) (using the existing name
variable) so only one Cache instance is created rather than recreating it on
every iteration.

90-118: ⚖️ Poor tradeoff

Synchronous helper-toolchain resolution runs on every ESP32 build.

provision_helper_toolchains calls resolve_toolchain_url_sync for each non-primary toolchain-* entry on the build path, even though the comment notes most FastLED sketches never compile ULP code. On a cold cache this adds blocking metadata round-trips (and potential download stalls) to builds that will never use the helper. Consider gating eager provisioning behind a flag/board-capability check, or moving it off the critical path (background/lazy on first ULP reference), so the common case isn't penalized.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/fbuild-build/src/esp32/orchestrator/packages.rs` around lines 90 -
118, The loop in provision_helper_toolchains synchronously calls
resolve_toolchain_url_sync for every non-primary toolchain entry, blocking ESP32
builds; change this to avoid eager blocking by either (A) gating the loop behind
a new config/flag or a board-capability check so you only provision when
ULP/helper toolchains are actually needed, or (B) move the provisioning off the
critical path by spawning a background task (or using an async resolver) to call
resolve_toolchain_url_* for each entry and logging results, or (C) defer
resolution to a lazy on-first-use path where the code that needs the helper
toolchain triggers resolution on demand; update places referencing Cache::new
and cache.toolchains_dir() accordingly to be usable from the chosen
background/async or gated flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/fbuild-cli/src/daemon_client.rs`:
- Around line 516-520: The error message currently hardcodes
"~/.fbuild/daemon/daemon.log"; replace that with the resolved daemon log path
from the fbuild-paths crate so users in dev/prod isolation get the correct
location. In the FbuildError::DaemonError construction (the block that formats
the message using `e`), call the fbuild-paths API to get the daemon log path
(e.g., fbuild_paths::daemon_log_path() or the appropriate function in
fbuild_paths::paths) and interpolate that returned path into the formatted
string instead of the hardcoded "~/.fbuild/daemon/daemon.log".
---
Nitpick comments:
In `@crates/fbuild-build/src/esp32/orchestrator/packages.rs`:
- Around line 94-95: Hoist the creation of the package cache so it is not
reconstructed each loop: move the call to
fbuild_packages::Cache::new(project_dir) out of the loop and store it in a local
variable (e.g. let cache = fbuild_packages::Cache::new(project_dir)) before
iterating; inside the loop continue to call cache.toolchains_dir().join(&name)
(using the existing name variable) so only one Cache instance is created rather
than recreating it on every iteration.
- Around line 90-118: The loop in provision_helper_toolchains synchronously
calls resolve_toolchain_url_sync for every non-primary toolchain entry, blocking
ESP32 builds; change this to avoid eager blocking by either (A) gating the loop
behind a new config/flag or a board-capability check so you only provision when
ULP/helper toolchains are actually needed, or (B) move the provisioning off the
critical path by spawning a background task (or using an async resolver) to call
resolve_toolchain_url_* for each entry and logging results, or (C) defer
resolution to a lazy on-first-use path where the code that needs the helper
toolchain triggers resolution on demand; update places referencing Cache::new
and cache.toolchains_dir() accordingly to be usable from the chosen
background/async or gated flow.
In `@crates/fbuild-packages/src/library/esp32_platform.rs`:
- Around line 119-152: Both enumerate_packages and get_package_url duplicate the
platform.json read/parse/lookup logic; create a small private helper (e.g.,
parse_platform_json or load_platform_packages) that reads platform.json,
deserializes to serde_json::Value (or returns the packages object), and returns
Result<serde_json::Map<String, Value>, FbuildError> so both enumerate_packages
and get_package_url call it; update enumerate_packages to call the new helper to
obtain the packages map and then map/sort entries, and update get_package_url to
use the same helper to fetch a package's version/url, preserving existing error
messages.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 007c9927-1799-4c6f-a5b8-14dddb46f48f

📥 Commits

Reviewing files that changed from the base of the PR and between 23e072c and 9620ae4.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • crates/fbuild-build/src/esp32/orchestrator/packages.rs
  • crates/fbuild-cli/src/daemon_client.rs
  • crates/fbuild-daemon/src/handlers/operations/build.rs
  • crates/fbuild-packages/src/library/esp32_platform.rs

Comment threadcrates/fbuild-cli/src/daemon_client.rs
Comment on lines +42 to +50
let event = serde_json::json!({
"type": "result",
"success": false,
"request_id": self.request_id,
"message": "daemon build worker terminated unexpectedly (panic or early return); check ~/.fbuild/daemon/daemon.log",
"exit_code": 1,
"output_file": null,
"output_dir": null,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use env-aware daemon log path in fallback result message.

Line 46 hardcodes ~/.fbuild/daemon/daemon.log, but log location is env-scoped. In dev mode this points users to the wrong file and weakens the new actionable diagnostic.

💡 Suggested patch
- let event = serde_json::json!({+ let daemon_log = fbuild_paths::get_daemon_log_file();+ let event = serde_json::json!({
"type": "result",
"success": false,
"request_id": self.request_id,
- "message": "daemon build worker terminated unexpectedly (panic or early return); check ~/.fbuild/daemon/daemon.log",+ "message": format!(+ "daemon build worker terminated unexpectedly (panic or early return); check {}",+ daemon_log.display()+ ),
"exit_code": 1,
"output_file": null,
"output_dir": null,
});

Based on learnings: fbuild-paths implements dev/prod path isolation as ~/.fbuild/{dev|prod}/.

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let event = serde_json::json!({
"type":"result",
"success": false,
"request_id":self.request_id,
"message":"daemon build worker terminated unexpectedly (panic or early return); check ~/.fbuild/daemon/daemon.log",
"exit_code":1,
"output_file": null,
"output_dir": null,
});
let daemon_log = fbuild_paths::get_daemon_log_file();
let event = serde_json::json!({
"type":"result",
"success": false,
"request_id":self.request_id,
"message": format!(
"daemon build worker terminated unexpectedly (panic or early return); check {}",
daemon_log.display()
),
"exit_code":1,
"output_file": null,
"output_dir": null,
});

- daemon_client: resolve daemon-log path via fbuild_paths::get_daemon_log_file
instead of hardcoding ~/.fbuild/daemon/daemon.log so dev-mode users see
the correct path.
- esp32_platform: factor shared platform.json read/parse/lookup into a
private read_packages_section() helper used by both get_package_url and
enumerate_packages.
- orchestrator/packages: hoist Cache::new out of the helper-toolchain loop;
skip resolve_toolchain_url_sync entirely when the cache directory already
exists so steady-state cost on a warm cache is zero (addresses the
"synchronous resolution runs on every ESP32 build" concern).
@zackees
zackees dismissed coderabbitai[bot]’s stale reviewJune 3, 2026 23:17

All three actionable concerns addressed in commit 7b48748: (1) shared platform.json parsing factored into read_packages_section helper; (2) Cache::new hoisted out of the helper-toolchain loop with cache-aware skip when the toolchain dir already exists (zero steady-state cost); (3) hardcoded ~/.fbuild/daemon/daemon.log replaced with fbuild_paths::get_daemon_log_file(). All 10 esp32_platform tests + 2 termination guard tests still pass.

@zackees
zackees merged commit 6396942 into mainJun 3, 2026
84 checks passed
@zackees
zackees deleted the fix/esp32-diagnostics-401 branch June 3, 2026 23:18
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Triage

Development

Successfully merging this pull request may close these issues.

fbuild ESP toolchain resolution: surface better diagnostic + provision all platform.json packages (FastLED#2700)

1 participant

@zackees