Repository navigation
Description
Goal
Investigate whether fbuild can offer real SCons execution as a platformio.ini backwards-compatibility mode for the dynamic extra_scripts behaviors that the current native shim deliberately rejects — and document the engine required and the architectural cost.
Framing note: The original request assumed "fbuild does not have this feature." That is only partly true. fbuild already ships a constrained SCons-compatibility layer (a Python
MockEnvsubprocess that resolves flag/path/lib mutations) — seecrates/fbuild-build/src/script_runtime.rsandcrates/fbuild-build/src/script_runtime_harness.py. This issue is about the remaining gap: scripts that need a real SCons engine, not a mock. The earlier journey is closed under #34 → #38 → #39 → #41 → #43; this issue picks up where #43's "explicit non-goals" left off.
Current state (what already works)
fbuild's native path resolves a narrow subset of extra_scripts without SCons:
PlatformIOConfig::get_extra_scripts()readspre:/post:entries (crates/fbuild-config/src/ini_parser/mod.rs).resolve_extra_script_overlay()spawns a Python subprocess runningscript_runtime_harness.py, which executes the user's scripts against a mock SConsenv/projenv(MockEnv) and serializes the resulting construction-variable scopes to JSON (crates/fbuild-build/src/script_runtime.rs:29).- The Rust side translates those scopes (
CPPDEFINES,CPPPATH,CCFLAGS,CFLAGS,CXXFLAGS,ASFLAGS,LINKFLAGS,LIBPATH,LIBS) into compile/link flag overlays and feeds them into the normal native pipeline.
Supported shims today: Append/AppendUnique/Prepend/Replace over the known scopes, plus read-only/no-op helpers Dump, BoardConfig, PioPlatform, Flatten, VerboseAction, Execute (no-op), IsCleanTarget, IsIntegrationDump, subst, GetProjectOption. Anything else hard-fails with "Recommendation: use --platformio."
Per the #43 real-world sample (25 GitHub repos), this covers 8/25 projects natively. The remaining misses that need genuine SCons semantics cluster as:
- Effectful
Execute(...)— scripts that actually run a tool to generate sources/headers/firmware images at build time (e.g.merge_firmware.py,generate_includes.py). SConscript(...)— pulling in additional SCons build fragments.AddBuildMiddleware/ custom builders / custom targets — Marlin-class build stacks.- Construction-variable rewrites beyond flag scopes —
PROGNAME,MKSPIFFSTOOL, customenv[...]tool substitutions. env.get(...)/GetProjectConfig/GetBuildType/ParseFlagsExtendedused to drive dynamic logic.
The actual open question
"Full SCons support" can mean three architecturally different things. The earlier issues (#39, #41) already established that PlatformIO's __idedata / pio project metadata export is insufficient to faithfully replay the build graph (no full link line, no library archive ordering, no action hooks). So the realistic options are:
Option A — Host a real SCons engine in-process/vendored
Vendor SCons (pure-Python, MIT license) plus PlatformIO's builder/ tools (the platform build scripts that define the actual compile/link/upload builders). Run the genuine construction-environment build far enough to either (a) emit a resolved build spec or (b) drive compilation itself.
- Engine required: a Python interpreter at build time (already a soft dependency for the current shim) and the full PlatformIO Core + platform packages, because
extra_scriptsrun inside PlatformIO's builder, not bare SCons. There is no "bare SCons" PlatformIO project — the SConstruct is PlatformIO'sbuilder/main.py. - Implication: This is effectively becoming a PlatformIO host. At that point the marginal value over the existing
--platformiopassthrough is small, and we'd own a vendored copy of two large Python codebases plus their version skew across platform packages. - Cost: Very high. Cross-platform Python packaging in a Rust/PyPI-distributed tool, version-matrix maintenance against PlatformIO releases, and the philosophical reversal of "we don't run SCons ourselves."
Option B — Delegate the resolution phase to PlatformIO, then compile natively (the #41 "state-dump bridge", upgraded from mock to real)
Instead of MockEnv, invoke the real pio run with an injected final POST script that runs after all user scripts, serializes the fully-resolved env/projenv state (tool paths, all scopes, PIOBUILDFILES, linker script, flash-image steps) to JSON, and exits before compilation. fbuild then performs native compile/link/deploy from that spec.
- Engine required: PlatformIO Core installed/available (same prerequisite as
--platformio), invoked once per env to produce the dump. No vendoring. - Covers: dynamic flag computation,
env.get,BoardConfig/PioPlatformfor real,build_unflags, andExecute(...)side effects (since real PlatformIO actually runs them, generated sources land on disk before the dump). - Does NOT cleanly cover: custom builders/targets and middleware whose behavior is only known at action-execution time (Prototype a PlatformIO/SCons state-dump bridge for scripted-project compatibility #41 failure criteria), and anything that requires the actual SCons DAG to drive incremental rebuilds.
- Cost: Medium. New "resolve via pio, build via fbuild" mode + the injected dump harness + a translation layer from PlatformIO's resolved state to fbuild's
BuildContext. Risk: divergence between dumped state and what PlatformIO would actually execute. Loses fbuild's caching/speed advantage for the resolution step (onepioinvocation per env).
Option C — Keep the MockEnv shim, widen it opportunistically (status quo trajectory)
Continue adding read-only/deterministic shims as real repos demand them, keep hard-failing on effectful behavior with the --platformio recommendation.
- Engine required: none beyond the current Python subprocess.
- Cost: Low and incremental, but structurally cannot cover effectful
Execute,SConscript, or custom builders — those need a real engine. Ceiling is roughly the "simple flag-file" class of scripts.
Option D — Status quo fallback only
--platformio already delegates the entire build to real PlatformIO for anything unsupported. Cost: zero; UX cost: user loses fbuild's speed for those projects.
Feasibility assessment
- Is real SCons support feasible? Technically yes via Option A, but it is architecturally self-defeating: hosting SCons means hosting PlatformIO, which is what
--platformioalready does. Not recommended. - Is there a feasible middle path? Option B is the only one that meaningfully extends coverage (especially generator scripts via real
Executeside effects) without permanently owning a vendored Python build system. It was prototyped-on-paper in Prototype a PlatformIO/SCons state-dump bridge for scripted-project compatibility #41 but never built; Research whether PlatformIO __idedata / project metadata can support a scripted-project bridge #39 confirmed the metadata-export shortcut alone is insufficient, so Option B specifically requires the injected-dump variant, notpio project metadata. - What's the cheapest high-value next step? Likely Option C for a couple more deterministic shims, with Option B as the strategic investment if/when generator-script projects become a priority.
Proposed scope for THIS issue
This is a feasibility/architecture decision issue, not an implementation issue. Deliverables:
- Confirm the engine analysis above against the current PlatformIO Core (
builder/main.py,builder/tools/piointegration.py) — verify there is no bare-SCons entry point that avoids hosting PlatformIO. - Build a throwaway Option B spike: inject a real final-POST dump script into
pio run, capture resolvedenv/projenv+ tool paths +PIOBUILDFILESfor one generator-script repo from Real-world platformio.ini sample reveals remaining native compatibility gaps #43 (e.g.UtilitechAS/amsreader-firmware, which usesExecute+VerboseAction) and one synthetic fixture. - Record whether the dump is rich enough to drive a native compile+link without a second
pioinvocation (the Prototype a PlatformIO/SCons state-dump bridge for scripted-project compatibility #41 success criterion). - Decide: pursue Option B, stay on Option C, or formally declare effectful SCons a permanent
--platformio-only path and document it.
Recommendation (for discussion)
Decline Option A (becoming a SCons host duplicates --platformio). Treat Option B as the only feasible way to close the generator-script gap, gated behind a spike that proves the injected-dump is replay-sufficient. Until then, keep widening the deterministic MockEnv shim (Option C) and keep effectful scripts on the --platformio fallback (Option D).
References
- Existing shim:
crates/fbuild-build/src/script_runtime.rs,crates/fbuild-build/src/script_runtime_harness.py,crates/fbuild-build/README.md("Nativeextra_scriptsBoundary") - Prior issues: platformio.ini advanced build configurations are not yet compatible with PlatformIO/SCons flag workflows #34 (gap), Define native support boundaries for PlatformIO extra_scripts env/projenv mutations #38 (boundary), Research whether PlatformIO __idedata / project metadata can support a scripted-project bridge #39 (idedata insufficient), Prototype a PlatformIO/SCons state-dump bridge for scripted-project compatibility #41 (state-dump bridge prototype), Real-world platformio.ini sample reveals remaining native compatibility gaps #43 (real-world 8/25 sample + non-goals)
- PlatformIO scripting / construction environments: https://docs.platformio.org/en/latest/scripting/construction_environments.html
- PlatformIO launch types (PRE/POST stages): https://docs.platformio.org/en/latest/scripting/launch_types.html
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsTriage
Activity
zackees commentedon Jun 14, 2026
Investigation: emulation shim vs packed lite-SCons sidecar
Following on from the original feasibility framing — there's a fifth option sitting between the current MockEnv (Option C) and full SCons vendoring (Option A) that's worth a focused look:
Surface-area math
What the existing MockEnv handles vs what real scripts in the #43 sample reach for:
Append/AppendUnique/Prepend/Replaceenv[KEY],env.get(key, default)env.subst("$VAR/...")Dump,BoardConfig,PioPlatform,IsCleanTarget,IsIntegrationDump,GetProjectOption(s),GetBuildType,Flatten,VerboseAction(no-op)Action(cmd_or_callable, comment=None)Execute(action)(effectful)AddPreAction(target, action)/AddPostAction(target, action)AddCustomTarget(name, deps, actions, …)AddBuildMiddleware(callback, regex=None)SConscript(path)AddMethod(callable, name)ParseFlagsExtended(line)-DX -Iy -lpathinto flag scopesTotal new API surface to write: ~12 methods + 3 small classes (
Action,Builder,Node). Concretely:Actionis "command-string or callable plus an optional comment".Builderis a name + a default Action + suffixes; scripts mostly register builders, fbuild executes the recorded calls.Nodeis a thin wrapper around a path string with__str__returning the path (enough to satisfyenv.File("...")/env.Dir("...")callers).The existing harness is 434 LOC of Python. The sidecar would land at maybe ~1.5K LOC total — well under the threshold where AI-assisted greenfield writing becomes the bottleneck.
Emulation shim vs sidecar — head-to-head
MockEnv.__getattr__Execute(action)action.execute(env); generated files appearenv.AddBuildMiddleware,AddCustomTarget)SConscript("child.py")exec(open(child).read(), child_scope)chainingenv.substrecursion, list flattening, scope normalization — each surfaces as a new ticketExecuteblocks the restExecute+ middleware + custom-target are real; remainder is real-DAG-dependent and stays on--platformioWhat a "lite SCons" can't do (and shouldn't try)
Three things the sidecar should explicitly not attempt — these are the architectural boundary that keeps it lite:
--platformio. Most PlatformIOextra_scriptsdon't actually need the DAG; they need one generator step to run.fbuild-header-scan); the sidecar doesn't need to replicate SCons'CScanner.env.MergeFlashImagefor ESP32,env.PackageJsonFirmwarefor OTA, etc.). The sidecar records them as named ops; fbuild either maps them to native equivalents (esptool merge_bin already lives in fbuild-deploy) or fails fast with a structured "needs platformio for builder X" message.Isolation model
Sidecar gets the same subprocess isolation the current MockEnv harness uses today: spawn
python lite_scons_sidecar.py <input.json>, capture stdout (resolved state JSON) + stderr (notes + errors), bounded wall-clock timeout. No shared interpreter state, no Python in the fbuild Rust binary. Same security profile as today.Effectful
Execute(action)does run the user's tool — but inside the same subprocess, so its filesystem effects are observable to fbuild via the returned generated-files manifest. This is the only meaningful behavioural delta from today's MockEnv from a sandboxing standpoint.Cost / spike scope
Initial build (AI-assisted greenfield against the inventoried surface):
Environment,Action,Builder,Node,subst,Execute,AddPre/PostAction,AddBuildMiddleware,AddCustomTarget,SConscript,AddMethod,ParseFlagsExtended).script_runtime.rs: extend the returned JSON shape to carry the generated-files manifest, recorded pre/post actions, and middleware records; wire them into the native compile pipeline.UtilitechAS/amsreader-firmware, 1 Marlin-class with middleware, 1 OTA packaging, 1 SConscript-recursive).Total: ~1 week to a decision-quality answer. That answer is either "we go from 8/25 to 18+/25" (ship) or "Builder semantics in PlatformIO defeat us" (fall back to Option B's
pio rundump path).Recommendation
Spike Option E as a 1-week sub-issue and decide on results. If the sidecar covers the #43 generator-script + middleware repos cleanly, ship it as the default backend for
extra_scriptsand retire the MockEnv per-method patching trajectory. If we hit a Builder-semantics wall, the spike's outputs (the inventory + the failing-repo characterisation) feed directly into Option B's design.The user's hunch (the surface is small enough that an AI-assisted lite reimplementation is feasible) lines up with the inventory: ~12 methods + 3 small classes + the existing 434-LOC foundation. Not zero risk — DAG-edge cases will surface — but it's the only path between "drift forever on per-script shim patches" (Option C) and "host PlatformIO entirely" (Option A) that also covers effectful
Execute, which is the cluster blocking the largest chunk of the #43 misses.zackees commentedon Jun 14, 2026
Spike results — Option E (lite-SCons sidecar)
Ran the spike. The prototype sidecar is ~620 LOC of Python (single file, no SCons dependency) and runs all five representative SCons patterns that the current MockEnv shim either no-ops or hard-fails. The surface-area estimate from the earlier comment held up.
Spike repo:
C:\Users\niteris\dev\fbuild-spike-553\(scratch directory, not committed). What got built:lite_scons_sidecar.py— the whole prototype:Environment,Action,Builder,Node,Ledger,substwith recursion,Executewith subprocess for command-strings + direct callable invocation for Python actions,AddPreAction/AddPostAction/AddCustomTarget/AddBuildMiddleware/SConscript/AddMethod/ParseFlagsExtended, plusBoardConfig/PioPlatform/GetProjectConfig/GetBuildType/env.getfall-through intoproject_options.scripts/:generator.py—Execute(Action(callable))writesinclude/buildinfo.hat script time.middleware.py—AddBuildMiddleware(callback, "*ArduinoCore-*/cores/arduino/*.cpp")(Marlin-class).postaction.py—AddPostAction("$BUILD_DIR/$PROGNAME$PROGSUFFIX", merge_firmware)(OTA-style packager).sconscript_main.py→sconscript_child.py— recursiveenv.SConscript(...)chained mutation.parseflags.py—env.get("PROJECT_EXTRA_FLAGS", default)→ParseFlagsExtended(...)→ conditionalif env.GetBuildType() == "release"flag append.Outputs after the run
{ "status": "ok", "scopes": { "CCFLAGS": ["-Wno-unused-parameter", "-DPARENT_RAN=1", "-Os"], "CPPDEFINES": [ {"kind": "kv", "key": "BUILDINFO_PRESENT", "value": "1"}, {"kind": "kv", "key": "CHILD_DEFINE", "value": "1"}, {"kind": "kv", "key": "MY_FLAG", "value": "1"} ], "CPPPATH": [ {"kind": "path", "value": ".../project/include"}, {"kind": "path", "value": ".../project/include/child"}, {"kind": "path", "value": ".../project/external/inc"} ], "LIBS": ["extrasym"] }, "vars": { "PROGNAME": "device-fw-v2", ... }, "executed_actions": [ {"kind": "callable", "repr": "<Action callable=write_buildinfo>", "rc": 0, "wallclock_ms": 0} ], "generated_files": [ {"path": ".../project/include/buildinfo.h", "size": 95} ], "recorded_post_actions": [ {"target": "$BUILD_DIR/$PROGNAME$PROGSUFFIX", "action_repr": "<Action callable=merge_firmware>"} ], "middleware": [ {"callback_repr": "tweak_flags_for_arduinocore", "regex": "*ArduinoCore-*/cores/arduino/*.cpp"} ], "notes": [], "errors": [] }Everything the surface-area table predicted, the sidecar produces.
Executeactually runs and the generated header lands on disk (verified bygenerated_files). Middleware is recorded with its regex so fbuild's native compile can call it pre-source. Post-actions are recorded with the unresolved target template (fbuild callssubstto resolve it after link).SConscriptrecursively mutates the same env across files.ParseFlagsExtendedroutes-I/-L/-l(joined and space-separated forms) into the right scopes; defines go toCPPDEFINES; everything else falls intoCCFLAGS.Bugs the spike surfaced (and the production version will need)
A spike's real value is the bug list it produces against a "this should obviously work" expectation:
env.get(key, default)must fall through toproject_options, not just_vars/_scopes. Real scripts treatenv.get("FOO")andenv.GetProjectOption("FOO")as interchangeable. First iteration only checked_vars/_scopes, soparseflags.pygot the default string instead of the project_options value, and the wrong flag set landed in scopes. One-line fix.ParseFlagsExtendedmust handle both-Ipathand-I path(space-separated). PlatformIO's own platform packages emit the joined form, but third-party flag-string outputs (toolchain probes,pkg-config-style helpers) emit space-separated. Real SCons handles both. Fixed via a 2-token lookahead in the parser loop.SConscriptresolves paths relative to the calling script's directory, not the project root. Real SCons docs this clearly; my first cut usedproject_dirand the child file was missing. Fixed via a stack-tracked__CURRENT_SCRIPT_DIR__updated by bothrun_scriptandSConscriptitself.All three are exactly the kind of subtle behavioural mismatch the "head-to-head" comment predicted. They're cheap to fix once observed. None of them are architectural.
What the spike did NOT exercise (yet)
The five scripts are synthetic — written to hit each cluster of MockEnv failures from the earlier inventory. They aren't real #43 repo scripts. The next beat (1-3 day verification round per the cost estimate) would:
UtilitechAS/amsreader-firmware(real generator script) and run the sidecar against itsextra_scriptslist.AddBuildMiddlewarepattern with real glob matching.env.MergeFlashImage(the ESP32-specific custom builder) — this is where the "PlatformIO-defined chip-family builders" boundary will surface in practice. Expectation: the sidecar records it as abuilder_callsentry and the Rust side either maps it to fbuild-deploy's existing esptool merge_bin or fails fast with a structured "needs platformio for builder MergeFlashImage" message.Decision-ready snapshot
ExecuteworksAddBuildMiddlewarerecordableAddPostActionrecordableSConscriptrecursive evalParseFlagsExtendedcovers-I path/-Ipath/-L/-l/-D KEY=VALscript_runtime_harness.pynotesand returns no-op (matches current MockEnv permissiveness)Recommendation, updated
Go. The surface-area estimate held, the architectural design (Action/Builder/Node primitives + recorded-actions ledger) covers the four clusters the MockEnv structurally can't (
Execute, middleware, post-action,SConscript), and the three bugs the spike caught are all behavioural-mismatch one-liners — none of them imply we need a DAG or any of the things we deliberately excluded.Next concrete step: a 2-3 day verification round against 3 real #43 repos. If those land cleanly, the prototype gets rewritten as a fbuild-internal module (under
crates/fbuild-build/src/lite_scons_harness.pyparallelingscript_runtime_harness.py), wired intoscript_runtime.rsvia an opt-in flag, and shipped as the default backend once it gets enough miles.If the verification round hits a
MergeFlashImage-class wall on PlatformIO-defined chip-family builders, the prototype'sbuilder_callsledger tells us exactly which builder names need either native-equivalent mapping or a structured "needs--platformio" fallback. That output also feeds Option B (thepio rundump path) as a known cliff if we end up there instead.The prototype is throwaway code, but the output of the spike — the bug list, the verified API surface, the JSON shape — is what the production design will be built from. Both directions (ship Option E / fall back to Option B) are now decidable with one more verification round.
zackees commentedon Jun 14, 2026
Meta decision — lite-SCons supersedes MockEnv
This is the load-bearing decision for the rest of #553. Promoting it from "Option E spike" to "this is the production path." Recording it here so subsequent PRs have a single source of truth to point at.
Direction
The lite-SCons harness (now shipping opt-in via #581) replaces the MockEnv shim (
script_runtime_harness.py) as the productionextra_scriptsbackend. MockEnv is on a retirement track.Rationale: the spike proved that the SCons API surface PlatformIO scripts actually use is ~12 methods + 3 small classes — small enough that a hand-written sidecar covers it without vendoring SCons or hosting PlatformIO (Options A/B). Lite-SCons is a functional superset of MockEnv:
Executeeffects,AddBuildMiddleware,AddPostActionwith unresolved target templates,SConscriptrecursion);env.get→project_optionsfallthrough,ParseFlagsExtendedspace-form, caller-relativeSConscriptpaths).Keeping both backends long-term is not the plan. Two parallel implementations of the same contract is exactly the kind of drift the monocrate policy exists to prevent — every future feature would have to be wired into both, and the MockEnv path's hard-fail semantics would diverge from the lite path's record-and-record-deferred semantics in subtle ways.
Staged retirement plan
Each step is a separate PR, gated on the previous step landing cleanly.
FBUILD_LITE_SCONS=1selects liteUtilitechAS/amsreader-firmwaregenerator, one Marlin-class withAddBuildMiddleware, one ESP32 withMergeFlashImage); characterise the PlatformIO-builder cliffFBUILD_LITE_SCONSunset now selects lite;FBUILD_LITE_SCONS=0becomes the emergency opt-out for the legacy MockEnv path. Pin existing MockEnv-specific tests to MockEnv via_with_mode(..., false)so they keep covering that path until removalscript_runtime_harness.py, delete the_with_modeoverload'sfalsebranch, deleteLITE_HARNESSvsHARNESSdispatch (only one harness left), delete theFBUILD_LITE_SCONSenv var, retire the MockEnv-specific tests and replace them with lite-SCons equivalentsWhat "supersedes" does NOT mean
The three architectural boundaries lite-SCons keeps stay in place — they're not what MockEnv was solving:
fbuild-header-scanfor that.env.MergeFlashImage,env.PackageJsonFirmware, etc.) implemented natively. Recorded asbuilder_callsentries; fbuild maps known names to nativefbuild-deployops or fails fast with a structured "needs--platformiofor builder X" message.Anything that genuinely needs a real DAG (real
SConscript-driven multi-target builds, content-signature incremental rebuilds, the full PlatformIO Builder system) stays on--platformio. That's the unchanged Option D fallback.What this means for current users
FBUILD_LITE_SCONS=1and get the four extra clusters.FBUILD_LITE_SCONS=0exists as the rollback. Existing builds that worked under MockEnv keep working because lite-SCons is a functional superset.Why now, not later
The honest answer to "should we just keep MockEnv around for safety?" is no: every script that fails on MockEnv today already gets the recommendation to use
--platformio. The lite-SCons path doesn't remove that escape hatch — it just shifts the cliff. Scripts that fail on lite-SCons (e.g. they use a PlatformIO chip-family builder we don't natively map) will fall back to--platformioexactly as they would have under MockEnv. The user-visible behaviour for those scripts is unchanged.What changes is the cliff location: more scripts native-build, fewer scripts fall back to
--platformio. That's the entire point of this issue.Reference
fix(ci): split script_runtime tests under LOC gate + format fixes (#581…
feat(build): opt-in lite-SCons extra_scripts harness (#553)
fix(ci): split script_runtime tests under LOC gate + format fixes (#581…
feat(build): opt-in lite-SCons extra_scripts harness (#553) (#581)
feat(build): default extra_scripts backend to lite-SCons (#553 step 3)
feat(build): default extra_scripts backend to lite-SCons (#553 step 3) (
feat(build): retire MockEnv extra_scripts shim (#553 step 4) (#585)
zackees commentedon Jun 14, 2026
Resolution
Closing — the feasibility question is answered and the chosen approach is implemented and shipped on
main.What this issue asked
What happened
Four options were laid out (A: vendor SCons + PlatformIO; B: real
pio rundump bridge; C: extend the MockEnv shim; D: status-quo--platformiofallback). A fifth option emerged from investigation:The spike against five representative patterns landed cleanly, surfacing three behavioural-mismatch bugs (
env.get → project_optionsfallthrough,ParseFlagsExtendedspace form, caller-relativeSConscriptpaths) — all fixed in the same investigation pass.Then a four-step staged retirement turned the spike into the production backend:
lite_scons_harness.pyalongside MockEnv;FBUILD_LITE_SCONS=1opts inFBUILD_LITE_SCONS=0is the emergency opt-outscript_runtime_harness.py; remove the env var; single backendWhere the answer lives
Architectural boundaries (still falling through to
--platformio)Three categories the lite backend deliberately doesn't model:
fbuild-header-scan.env.MergeFlashImage,env.PackageJsonFirmware, etc.). Recorded asbuilder_callsentries; fbuild maps known names to nativefbuild-deployops or fails fast with a structured "needs--platformiofor builder X" message.Anything in those three categories is the same
--platformiofallback the issue's Option D referenced — that escape hatch is unchanged.Deferred follow-up (not gating this closure)
Step 2 — verification round against 3 real #43 repos. The spike covered 5 representative synthetic patterns + the unit + integration tests in
crates/fbuild-build/tests/lite_scons_acceptance.rs. A 2-3 day spike againstUtilitechAS/amsreader-firmware(generator script), one Marlin-class withAddBuildMiddleware, and one ESP32 withMergeFlashImagewould characterise the PlatformIO-chip-family-builder cliff against real-world code. Worth doing; not blocking on this issue.If that cliff turns out to bite real users in practice, the prototype's
builder_callsledger structure tells us exactly which builders need native equivalents infbuild-deploy. The structural pieces are already in place.cc @zackees
chore(deps): bump fbuild 2.2.27 -> 2.2.28 (PyPI 2026-06-15) (#3056)