Skip to content

Latest commit

 

History

History
187 lines (147 loc) · 15.8 KB

File metadata and controls

187 lines (147 loc) · 15.8 KB

CLAUDE.md

fbuild is a PlatformIO-compatible embedded build tool, organized as a small fixed set of crates (kept as close to a monocrate as possible — see the "Never add a new crate" rule below). See @docs/CLAUDE.md for which architecture doc to read based on what you're working on.

Agent docs routing (#695)

When operating in this repo on a task that isn't covered by the architectural overview, jump to the right per-topic doc instead of grepping for it:

Task Read
"Which fbuild subcommand do I run?" agents/docs/commands-reference.md
"How does deploy actually get firmware to the board?" agents/docs/deploy-architecture.md
"What DTR/RTS state do I open this CDC port at?" docs/usb-cdc-control-line-matrix.md
"How do I run the serial detection code against a real ESP32?" agents/docs/serial-testing.md (#899 — Docker/WSL real-device harness)
"Where does this path/cache/build dir live, and why won't my cache key hit?" agents/docs/path-conventions.md
"How do I cross-compile / why did a release lane break?" agents/docs/cross-compilation.md
"Which crate owns this code?" crates/CLAUDE.md
"Which architecture doc maps to my crate?" docs/CLAUDE.md
"Is this serial port the right device?" fbuild serial probe list (#686)

The four rules an agent must internalize before doing anything else (all listed in "Essential Rules" below): never bypass soldr, never bypass uv, every directory needs a README.md, never invoke pyocd / esptool / dfu-util / picotool directly — go through fbuild deploy (the last is #694's scope; the hook ban lands separately).

Essential Rules

  • USB VID/PID source of truth: Never add or embed a board/device VID or PID in fbuild code, tests used by production, generated Rust tables, or release artifacts. All VID/PID records must come from the published FastLED/boards registry and be ingested through its normal build/publish pipeline. Test-only fixtures may use synthetic or copied IDs to exercise parsing and selection, but they must not become runtime defaults. If a VID/PID is missing, fix the boards registry; do not add an exception here.

    The transport is a zstd-compressed protobuf, usb-vids.proto.zstd, published by the boards site.yml workflow to https://fastled.github.io/boards/usb-vids.proto.zstd and consumed via USB_VIDS_PROTO_ZSTD_URL (crates/fbuild-core/src/usb/data.rs). Its nested Vendor{vid, name, [Product{pid, name}]} shape yields both projections the resolver needs — VID → vendor and VID:PID → {vendor, product} — from one artifact, which is why no per-VID blob or generated table is warranted. fbuild uses it in both paths. populate_online_cache_from_paths_and_urls refetches only when the cache is past its TTL; if that fetch fails it installs the last-ingested cache-root copy, and only then falls back to the legacy JSON overlay. Degrading to a hardcoded literal is deliberately not a rung on that ladder. The archives under crates/fbuild-core/data/ are frozen cfg(test) fixtures only — never refresh one to solve a production lookup or deploy problem.

    Cascade downstream after publishing. A boards change reaches consumers only through an fbuild release: republish the artifact, cut the fbuild version, then move the consumer's pin. In FastLED that is fbuild==X.Y.Z in pyproject.toml — its uv.lock is gitignored, so the pin is the only committed half, and consumers must still relock locally (uv lock && uv sync) or they keep running the previous wheel. FastLED mirrors this rule in its agents/docs/usb-vid-pid-registry.md. Per-module migration status lives in docs/usb-vidpid-audit.md; the guard is ci/check_usb_vidpid_literals.py.

  • Modules-first for new functionality; crate splits only for compile parallelism (backed by --timings). New functionality is still folded into an existing crate as a module, never a drive-by new crate — that original rule (no scope-creep crates) stands. The one sanctioned reason to add a workspace member is splitting an existing giant crate to compile in parallel, backed by cargo build --timings data and maintainer sign-off (#1008 is that sign-off for the fbuild-build / fbuild-packages splits). Such splits keep the original crate as a thin facade that re-exports the extracted crates at their old paths, so consumers are unchanged. If code is needed by two crates that can't depend on each other (e.g. the CLI and the daemon), put the shared, dependency-free pieces in a crate both already depend on (fbuild-core / fbuild-paths). Enforced by CI (crate-gate.ymlci/check_workspace_crates.py): adding a workspace member fails the build unless you also add it to the approved allowlist (and ci/hooks/crate_guard.py) with a maintainer-reviewed rationale.

  • Always use a globally-installed soldr to execute Rust commands. Bare cargo/rustc and legacy uv run cargo shims are blocked by hook. soldr uses rustup which to pick the rustup-managed toolchain from rust-toolchain.toml. The standard Cargo path is soldr cargo ..., so repo Rust builds get soldr's managed zccache path by default; do not add repo-specific RUSTC_WRAPPER wiring for normal builds. Install soldr globally via uv tool install soldr (or see https://github.com/zackees/soldr).

  • Always use uv for Python. Bare python/pip are blocked by hook. Use uv run ... or uv pip ....

  • MSRV: 1.95.0 | Edition: 2021 | Toolchain: 1.95.0 pinned in rust-toolchain.toml (clippy + rustfmt)

  • CI hosts: Linux, Windows. All warnings denied (RUSTFLAGS="-D warnings"). There are no macOS runners — macOS is a build target, not a test host: every apple-darwin binary is cross-built from Linux by the Linux-hosted native workflows, release-auto.yml and build.yml. Do not add a macos-latest lane back.

  • Cross-compilation goes through soldr, and only soldrsoldr prepare --target X then soldr build --target X. The zig- and xwin-based wrappers are retired and enforced-out by ci/check_no_legacy_cross.py plus the tool_guard.py hook. Never pass zigbuild's .2.17 glibc suffix to soldr: it has no such target, warns, falls back to the host toolchain and still exits 0, shipping a GLIBC_2.39 wheel tagged manylinux_2_17. soldr's own sysroot links at 2.16. See agents/docs/cross-compilation.md.

  • Every directory with files must have a README.md (enforced by hook)

Commands

bash test                   # unit tests only
bash test --full            # unit + stress + integration tests
bash test -p <crate> -- <test_name>
soldr cargo check --workspace --all-targets
soldr cargo clippy --workspace --all-targets -- -D warnings
soldr cargo fmt --all
RUSTDOCFLAGS="-D warnings" soldr cargo doc --workspace --no-deps

# Public deploy API conventions
soldr cargo run -p fbuild-cli -- deploy tests/platform/uno -e uno --to emu
soldr cargo run -p fbuild-cli -- deploy tests/platform/uno -e uno --to emu --monitor
soldr cargo run -p fbuild-cli -- deploy tests/platform/esp32dev -e esp32dev-qemu --to emu --emulator qemu

# test-emu: build + run in emulator (CI-friendly, exits with emulator exit code)
soldr cargo run -p fbuild-cli -- test-emu tests/platform/uno -e uno
soldr cargo run -p fbuild-cli -- test-emu tests/platform/esp32s3 -e esp32s3 --timeout 10
soldr cargo run -p fbuild-cli -- test-emu tests/platform/mega -e megaatmega2560 --emulator simavr

# Per-file rustfmt
soldr rustfmt --check <file.rs>

# Board definition management
uv run python ci/validate_boards.py                    # validate against PlatformIO
uv run python ci/validate_boards.py --external         # compare against Arduino + Zephyr
uv run python ci/board_sources.py --search QUERY       # search all external sources
uv run python ci/board_sources.py --compare            # find boards missing from fbuild
uv run python ci/board_sources.py --list-arduino       # list Arduino package index boards
uv run python ci/board_sources.py --list-zephyr        # list Zephyr boards
soldr cargo run -p fbuild-config --bin enrich_boards  # enrich from local PlatformIO

Distribution

Releases ship via the Autonomous Release GitHub Action (.github/workflows/release-auto.yml). PyPI is the distribution channel; per-platform native binaries are built, assembled into wheels, and uploaded via PyPI trusted publishing — there is no local publish script.

To cut a release:

# 1. Bump version in both files (must match)
#    Cargo.toml  -> [workspace.package] version
#    pyproject.toml -> [project] version
# 2. Push the bump commit to main (do NOT push a tag manually —
#    the action creates one only after the build + upload succeed)

See docs/RELEASING.md for the full flow, gating logic, and re-run instructions when a release stalls.

Hooks (enforced automatically)

All hooks are Python scripts in ci/hooks/, invoked via uv run:

  • UserPromptSubmit: ci/hooks/board_context.py detects board-related prompts and injects skill guidance (board lookup workflow, external source URLs, relevant commands)
  • PreToolUse: ci/hooks/tool_guard.py blocks bare Rust commands and any uv run invocation of soldr/cargo (must use a globally-installed soldr directly) and bare python/pip (must use uv) across supported shell tools, not just Bash
  • PreToolUse: ci/hooks/crate_guard.py blocks Edit/Write of Cargo.toml at any path outside the approved set (workspace root + 14 member dirs + dylints/ban_raw_subprocess). Real-time monocrate enforcement; complements the batch CI check at ci/check_workspace_crates.py. Keep the allowlists in both files in sync
  • PostToolUse: ci/hooks/lint.py auto-formats + runs clippy on edited .rs files
  • PostToolUse: ci/hooks/readme_guard.py errors if directory lacks README.md
  • SessionStart: ci/hooks/check-on-start.py captures git fingerprint
  • Stop: ci/hooks/code-review-on-stop.py triggers /code-review skill if source files changed
  • Stop: ci/hooks/check-on-stop.py runs full workspace lint + tests (skips if no changes)

Skills

Custom Claude Code skills in .claude/skills/:

  • /board-support — Diagnose and fix board definition issues. Searches fbuild's database, PlatformIO, Arduino package indices, and Zephyr boards. Auto-suggested by the board_context.py hook when board-related prompts are detected.
  • /code-review — End-of-session code review. Checks for hardcoded values (should be in JSON), code that belongs in core instead of platform crates, board/MCU JSON quality, orchestrator completeness, and bugs. Auto-triggered by Stop hook.

Language Policy

  • Python is only for CI scripts, packaging, hooks, and PyO3 bindings. All tests, benchmarks, and application logic must be written in Rust.
  • uv run is required only because hooks enforce it for toolchain management — it is not an endorsement of Python for project code.
  • Exception: fbuild-python crate provides PyO3 bindings so FastLED can from fbuild.api import SerialMonitor.
  • When in doubt, write it in Rust.

Development Philosophy: TDD

  • Red → Green → Refactor. Write failing tests first, then implement the minimum code to make them pass, then refactor.
  • Tests are the spec. If the test suite passes, the feature works. If behavior isn't tested, it doesn't exist.
  • Comprehensive tests over comprehensive docs. Tests are executable documentation.
  • Test real behavior: use tempfile for filesystem tests, not mocks. Test the contract, not the implementation.
  • Mocks vs. real fixtures (#838). Use mocks only when the abstraction has no concrete dependency you can stand up cheaply. Real integration boundaries — subprocess invocation, real filesystem layout, serial-port behavior — must be exercised with a real binary + tempfile::TempDir, gated with #[ignore] when slow. Example: orchestration logic (stage counting, input ordering, concurrency) over a SketchBuilder trait is fine to mock; the actual compile/link/flash path is not.
  • A/B testing: FastLED can switch between --platformio and fbuild. The Python integration tests in ~/dev/fbuild/tests/ are the acceptance criteria.

Ignored-test policy (#839)

Every #[ignore] attribute MUST carry a reason string: #[ignore = "..."]. The reason MUST either cite a tracking issue (#NNN) or name a concrete hardware/toolchain requirement (e.g. requires teensy 3.0, downloads ESP32 toolchain (~hundreds of MB)). Bare #[ignore] is a policy violation and will be flagged in the weekly inventory.

The current inventory is auto-published to a stable tracking issue every Monday by .github/workflows/audit-ignored-tests.yml, which runs ci/audit_ignored_tests.py --markdown and rewrites the issue body. Manual dispatch: gh workflow run audit-ignored-tests.yml --repo FastLED/fbuild. Tests left ignored 90+ days without a hardware reason will be escalated to CodeRabbit review in a follow-up.

Core Principles

  • Simplicity first. Minimal code impact. No over-engineering.
  • No laziness. Root causes only. Senior developer standards.
  • Verify before done. Run tests, demonstrate correctness.
  • Plan non-trivial work in GitHub issues for FastLED/fbuild. Capture lessons in the relevant issue or PR discussion; only add repo docs when the information is durable reference material.

Key Constraints

  • No file-based locks, with one sanctioned exception — almost all synchronization is through the daemon's in-memory managers. The narrow exception is fbuild-daemon startup/lifetime root-ownership and spawn-herd election (soldr-style, #1159): a version-blind root-owner.lock held for the daemon's whole lifetime, plus a spawn.lock single-flight election for concurrent CLI spawns. zccache compile/object access keeps its own zccache-internal in-memory synchronization — these locks never gate cache reads/writes. Locks are OS-released on process death and must never be manually broken or deleted.
  • Dev mode isolationFBUILD_DEV_MODE=1~/.fbuild/dev/. The daemon endpoint is no longer a fixed port: it's derived per (backend version + cache identity) in 10000–49151 on Windows (below commonly excluded dynamic ports; #1322) and the IANA dynamic range 49152–65535 elsewhere (fbuild_paths::default_daemon_port / daemon_endpoint_key), so different-version checkouts get isolated daemons and can't serve each other wrong-version builds (#1009). Override with FBUILD_DAEMON_PORT.
  • HTTP API compatibility — same endpoints and JSON schemas as the Python daemon
  • Windows USB-CDC — 30 retries, aggressive buffer drain, DTR/RTS toggling after flash
  • Emulator CLI convention — prefer fbuild test-emu for CI; fbuild deploy --to emu [--emulator <kind>] for interactive use; keep --target and --qemu only as compatibility aliases
  • Serial recovery & client API — post-deploy serial-port recovery is owned by Deployer::post_deploy_recovery (default: 3 s fast-poll; platform-specific deployers can override for e.g. LPC + CMSIS-DAP wedge recovery). Clients consume serial through fbuild.api.SerialMonitor exclusively — direct pyserial use by clients is deprecated and will be lint-enforced in a follow-up sweep (#605 Phase 1).

Reference Implementations

  • Python fbuild: ~/dev/fbuild (production) and main branch of this repo
  • zccache: ~/dev/zccache (Rust workspace pattern, CI, distribution)
  • FastLED: ~/dev/fastled (consumer of fbuild's serial API)