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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ The current inventory is auto-published to a stable tracking issue every Monday
## 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, FastLED/fbuild#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 isolation** — `FBUILD_DEV_MODE=1` → `~/.fbuild/dev/`. The daemon endpoint is no longer a fixed port: it's derived per (backend version + cache identity) in the IANA dynamic range 49152–65535 (`fbuild_paths::default_daemon_port` / `daemon_endpoint_key`), so different-version checkouts get isolated daemons and can't serve each other wrong-version builds (FastLED/fbuild#1009). Override with `FBUILD_DAEMON_PORT`.
- **Dev mode isolation** — `FBUILD_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; FastLED/fbuild#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 (FastLED/fbuild#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
Expand Down
2 changes: 1 addition & 1 deletion crates/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ fbuild-test-support (test utilities) ──────────────

- **fbuild-core** — `FbuildError`/`Result`, `BuildProfile`, `Platform`, `SizeInfo`, `DaemonState`. USB identity catalogues are fetched from FastLED/boards and must never be generated or embedded here outside test fixtures.
- **fbuild-config** — `PlatformIOConfig` (INI parser with `extends` inheritance), `BoardConfig`, `McuSpec`
- **fbuild-paths** — Dev/prod path isolation (`~/.fbuild/{dev|prod}/`), version+identity-keyed daemon endpoint (`daemon_endpoint_key`/`default_daemon_port`, dynamic range 49152–65535; FastLED/fbuild#1009), cache dirs
- **fbuild-paths** — Dev/prod path isolation (`~/.fbuild/{dev|prod}/`), version+identity-keyed daemon endpoint (`daemon_endpoint_key`/`default_daemon_port`, Windows 10000–49151 to avoid excluded dynamic ports, 49152–65535 elsewhere; FastLED/fbuild#1009), cache dirs
- **fbuild-packages** — URL-based package downloads, toolchain resolution, library manager, parallel pipeline
- **fbuild-serial** — `SharedSerialManager` (centralized serial I/O), deploy preemption protocol, WebSocket messages, USB-CDC retry logic
- **fbuild-build** — `BuildOrchestrator` trait, per-platform orchestrators (AVR, ESP32, ESP8266, RP2040, STM32, Teensy, WASM)
Expand Down
2 changes: 1 addition & 1 deletion crates/fbuild-paths/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Single source of truth for all `.fbuild` directory paths, with dev/prod isolatio
- `get_other_fbuild_root()` -- Returns the opposite mode's root (for cross-mode daemon discovery)
- `get_daemon_dir()` / `get_daemon_pid_file()` / `get_daemon_port_file()` / `get_daemon_log_file()` / `get_daemon_status_file()` -- Daemon file paths
- `daemon_ownership` module -- `RootOwnershipGuard` (version-blind, per-cache-root exclusive lock at `root-owner.lock`, held by the daemon for its whole lifetime; `fbuild clean cache` takes it exclusively before deleting the zccache store), `SpawnLockGuard` (`spawn.lock` single-flight election so concurrent CLI spawns don't race), and `OwnerClaim`/`write_owner_claim()`/`read_owner_claim()`/`remove_owner_claim()` (a `root-owner.json` claim recording pid/exe/version/mode/cache_root_key/port, written after the daemon acquires ownership and knows its port; never authoritative on its own — always verified against pid liveness + exe identity before acting on it)
- `get_daemon_port()` -- Port resolution with four-level priority: env var, current mode port file, cross-mode port file, default (8865 dev / 8765 prod)
- `get_daemon_port()` -- Port resolution priority: explicit environment override, live endpoint-keyed port file, then a deterministic version/cache-identity default (10000–49151 on Windows to avoid dynamic exclusion blocks; 49152–65535 elsewhere)
- `get_daemon_url()` -- Daemon HTTP URL (`http://127.0.0.1:{port}`)
- `get_cache_root()` -- Global cache dir (`FBUILD_CACHE_DIR` override or `~/.fbuild/{mode}/cache`)
- `get_project_build_root()` -- Per-project build-dir root (`FBUILD_BUILD_DIR` override or `<project>/.fbuild/build`). Returns the *root* — does not append `<env>/<profile>`. Most callers should use `BuildLayout` instead.
Expand Down
40 changes: 32 additions & 8 deletions crates/fbuild-paths/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,19 +77,29 @@ fn endpoint_key_from_material(material: &str) -> String {

/// Deterministic default daemon port derived from [`daemon_endpoint_key`].
///
/// Lands in the IANA dynamic range (49152–65535) so it never collides with a
/// well-known service, and is stable for a given version+identity. Distinct
/// versions (and dev vs prod, since mode is part of the identity) get distinct
/// ports. `FBUILD_DAEMON_PORT` still overrides this (see [`get_daemon_port`]).
/// On Windows this lands in 10000–49151, below the dynamic range that
/// Hyper-V/HNS commonly reserves in excluded blocks. Other platforms use the
/// IANA dynamic range (49152–65535). The result is stable for a given
/// version+identity; distinct versions (and dev vs prod, since mode is part of
/// the identity) get distinct ports. `FBUILD_DAEMON_PORT` still overrides this
/// (see [`get_daemon_port`]).
pub fn default_daemon_port() -> u16 {
port_from_endpoint_key(&daemon_endpoint_key())
}

/// Map a hex endpoint key into the IANA dynamic port window (49152–65535).
/// Pure + deterministic (unit-tested).
/// Map a hex endpoint key into the platform's default daemon-port window.
/// Pure and deterministic (unit-tested).
fn port_from_endpoint_key(key: &str) -> u16 {
#[cfg(windows)]
const LOW: u32 = 10000;
#[cfg(windows)]
const SPAN: u32 = 49152 - LOW;

#[cfg(not(windows))]
const LOW: u32 = 49152;
const SPAN: u32 = 65536 - LOW; // 16384
#[cfg(not(windows))]
const SPAN: u32 = 65536 - LOW;

let n = u64::from_str_radix(key, 16).unwrap_or(0);
(LOW + (n % u64::from(SPAN)) as u32) as u16
}
Expand Down Expand Up @@ -516,6 +526,7 @@ mod tests {
assert!(live.chars().all(|c| c.is_ascii_hexdigit()));
}

#[cfg(not(windows))]
#[test]
fn default_daemon_port_is_in_dynamic_range() {
let p = default_daemon_port();
Expand All @@ -525,16 +536,29 @@ mod tests {
);
}

#[cfg(windows)]
#[test]
fn default_daemon_port_avoids_windows_dynamic_exclusion_range() {
let p = default_daemon_port();
assert!(
(10000..49152).contains(&p),
"Windows daemon port {p} overlaps the dynamic exclusion range"
);
}

#[test]
fn port_from_key_is_deterministic_and_ranged() {
// Same key → same port; keys differing (e.g. by version or checkout)
// map into the dynamic range and generally differ.
// map into the platform range and generally differ.
assert_eq!(
port_from_endpoint_key("0123456789abcdef"),
port_from_endpoint_key("0123456789abcdef")
);
for key in ["0000000000000000", "ffffffffffffffff", "deadbeefcafef00d"] {
let p = port_from_endpoint_key(key);
#[cfg(windows)]
assert!((10000..49152).contains(&p), "key {key} → {p} out of range");
#[cfg(not(windows))]
assert!((49152..=65535).contains(&p), "key {key} → {p} out of range");
}
// Two distinct version/identity keys should not collapse to one port
Expand Down
Loading