Skip to content

refactor(platform): centralize host mechanics behind fbuild-core::platform and cfg_select! #1306

Description

@zackees

Context

Soldr and zccache have completed the same architectural upgrade, and running-process is executing its adaptation now:

fbuild currently pins Rust/MSRV 1.94.1, while std::cfg_select! is stable in Rust 1.95.0. A preliminary source scan found 386 host-cfg match lines across 93 Rust files under crates/. The largest concentrations are fbuild-toolchain (85), fbuild-core (68), fbuild-deploy (59), fbuild-daemon (46), fbuild-cli (37), and fbuild-serial (34). Direct native API usage appears in 21 files, including:

  • fbuild-core::{containment,path,process_identity,subprocess}
  • daemon lifecycle/locking
  • CLI IDE and USB recovery
  • serial port classification/scanning/recovery
  • RP2040, probe-rs, WCH, and WLink deployment
  • QEMU/toolchain host integration

Native dependencies are also distributed: windows-sys is declared in fbuild-cli, fbuild-deploy, and fbuild-serial; libc is declared in fbuild-core and fbuild-daemon; and interprocess is consumed by fbuild-daemon.

This is not all one category. fbuild must keep three concepts distinct:

  1. Host mechanics ? what OS/architecture the fbuild executable is running on; this belongs behind the platform boundary.
  2. Host artifact policy ? which compiler, emulator, deploy tool, or archive must be downloaded for that host; product code owns the policy but must consume neutral host facts instead of compile-time cfg.
  3. Embedded build target ? the PlatformIO/board/MCU target being compiled or flashed; fbuild_core::Platform, board policy, toolchain target triples, and orchestrator dispatch remain outside the host-platform layer.

The existing monocrate rule forbids adding a fbuild-platform crate for new functionality. fbuild-core is dependency-cycle-safe and is already the foundational dependency for the workspace, so the fbuild-specific adaptation should be an internal fbuild_core::platform module.

Proposal

Adopt the rule:

One module selects the host. Every fbuild crate consumes a neutral platform facade.

1. Raise the toolchain to Rust 1.95.x

Update every declaration of fbuild's own MSRV/toolchain from 1.94.1 to the same pinned Rust 1.95.x release:

  • workspace rust-version
  • rust-toolchain.toml
  • MSRV/fmt/Dylint/native-build workflows
  • Docker and serial-test bootstrap scripts
  • repository guidance and workflow documentation
  • focused regression coverage that prevents the declarations drifting

Do not rewrite fixtures that intentionally model arbitrary user or embedded toolchains.

2. Bootstrap the single host-selection boundary in fbuild-core

Create fbuild_core::platform as a module, not a new workspace member. Its root contains the only production host-selection site, using std::cfg_select! with explicit Windows, Linux, and macOS arms and no fallback arm.

The research phase must lock the exact layout, but the neutral capability surface should cover at least:

  • platform::process: command configuration, background spawn, containment, termination, PID/image inspection, and exit interpretation; delegate to running-process wherever it already owns the portable primitive.
  • platform::fs: file identity, links/reparse behavior, permissions, atomic replacement, volume/mount facts, and native error normalization.
  • platform::ipc: fbuild-owned endpoint/listener/peer mechanics not already abstracted by running-process/interprocess.
  • platform::executable: native executable names, PATH/PATHEXT candidates, current/sibling image discovery, and runnable-image materialization.
  • platform::host: neutral HostOs, HostArch, path-list separator, home/runtime facts, and host keys consumed by toolchain/package/deploy artifact policy.
  • platform::device: serial-port, USB/PnP, sysfs/IOKit/SetupAPI, removable-volume, and topology primitives required by fbuild-serial and deployers.

Neutral facade types must prevent raw handles/fds, Win32 structs, Unix extension traits, concrete socket/pipe types, and concrete OS modules from escaping into shared callers. Every new directory must include its required README.md.

Do not duplicate platform behavior already provided by running-process, serialport, interprocess, or other dependencies. Wrap/delegate where their neutral API is sufficient; own only fbuild-specific seams and types.

3. Research, freeze, and ratchet the migration inventory

Before moving implementations:

  1. Inventory host selectors, native imports/paths, target-specific dependency tables, compile-time host facts, and concrete platform-module references across all handwritten Rust sources, including inactive cfg branches, inline tests, integration tests, examples, benches, and build.rs.
  2. Reconcile Windows, Linux, and macOS inventories.
  3. Classify every occurrence as:
    • host mechanic to migrate;
    • host artifact policy that should consume neutral runtime/const host facts;
    • embedded build-target policy that must remain outside this refactor;
    • generated/vendor source;
    • narrowly justified specialized artifact, if one truly cannot use the facade.
  4. Freeze an exact-occurrence ledger keyed by normalized repo-relative path, finding kind, construct, and ordinal.
  5. Make ordinary migration PRs delete ledger rows only. A stale row, new unlisted occurrence, duplicate row, changed total, or Dylint/scanner disagreement must fail CI.

The preliminary 386-line scan is discovery evidence, not the authoritative ledger total.

4. Migrate by capability

Split implementation into ordered, reviewable phases after the ledger is frozen. Each phase must:

  • begin with focused RED characterization or boundary evidence;
  • move one coherent capability behind fbuild_core::platform;
  • replace caller-side cfg!/#[cfg] decisions with the neutral facade;
  • remove the exact corresponding ledger rows in the same PR;
  • report before/after global and affected-crate counts;
  • prove the stale-row and new-occurrence checks;
  • finish GREEN on focused tests plus the repository validation appropriate to the touched crates.

Suggested order: host/executable facts used by artifact selection; process/containment; filesystem/path mechanics; daemon IPC/lifecycle; serial/USB/device mechanics; specialized deploy/emulator cases; final zero-baseline enforcement.

5. Permanently enforce the boundary

Add a pre-expansion Dylint plus an independent whole-tree scanner, following the soldr/zccache/running-process precedent.

Outside the selector and private Windows/Linux/macOS implementation trees, reject:

  • host selectors in #[cfg], #[cfg_attr], cfg!(), cfg_if!, and macro token trees;
  • compile-time host escape routes such as std::env::consts::{OS, ARCH} or CARGO_CFG_TARGET_* when used to select host mechanics;
  • native imports/paths such as std::os::*, windows-sys, libc, native interprocess transports, raw handles/fds, and concrete platform modules;
  • direct references to the selected implementation alias;
  • new target-specific native dependency tables outside the platform implementation boundary.

The checker must distinguish explicit embedded compiler/build-target policy from host selection. Tests are not an escape hatch: generic tests exercise the neutral facade, while implementation-specific tests live beside the relevant private implementation.

At zero, delete the transitional ledger/dump escape hatch and retain the permanent zero-violation Dylint, whole-tree scanner, manifest checker, and three-host behavior coverage.

Acceptance criteria

  • RED: focused fixtures prove current private/inactive host cfg, native API access, concrete implementation references, and test-only occurrences evade the proposed boundary.
  • fbuild's MSRV and every canonical toolchain declaration are updated consistently from 1.94.1 to the selected Rust 1.95.x release, with drift coverage.
  • No new workspace crate is added; the boundary lives in fbuild_core::platform in accordance with the monocrate policy.
  • Exactly one production cfg_select! selects Windows, Linux, or macOS, with no fallback arm.
  • The reviewed three-host inventory classifies host mechanics, host artifact policy, embedded target policy, and any specialized artifacts without conflating them.
  • All shared callers use neutral platform facade types/functions and cannot name native OS APIs, raw native types, or concrete implementations.
  • Existing running-process broker/containment behavior, daemon compatibility, serial/USB recovery, deploy behavior, toolchain resolution, and diagnostics remain compatible unless a separately approved issue changes them.
  • A Linux-hosted build regression proves that selecting a Windows/macOS embedded toolchain artifact or firmware target does not select non-Linux host mechanics.
  • Windows, Linux, and macOS CI exercise representative process, filesystem, executable/host, IPC, serial/USB/device, deploy, and emulator behavior through the same facade paths.
  • The exact-occurrence ledger monotonically reaches zero; the permanent Dylint, scanner, and manifest checker reject new host-platform leakage.
  • GREEN: focused tests and the repository's full validation entrypoints pass for each migration phase and at final closeout.

Decisions

  • Priority: P2 architectural debt; behavior works today, but the breadth of distributed host selection makes portability changes and review increasingly risky.
  • Use fbuild-core, not a new fbuild-platform crate, because the repository explicitly keeps functionality in the existing crate set and the dependency graph makes fbuild-core the cycle-safe shared home.
  • Require Rust 1.95.x because stable std::cfg_select! is the mechanism used by the soldr/zccache precedent and gives an exhaustive compile-time selector.
  • Treat the six capability namespaces as the initial architecture; the frozen inventory may merge an empty namespace or split a demonstrably overloaded one without weakening the single-selector rule.
  • Keep embedded target selection and package/tool artifact policy in their current product owners; only raw host facts and mechanics move behind the facade.
  • Use an exact-occurrence ratchet rather than a file allowlist so migrated files cannot silently acquire new platform debt.

Related issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions