Skip to content

Lift the netstandard2.0 floor off the tooling stack via an out-of-process MSBuild bridge#447

Draft
ChrisonSimtian wants to merge 10 commits into
Fallout-build:mainfrom
ChrisonSimtian:tooling/lift-ns20-floor
Draft

Lift the netstandard2.0 floor off the tooling stack via an out-of-process MSBuild bridge#447
ChrisonSimtian wants to merge 10 commits into
Fallout-build:mainfrom
ChrisonSimtian:tooling/lift-ns20-floor

Conversation

@ChrisonSimtian

Copy link
Copy Markdown
Collaborator

⚠️ Breaking changeFallout.Tooling / Fallout.Tooling.Generator drop netstandard2.0 (now net8.0;net10.0 and net10.0) and Fallout.MSBuildTasks drops net472. A consumer referencing those assemblies directly from a netstandard2.0/net472 project must move to net8+/net10. The MSBuild integration's behaviour is preserved for VS2019/2022 via a new out-of-process bridge (below).

Implements Lever 2 of #441 — frees the tooling stack from the ns2.0 floor so Fallout.Core (ns2.1) can absorb Configure<T> / AbsolutePath / the tool contracts (de-static Phase 2b/2c/2e). Design recorded in ADR-0009 (in this PR).

How

netstandard2.0 is the only TFM that can't consume Fallout.Core (ns2.1), and the floor came solely from Fallout.MSBuildTasks's net472 target (loaded by full-framework VS MSBuild) referencing Fallout.Tooling.

  • Task logic extracted to a net10 Fallout.MSBuildTasks.Engine.
  • Core MSBuild (dotnet/SDK): in-process net10 task — the hot path, keeps ALC isolation.
  • Full-framework MSBuild (VS2019/2022): a thin net472 ToolTask bridge that shells out to a standalone net10 worker (Fallout.MSBuildTasks.Worker) — zero Fallout deps, so Tooling no longer needs ns2.0.
  • .targets selects by $(MSBuildRuntimeType); packaged into build\netcore (task) / build\netfx (bridge + worker).
  • Fallout.Tooling keeps a net8 flavour for Fallout.ProjectModel's down-level targets — net8 consumes ns2.1, so the floor is still lifted.

The bridge is time-boxed: delete it once VS2026's .NET MSBuild can load net10 tasks in-process.

Verification ⚠️ (why this is a draft)

  • ✅ Full solution build + entire test suite green; the worker is smoke-tested out-of-process.
  • Not yet verified: the consumer task path + Pack output, and the net472 VS full-framework bridge — these aren't exercised on non-Windows or by this repo's dogfood (which runs FalloutTasksEnabled=False). Needs a Windows + VS + Pack smoke test before un-drafting.

🤖 Generated with Claude Code

ChrisonSimtian and others added 6 commits June 30, 2026 20:17
…MSBuild bridge

Decision record for freeing Fallout.Tooling + Fallout.Tooling.Generator from the ns2.0
floor that Fallout.MSBuildTasks's net472 target imposes. Extract task logic into a net10
engine; serve dotnet/SDK builds with an in-proc net10 task and VS2019/2022 full-framework
MSBuild with a thin net472 ToolTask bridge that shells out to a standalone net10 worker.
Unblocks lifting AbsolutePath / Host / IFalloutBuild into Fallout.Core. Implements Lever 2
of Fallout-build#441 (Lever 1 / SourceGenerators cone is Fallout-build#442, independent). Bridge is time-boxed and
deletable once VS2026's .NET MSBuild lands.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First step of ADR-0009. Lifts the bodies of CodeGenerationTask / EmbedPackagesForSelfContained
/ PackPackageTools into an MSBuild-free, net10-only engine (CodeGenerationEngine +
PackageToolingEngine, plain data in/out via PackagedToolFile). This is the shared home both the
in-process net10 task and the out-of-process worker will call, so the net472 host can later become
a thin bridge and Fallout.Tooling can drop the ns2.0 floor.

Additive — nothing consumes the engine yet; the existing tasks are untouched. Solution + full
build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Second step of ADR-0009. Fallout.MSBuildTasks.Protocol is a dependency-free contract
(netstandard2.0;net10) of request/response POCOs + System.Text.Json helpers, referenced by
both the net472 bridge and the net10 worker. Fallout.MSBuildTasks.Worker is the standalone
net10 console the bridge shells out to: <verb> <input.json> <output.json>, dispatching codegen
/ embed-packages / pack-tools to the engine and writing a WorkerResponse (exit 0/1/2).

Additive — nothing invokes the worker yet. Smoke-tested out-of-process: bad-invocation → exit 2;
embed-packages against a real project.assets.json → exit 0 with a valid package list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Third step of ADR-0009. Fallout.MSBuildTasks.Bridge (net472, RootNamespace Fallout.MSBuildTasks
so the task type names match the .targets UsingTask) is three ToolTask shims —
CodeGenerationTask / EmbedPackagesForSelfContainedTask / PackPackageToolsTask — over a base
WorkerBridgeTask that writes inputs to a temp JSON, shells out to `dotnet
Fallout.MSBuildTasks.Worker.dll <verb> in out` (dotnet host via DOTNET_HOST_PATH, else PATH),
and reads item outputs back. It references only the dependency-free Protocol — no Fallout.Tooling.

To suit ToolTask, the worker now emits messages on stdout and errors on stderr (LogStandardErrorAsError),
with the exit code as success; the output JSON carries only item outputs. WorkerResponse trimmed accordingly.

Builds green (worker net10, bridge net472). The flip (TFMs net10-only + .targets runtime switch) is next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… .targets

The flip (final step of ADR-0009). The in-proc CodeGeneration / EmbedPackages / PackPackageTools
tasks now delegate to Fallout.MSBuildTasks.Engine and Fallout.MSBuildTasks goes net10-only.
Fallout.Tooling drops netstandard2.0 (now net8.0;net10.0 — net8 kept for Fallout.ProjectModel's
down-level targets; net8 consumes ns2.1, so Core absorption is unblocked) and
Fallout.Tooling.Generator goes net10-only. The .targets picks the assembly by $(MSBuildRuntimeType):
Full (VS2019/2022) -> the net472 bridge, Core (dotnet/SDK) -> the in-proc net10 task.

ns2.0 — the only TFM that can't consume Fallout.Core (ns2.1) — is gone from the tooling stack, so
AbsolutePath / Host / Configure<T> / the tool contracts can now move into Core (Phase 2b/2c/2e).

Full solution build + test suite green (StronglyTypedSolution snapshot refreshed for the four new
projects). VS full-framework path can't be exercised on this OS; the net10/CLI path is verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…x folder

Fallout.Common (the consumer-facing build package) packed MSBuildTasks's net472 publish output
into build\netfx; with MSBuildTasks now net10-only that glob is dead. Replace it with the net472
bridge plus the net10 worker (same folder, so the bridge finds Fallout.MSBuildTasks.Worker.dll
beside itself). build\netcore still ships the in-proc net10 task. Also point the dogfood's inert
FalloutTasksDirectory at the real net10 publish path (was a stale net8.0).

NOTE: the consumer task path (especially full-framework VS) and `Pack` output are not exercised by
this repo's build/test (the dogfood runs with FalloutTasksEnabled=False), and the net472/VS path
can't be smoke-tested on non-Windows — both need a Windows + Pack verification before merge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ChrisonSimtian ChrisonSimtian added breaking-change Change is breaking — requires major version bump per CLAUDE.md semver policy. target/2026 Targets the 2026 calendar-version line (current). See ADR-0004. labels Jun 30, 2026
@ChrisonSimtian

Copy link
Copy Markdown
Collaborator Author

Bridge smoke test (Windows + VS2019/2022 full-framework MSBuild): the Full path fails — STJ binding.

Ran the consumer codegen path on this branch (commit 7220ab5) across all three runtimes:

  • dotnet / SDK MSBuild (Core, in-proc net10 task): ✅ generates valid output
  • VS2022 MSBuild.exe (Full, net472 bridge): ❌
  • VS2019 MSBuild.exe (Full, net472 bridge): ❌ (identical)

The bridge throws before it ever spawns the worker:

error MSB6003: System.IO.FileNotFoundException: Could not load file or assembly
'System.Text.Json, Version=10.0.0.0, ... PublicKeyToken=cc7b13ffcd2ddd51'
  at Fallout.MSBuildTasks.Protocol.WorkerJson.Serialize[T]
  at CodeGenerationTask.BuildRequestJson() -> WorkerBridgeTask.GenerateCommandLineCommands()

Root cause: Fallout.MSBuildTasks.Protocol serializes with System.Text.Json. The packaged STJ is assembly version 10.0.0.8; the code references 10.0.0.0. The bridge runs in-process inside .NET-Framework MSBuild.exe, where fusion needs an exact version or a binding redirect — neither is present (and a Bridge.dll.config redirect isn't honored for an in-proc task; only the host MSBuild.exe.config is). On .NET Core, roll-forward hides this for the in-proc task and the worker, so only Full breaks.

This contradicts ADR-0009 §3 ("the bridge references only Microsoft.Build.* + the BCL — zero Fallout dependencies"): the Protocol's STJ dependency reintroduces exactly the version-sensitive in-proc dependency the bridge design set out to avoid.

Fix (pushed): WorkerJson now uses the in-box DataContractJsonSerializer (System.Runtime.Serialization); STJ PackageReference dropped from the Protocol project. STJ no longer ships in build/netfx. Re-ran all three drivers: Core, VS2019, VS2022 now produce byte-identical generated output.

Repro runbook: docs/agents/msbuild-bridge-smoke-test.md.

ChrisonSimtian and others added 4 commits July 1, 2026 10:12
…net472 bridge loads

The net472 bridge loads Fallout.MSBuildTasks.Protocol in-process inside full-framework
MSBuild.exe (VS2019/2022). The Protocol serialized with System.Text.Json, whose packaged
assembly version (10.0.0.8) never matches the compile-time reference (10.0.0.0); .NET
Framework fusion needs an exact match or a host binding redirect, and MSBuild.exe has
neither for STJ. The bridge threw FileNotFoundException in WorkerJson.Serialize before it
could spawn the worker. .NET Core roll-forward masked this on the in-proc task and the
worker, so only the Full path broke and CI (which runs FalloutTasksEnabled=False) missed it.

Switch WorkerJson to the in-box DataContractJsonSerializer (System.Runtime.Serialization)
and drop the System.Text.Json PackageReference. The DTOs are trivial POCOs that round-trip
cleanly, both ends share WorkerJson so the wire format stays symmetric, and STJ no longer
ships in build/netfx. This makes the bridge genuinely "Microsoft.Build.* + BCL only" as
ADR-0009 intends. Verified: dotnet (Core), VS2019 and VS2022 (Full) now produce
byte-identical codegen output.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Documents how to verify the full-framework (VS2019/2022) MSBuild bridge from ADR-0009:
which MSBuild runtime exercises which path, the homelab VM spec, VS version currency, the
exact repro steps, and the System.Text.Json binding defect plus its fix. This path is
Windows-only and not covered by CI (the dogfood runs FalloutTasksEnabled=False), so it
needs a manual smoke test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Covers the wire contract the net472 bridge and net10 worker share via WorkerJson:
- round-trip each DTO (CodegenRequest, EmbedPackagesRequest, PackToolsRequest, WorkerResponse,
  including empty/default collections) through DataContractJsonSerializer;
- assert the Protocol assembly references no System.Text.Json — the regression guard for the
  bridge binding failure (STJ can't load in-process under full-framework MSBuild.exe). A round-trip
  test alone can't catch that (net10 roll-forward and the test host's binding redirects both mask
  it); the dependency invariant does, cross-platform. End-to-end bridge behaviour stays covered by
  the Windows-only smoke test in docs/agents/msbuild-bridge-smoke-test.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adding Fallout.MSBuildTasks.Protocol.Specs to fallout.slnx adds one project to the
StronglyTypedSolutionGenerator output; refresh the Verify snapshot to match (one new line).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ChrisonSimtian

Copy link
Copy Markdown
Collaborator Author

TLDR; in order to break up the whole god-class cluster, I need to break a few things open, this being one of them.
Isolated this, broke out the legacy stuff into a standalone-bridge to service windows vs2019 and windows 2022 (macos vs2022 uses netcore already Oo)

Also, interesting read on VS support:
https://learn.microsoft.com/en-us/visualstudio/releases/2026/servicing-vs#support-for-older-versions
VS2026: End of Support = 2028
VS2022: End of Support = 2032 (!)
VS2019: End of Support = 2029 (!)

Classic Microsoft move... I'd say with that in mind: we do a bridge. We keep it side-by-side as long as we can but drop it latest 2032 (oh god thats in 6 years from now!)

@ChrisonSimtian ChrisonSimtian added the help wanted Extra attention is needed label Jul 1, 2026
@ChrisonSimtian

Copy link
Copy Markdown
Collaborator Author

This also needs some testing. So please be my guest, run it against all sorts of pre 2026 VS instances :-)

@ChrisonSimtian

Copy link
Copy Markdown
Collaborator Author

@dennisdoomen not quite tidy but have a look and let me know what you think.
I want to lift this into its own little library to unlock us doing a lift & shift for the rest of fallout. Plus this whole thing is going away with vs2026 on windows (already gone on vs2022 for macos funnily enough, I assume rider and vscode never had this problem because they're not based on a 100yr old codebase ...)

@ChrisonSimtian ChrisonSimtian added the Dont Merge This cant be merged for various reasons (i.e. its being hold back, its broken, etc) label Jul 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking-change Change is breaking — requires major version bump per CLAUDE.md semver policy. Dont Merge This cant be merged for various reasons (i.e. its being hold back, its broken, etc) help wanted Extra attention is needed target/2026 Targets the 2026 calendar-version line (current). See ADR-0004.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant