diff --git a/build/_build.csproj b/build/_build.csproj index cee68e3d..9eb83e5b 100644 --- a/build/_build.csproj +++ b/build/_build.csproj @@ -17,7 +17,7 @@ False - $(MSBuildThisFileDirectory)\..\src\Fallout.MSBuildTasks\bin\Debug\net8.0\publish + $(MSBuildThisFileDirectory)\..\src\Fallout.MSBuildTasks\bin\Debug\net10.0\publish diff --git a/docs/adr/0009-lift-ns20-floor-via-msbuild-bridge.md b/docs/adr/0009-lift-ns20-floor-via-msbuild-bridge.md new file mode 100644 index 00000000..11670c3a --- /dev/null +++ b/docs/adr/0009-lift-ns20-floor-via-msbuild-bridge.md @@ -0,0 +1,62 @@ +# ADR-0009: Lift the `netstandard2.0` floor off the tooling stack via an out-of-process MSBuild bridge + +## Status + +Proposed (2026-06-30). Implements the **Lever 2** half of [#441](https://github.com/Fallout-build/Fallout/issues/441) ("move off the ns2.0/net472 floor"); the **Lever 1** half (freeing the `Fallout.SourceGenerators` Roslyn cone) is tracked separately in [#442](https://github.com/Fallout-build/Fallout/pull/442) and is an independent cone — the two do not interact. Unblocks the deferred onion lifts (`AbsolutePath`, `Host`, and lifting `IFalloutBuild` into `Fallout.Core`). + +## Context + +Two project graphs are pinned to `netstandard2.0`, for unrelated reasons: + +- **Cone A — `Fallout.SourceGenerators`** is a Roslyn source generator and is ns2.0 by compiler requirement. Its references (`Build.Shared`, `Solution`, `Persistence.Solution`) do **not** touch the tooling stack. Addressed by Lever 1 (#442); out of scope here. +- **Cone B — `Fallout.MSBuildTasks`** targets `net10.0;net472`. Its `net472` flavour exists because the MSBuild tasks it ships are loaded **into the consumer's MSBuild host**, and the in-IDE build of VS 2019/2022 (and `msbuild.exe`) is **full-framework** MSBuild, which can only load a `net472` task assembly in-process. That `net472` reference is the **sole** force dragging `Fallout.Tooling` and `Fallout.Tooling.Generator` down to a `net10.0;netstandard2.0` multi-target: + + ``` + Fallout.MSBuildTasks (net10.0;net472) ──► Fallout.Tooling (net10.0;netstandard2.0) + └─► Fallout.Tooling.Generator (netstandard2.0) + ``` + +Because `Fallout.Tooling` is ns2.0 and `Fallout.Core` is ns2.1, anything `Tooling` must consume cannot live in Core. That is the direct cause of: + +- the tool-execution contract living in the dedicated ns2.0 leaves `Fallout.Application.Tooling.Execution` / `.Requirements` instead of Core; +- `Configure` being un-liftable to Core (Phase 2a deferral); +- `AbsolutePath` (2b) and `Host` (2c) being un-liftable, which in turn blocks lifting `IFalloutBuild` into Core (2e). + +The three concrete tasks (`CodeGenerationTask`, `EmbedPackagesForSelfContainedTask`, `PackPackageToolsTask`, all extending `ContextAwareTask : Microsoft.Build.Utilities.Task`) are build-time **file producers** — tool-wrapper codegen, package embedding, tool packing. None needs a chatty in-process MSBuild API; each is expressible as *inputs → work → outputs + diagnostics*. That makes them safe to run out-of-process. + +`ContextAwareTask` carries a `CustomAssemblyLoader : AssemblyLoadContext` whose only job is to isolate Fallout's dependency closure from MSBuild's own assemblies inside the host — a known-fragile piece that exists purely because the work runs in-process. + +## Decision + +Extract the task **logic** into a `net10` engine and front it with two thin adapters, selected by MSBuild runtime: + +``` +Fallout.MSBuildTasks.Engine (net10) ← the real work; references net10 Tooling + Generator + ├─ Fallout.MSBuildTasks (net10) in-proc Task → calls the engine directly + └─ Fallout.MSBuildTasks.Bridge (net472) ToolTask → shells out to a standalone net10 worker +``` + +1. **`Fallout.Tooling` and `Fallout.Tooling.Generator` become `net10`-only.** With the `net472` consumer gone, the floor is lifted. `Fallout.Core` (or a single set of net-modern contracts) can then absorb the tool contracts, `Configure`, and `AbsolutePath` — unblocking 2b/2c/2e. +2. **Core MSBuild hosts (`dotnet` / SDK build) load the in-proc `net10` task** — the hot path, no process spawn. It keeps the `ContextAwareTask`/`AssemblyLoadContext` isolation (loading net10 Tooling + Serilog/STJ into SDK-MSBuild can still clash). +3. **Full-framework MSBuild hosts (VS 2019/2022, `msbuild.exe`) load the `net472` bridge** — a `Microsoft.Build.Utilities.ToolTask` that shells out via `System.Diagnostics.Process` to a **standalone net10 worker console** (`Fallout.MSBuildTasks.Worker`). `ToolTask` is purpose-built for this: process launch, output capture, cancellation, and `LogEventsFromTextOutput` to surface worker diagnostics in the VS error list. + - The bridge references **only** `Microsoft.Build.*` + the BCL — **zero Fallout dependencies**, no `AssemblyLoadContext` dance. + - The worker is **standalone**, not a verb on the `fallout` CLI — this is a time-boxed workaround and must add no surface to the long-lived CLI. +4. **`.targets` selects the adapter on `$(MSBuildRuntimeType)`** (`Core` → in-proc task; `Full` → bridge). The legacy `Nuke*`→`Fallout*` property bridging and `FalloutTasksEnabled` flag are untouched. +5. **`dotnet` host discovery** uses `$(DOTNET_HOST_PATH)` (set by the SDK), falling back to PATH, passed to the bridge so it can launch `dotnet Fallout.MSBuildTasks.Worker.dll`. +6. **I/O marshalling stays boring** — `ITaskItem[]`/properties (e.g. `FalloutSpecificationFiles`) serialize to a temp JSON arg file; the worker writes its output files plus a small results JSON the bridge reads back for output item groups. + +The net10 runtime being present at the consumer's build is **assumed, not mitigated** — Fallout is a net10 build system; if net10 were absent, nothing would run. + +## Consequences + +- **Unblocks the de-static "lift to Core" track** (2b `AbsolutePath`, 2c `Host`, 2e `IFalloutBuild`) and lets the ns2.0 tooling leaves (`Application.Tooling.Execution`/`.Requirements`) eventually fold back into Core. +- **Deletes the fragile in-host ALC isolation for VS** — the bridge has no Fallout deps to isolate. +- **Adds a per-invocation process spawn for full-framework builds only.** These are build-time, run-once-per-build operations; the cost is negligible and the hot `dotnet`/CLI path keeps its in-proc speed. +- **The bridge is a fenced, deletable legacy adapter** for VS 2019/2022. VS 2026's move to a 64-bit/.NET MSBuild is expected to load net10 tasks in-process directly, at which point `Full` hosts also get a modern runtime and the bridge is dead weight. **Retirement = delete the `.Bridge` project + the `Full` branch in `.targets`.** Keep it past then only if it is costing nothing. +- **Behaviour parity is the bar:** VS-driven consumer builds (codegen, package embed, tool pack) must produce identical output and identical error-list diagnostics through the bridge as through the in-proc task. + +## Alternatives considered + +- **Drop `net472` outright** (the original Lever 2 framing). Simplest, but breaks the Fallout MSBuild tasks for in-VS / `msbuild.exe` builds on VS 2019/2022. Rejected: the bridge preserves those at low, time-boxed cost. +- **Unify everyone onto the out-of-process worker** (no in-proc task). One code path, but pays the process-spawn cost on every `dotnet`/CLI build and discards the existing ALC isolation that already works. Rejected in favour of in-proc-for-Core, bridge-for-Full. +- **MSBuild task host (`Runtime="NET"`)** to run a net-core task from full-framework MSBuild. Not a reliable cross-runtime path from net472; the explicit shell-out is more robust and fully decoupled. diff --git a/docs/adr/README.md b/docs/adr/README.md index a932561f..0ed0bd7f 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -40,3 +40,4 @@ If you change a decision, do NOT silently rewrite the old ADR — add a new one | [0004](0004-calendar-versioning-and-dual-pace-channels.md) | Calendar versioning + dual-pace channels (edge/stable) + experimental APIs | Accepted (§3 amended by 0007; channel ladder §2 superseded by 0008) | | [0007](0007-cut-release-branch-on-demand.md) | Cut `release/YYYY` on demand, not preemptively | Accepted | | [0008](0008-collapse-experimental-into-main.md) | Collapse `experimental` into `main`; `main` is the sole prerelease lane | Accepted | +| [0009](0009-lift-ns20-floor-via-msbuild-bridge.md) | Lift the `netstandard2.0` floor off the tooling stack via an out-of-process MSBuild bridge | Proposed | diff --git a/docs/agents/msbuild-bridge-smoke-test.md b/docs/agents/msbuild-bridge-smoke-test.md new file mode 100644 index 00000000..6b95320c --- /dev/null +++ b/docs/agents/msbuild-bridge-smoke-test.md @@ -0,0 +1,120 @@ +# MSBuild bridge smoke test (PR #447 / ADR-0009) + +Runbook for verifying the **full-framework MSBuild bridge** added by [ADR-0009](../adr/0009-lift-ns20-floor-via-msbuild-bridge.md) +(branch `tooling/lift-ns20-floor`). CI does **not** cover this path — the repo dogfood runs +`FalloutTasksEnabled=False`, and the bridge only runs under full-framework `MSBuild.exe`, which is +**Windows-only**. So it needs a manual, Windows + VS smoke test. + +## What exercises which path + +The `.targets` select by `$(MSBuildRuntimeType)`: + +| Driver | RuntimeType | Path | Covered by | +|---|---|---|---| +| `dotnet` / SDK MSBuild | `Core` | in-proc net10 task (`build/netcore`) | `dotnet pack`, CI | +| VS 2019 / 2022 `MSBuild.exe` | `Full` | **net472 bridge → net10 worker** (`build/netfx`) | **this runbook only** | +| VS 2026 (v18) `MSBuild.exe` | `Core` | in-proc net10 task | same as `dotnet` | + +**VS 2026 does not exercise the bridge** — its MSBuild is .NET-based, so it takes the in-proc path. +The bridge is reachable *only* via full-framework MSBuild (VS 2019/2022 or Build Tools). Per the ADR +the bridge is **time-boxed**: delete it once consumers are off full-framework MSBuild. + +## VS version currency (as of 2026-07) + +| Version | Status | Bridge? | +|---|---|---| +| VS 2026 (v18) | current | no (Core) | +| VS 2022 (v17) | prior gen, widely used | **yes** | +| VS 2019 (v16) | legacy, near EOL; its MSBuild can't resolve the net10 SDK | **yes** | +| VS for Mac | **retired 2024-08-31**; never had full-framework MSBuild | n/a | + +## Homelab VM spec + +A parked Windows image, spun up only when touching the bridge / MSBuild-task code: + +- Windows 10/11 or Windows Server. +- **VS Build Tools 2022** with the `Microsoft.Component.MSBuild` workload — the BuildTools SKU is + enough; full VS is not needed. (Optionally Build Tools 2019 too, for the oldest supported path.) +- The pinned **.NET SDK** (`global.json`, currently `10.0.100`) — provides `dotnet` for the worker. +- `git`. + +Does **not** need VS 2026 — that only re-tests the `Core` path the dev box's `dotnet` already covers. + +## How to run + +From a worktree on the PR branch (`MSBuild.exe` runs must use PowerShell, not Git Bash — Bash +mangles `/switch` args and `/c/...` paths): + +```powershell +# 1. Publish the three task projects (Release) +dotnet publish src/Fallout.MSBuildTasks/Fallout.MSBuildTasks.csproj -c Release +dotnet publish src/Fallout.MSBuildTasks.Bridge/Fallout.MSBuildTasks.Bridge.csproj -c Release +dotnet publish src/Fallout.MSBuildTasks.Worker/Fallout.MSBuildTasks.Worker.csproj -c Release +dotnet build src/Fallout.SourceGenerators/Fallout.SourceGenerators.csproj -c Release + +# 2. Pack Fallout.Common (gate on _IsPacking) and extract its build/ assets +dotnet pack src/Fallout.Common/Fallout.Common.csproj -c Release -p:_IsPacking=true -o +# unzip \Fallout.Common.*.nupkg -> , giving \build\{netcore,netfx} + +# 3. Consumer that imports \build\Fallout.Common.{props,targets}, sets a tool spec in +# @(FalloutSpecificationFiles), and runs the FalloutCodeGeneration target. A non-SDK .proj +# lets BOTH VS2019 and VS2022 evaluate it (VS2019's MSBuild can't resolve the net10 SDK). + +$env:DOTNET_HOST_PATH = "C:\Program Files\dotnet\dotnet.exe" # so the bridge can launch the worker +& "\MSBuild\Current\Bin\MSBuild.exe" Consumer.proj -t:FalloutCodeGeneration -p:PkgBuild=\build +``` + +**Pass criteria:** the `Full`-path runs produce the *same* generated output as `dotnet` (`Core`), +with the worker process actually spawned and no load/protocol errors. + +## Result — 2026-07-01 (commit 7220ab5a) + +| Driver | Outcome | +|---|---| +| `dotnet` (Core) | ✅ generated valid `Git.Generated.cs` (in-proc net10 task) | +| VS 2022 `MSBuild.exe` (Full) | ❌ bridge crashes before spawning the worker | +| VS 2019 `MSBuild.exe` (Full) | ❌ identical crash | + +**Defect — the net472 bridge can't load `System.Text.Json`.** + +``` +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` (`WorkerProtocol.cs`) 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. There is none (and a `Bridge.dll.config` redirect would not help — + fusion only reads the host `MSBuild.exe.config` for in-proc assemblies). On .NET Core both the + in-proc task and the worker bind fine via roll-forward, which is why only `Full` fails. +- 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 design set out to avoid. + +**Fix direction:** make `WorkerJson` framework-safe so the net472 bridge carries no redirect- +sensitive dependency — e.g. serialize the (trivial) DTOs with the in-box `DataContractJsonSerializer`, +or hand-roll, keeping STJ (if at all) on the net10 worker side only. The blocker is the bridge's +*outbound* serialization (`BuildRequestJson`); the worker (net10) is fine. + +### Fixed — 2026-07-01 + +`WorkerJson` (`Fallout.MSBuildTasks.Protocol`) now uses the in-box `DataContractJsonSerializer` +(`System.Runtime.Serialization`) instead of `System.Text.Json`, and the STJ `PackageReference` is +removed from the Protocol project. STJ no longer ships in `build/netfx`, so the net472 bridge loads +with no redirect-sensitive dependency. Re-run: + +| Driver | Outcome | +|---|---| +| `dotnet` (Core) | ✅ `Git.Generated.cs` | +| VS 2022 `MSBuild.exe` (Full) | ✅ bridge → worker, output **byte-identical** to Core | +| VS 2019 `MSBuild.exe` (Full) | ✅ identical | + +All three generated files share one SHA256 — behaviour parity (ADR-0009 §"Consequences") holds. + +> No automated tests cover the bridge/worker serialization — this smoke test is the only coverage. +> A `WorkerJson` round-trip unit test in a `Fallout.MSBuildTasks.Protocol.Specs` sibling would catch +> a regression without the manual VS run. diff --git a/fallout.slnx b/fallout.slnx index 808eb0ea..7d68a032 100644 --- a/fallout.slnx +++ b/fallout.slnx @@ -23,6 +23,10 @@ + + + + @@ -42,6 +46,7 @@ + diff --git a/src/Fallout.Common/Fallout.Common.csproj b/src/Fallout.Common/Fallout.Common.csproj index b0c922b1..c85f8dc5 100644 --- a/src/Fallout.Common/Fallout.Common.csproj +++ b/src/Fallout.Common/Fallout.Common.csproj @@ -35,8 +35,15 @@ + - + + + diff --git a/src/Fallout.MSBuildTasks.Bridge/CodeGenerationTask.cs b/src/Fallout.MSBuildTasks.Bridge/CodeGenerationTask.cs new file mode 100644 index 00000000..b639db26 --- /dev/null +++ b/src/Fallout.MSBuildTasks.Bridge/CodeGenerationTask.cs @@ -0,0 +1,33 @@ +using System.Linq; +using Fallout.MSBuildTasks.Protocol; +using Microsoft.Build.Framework; + +namespace Fallout.MSBuildTasks; + +/// Full-framework shim for the tool-wrapper code generation task. +public sealed class CodeGenerationTask : WorkerBridgeTask +{ + [Required] + public ITaskItem[] SpecificationFiles { get; set; } + + [Required] + public string BaseDirectory { get; set; } + + public bool UseNestedNamespaces { get; set; } + + public string BaseNamespace { get; set; } + + public bool UpdateReferences { get; set; } + + protected override string Verb => WorkerVerbs.Codegen; + + protected override string BuildRequestJson() + => WorkerJson.Serialize(new CodegenRequest + { + SpecificationFiles = SpecificationFiles.Select(x => x.GetMetadata("FullPath")).ToArray(), + BaseDirectory = BaseDirectory, + UseNestedNamespaces = UseNestedNamespaces, + BaseNamespace = BaseNamespace, + UpdateReferences = UpdateReferences, + }); +} diff --git a/src/Fallout.MSBuildTasks.Bridge/EmbedPackagesForSelfContainedTask.cs b/src/Fallout.MSBuildTasks.Bridge/EmbedPackagesForSelfContainedTask.cs new file mode 100644 index 00000000..d2099cc7 --- /dev/null +++ b/src/Fallout.MSBuildTasks.Bridge/EmbedPackagesForSelfContainedTask.cs @@ -0,0 +1,27 @@ +using System.Linq; +using Fallout.MSBuildTasks.Protocol; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Fallout.MSBuildTasks; + +/// Full-framework shim for resolving packages to embed for self-contained single-file builds. +public sealed class EmbedPackagesForSelfContainedTask : WorkerBridgeTask +{ + [Required] + public string ProjectAssetsFile { get; set; } + + [Required] + public string TargetFramework { get; set; } + + [Output] + public ITaskItem[] TargetOutputs { get; set; } + + protected override string Verb => WorkerVerbs.EmbedPackages; + + protected override string BuildRequestJson() + => WorkerJson.Serialize(new EmbedPackagesRequest { ProjectAssetsFile = ProjectAssetsFile }); + + protected override void ConsumeResponse(WorkerResponse response) + => TargetOutputs = response.Files.Select(x => (ITaskItem)new TaskItem(x)).ToArray(); +} diff --git a/src/Fallout.MSBuildTasks.Bridge/Fallout.MSBuildTasks.Bridge.csproj b/src/Fallout.MSBuildTasks.Bridge/Fallout.MSBuildTasks.Bridge.csproj new file mode 100644 index 00000000..230d2daa --- /dev/null +++ b/src/Fallout.MSBuildTasks.Bridge/Fallout.MSBuildTasks.Bridge.csproj @@ -0,0 +1,26 @@ + + + + net472 + false + + Fallout.MSBuildTasks + Fallout.MSBuildTasks.Bridge + + + + + + + + + + + + + diff --git a/src/Fallout.MSBuildTasks.Bridge/PackPackageToolsTask.cs b/src/Fallout.MSBuildTasks.Bridge/PackPackageToolsTask.cs new file mode 100644 index 00000000..cb2f6d5c --- /dev/null +++ b/src/Fallout.MSBuildTasks.Bridge/PackPackageToolsTask.cs @@ -0,0 +1,41 @@ +using System.Linq; +using Fallout.MSBuildTasks.Protocol; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Fallout.MSBuildTasks; + +/// Full-framework shim for collecting a package's tool files (with pack metadata). +public sealed class PackPackageToolsTask : WorkerBridgeTask +{ + [Required] + public string ProjectAssetsFile { get; set; } + + [Required] + public string NuGetPackageRoot { get; set; } + + [Required] + public string TargetFramework { get; set; } + + [Output] + public ITaskItem[] TargetOutputs { get; set; } + + protected override string Verb => WorkerVerbs.PackTools; + + protected override string BuildRequestJson() + => WorkerJson.Serialize(new PackToolsRequest + { + ProjectAssetsFile = ProjectAssetsFile, + NuGetPackageRoot = NuGetPackageRoot, + TargetFramework = TargetFramework, + }); + + protected override void ConsumeResponse(WorkerResponse response) + => TargetOutputs = response.ToolFiles.Select(x => + { + var item = new TaskItem(x.File); + item.SetMetadata("BuildAction", x.BuildAction); + item.SetMetadata("PackagePath", x.PackagePath); + return (ITaskItem)item; + }).ToArray(); +} diff --git a/src/Fallout.MSBuildTasks.Bridge/WorkerBridgeTask.cs b/src/Fallout.MSBuildTasks.Bridge/WorkerBridgeTask.cs new file mode 100644 index 00000000..c95d7c52 --- /dev/null +++ b/src/Fallout.MSBuildTasks.Bridge/WorkerBridgeTask.cs @@ -0,0 +1,105 @@ +using System; +using System.IO; +using System.Linq; +using Fallout.MSBuildTasks.Protocol; +using Microsoft.Build.Utilities; + +namespace Fallout.MSBuildTasks; + +/// +/// Base for the full-framework MSBuild task shims. Serializes the task inputs to a temp JSON file, +/// shells out to the net10 worker (dotnet Fallout.MSBuildTasks.Worker.dll <verb> in out), +/// and lets surface the worker's stdout/stderr as build messages/errors. +/// Item outputs (if any) are read back from the worker's output JSON. +/// +public abstract class WorkerBridgeTask : ToolTask +{ + private string _inputPath; + private string _outputPath; + + protected WorkerBridgeTask() + { + // Worker errors arrive on stderr; treat them as build errors so they reach the VS error list. + LogStandardErrorAsError = true; + } + + /// The worker verb (see WorkerVerbs). + protected abstract string Verb { get; } + + /// Serialize this task's inputs to the worker request JSON. + protected abstract string BuildRequestJson(); + + /// Map the worker's item outputs back onto this task's [Output] properties. + protected virtual void ConsumeResponse(WorkerResponse response) { } + + protected override string ToolName => "dotnet"; + + protected override string GenerateFullPathToTool() => DotNetHost.Resolve(); + + private string WorkerDll => Path.Combine( + Path.GetDirectoryName(new Uri(typeof(WorkerBridgeTask).Assembly.Location).LocalPath) ?? ".", + "Fallout.MSBuildTasks.Worker.dll"); + + protected override string GenerateCommandLineCommands() + { + _inputPath = Path.GetTempFileName(); + _outputPath = Path.GetTempFileName(); + File.WriteAllText(_inputPath, BuildRequestJson()); + return $"\"{WorkerDll}\" {Verb} \"{_inputPath}\" \"{_outputPath}\""; + } + + public override bool Execute() + { + try + { + var succeeded = base.Execute(); + if (succeeded && _outputPath != null && File.Exists(_outputPath)) + { + var json = File.ReadAllText(_outputPath); + if (!string.IsNullOrWhiteSpace(json)) + ConsumeResponse(WorkerJson.Deserialize(json)); + } + + return succeeded && !Log.HasLoggedErrors; + } + finally + { + TryDelete(_inputPath); + TryDelete(_outputPath); + } + } + + private static void TryDelete(string path) + { + try + { + if (path != null && File.Exists(path)) + File.Delete(path); + } + catch + { + // Best-effort temp cleanup. + } + } +} + +/// Locates the dotnet host to run the net10 worker. +internal static class DotNetHost +{ + public static string Resolve() + { + // The SDK sets DOTNET_HOST_PATH for in-build processes; prefer it. + var hostPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH"); + if (!string.IsNullOrEmpty(hostPath) && File.Exists(hostPath)) + return hostPath; + + var exe = Environment.OSVersion.Platform == PlatformID.Win32NT ? "dotnet.exe" : "dotnet"; + var path = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; + var fromPath = path.Split(Path.PathSeparator) + .Where(d => !string.IsNullOrWhiteSpace(d)) + .Select(d => Path.Combine(d.Trim(), exe)) + .FirstOrDefault(File.Exists); + + return fromPath ?? "dotnet"; + } +} diff --git a/src/Fallout.MSBuildTasks.Engine/CodeGenerationEngine.cs b/src/Fallout.MSBuildTasks.Engine/CodeGenerationEngine.cs new file mode 100644 index 00000000..2e64f07d --- /dev/null +++ b/src/Fallout.MSBuildTasks.Engine/CodeGenerationEngine.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using Fallout.CodeGeneration; +using Fallout.CodeGeneration.Model; +using Fallout.Common.IO; +using Fallout.Common.Utilities.Collections; + +namespace Fallout.MSBuildTasks.Engine; + +/// +/// MSBuild-free tool-wrapper code generation. Lifted out of CodeGenerationTask so the same +/// logic serves both the in-process net10 task and the out-of-process worker. +/// +public static class CodeGenerationEngine +{ + public static void Generate( + IReadOnlyList specificationFiles, + string baseDirectory, + bool useNestedNamespaces, + string baseNamespace, + bool updateReferences, + Action log) + { + string GetFilePath(Tool tool) + => (AbsolutePath)baseDirectory + / (useNestedNamespaces ? tool.Name : ".") + / tool.DefaultOutputFileName; + + string GetNamespace(Tool tool) + => !useNestedNamespaces + ? baseNamespace + : string.IsNullOrEmpty(baseNamespace) + ? tool.Name + : $"{baseNamespace}.{tool.Name}"; + + specificationFiles + .ForEachLazy(x => log($"Handling {x} ...")) + .ForEach(x => CodeGenerator.GenerateCode(x, GetFilePath, GetNamespace)); + + if (updateReferences) + ReferenceUpdater.UpdateReferences(specificationFiles); + } +} diff --git a/src/Fallout.MSBuildTasks.Engine/Fallout.MSBuildTasks.Engine.csproj b/src/Fallout.MSBuildTasks.Engine/Fallout.MSBuildTasks.Engine.csproj new file mode 100644 index 00000000..483eef45 --- /dev/null +++ b/src/Fallout.MSBuildTasks.Engine/Fallout.MSBuildTasks.Engine.csproj @@ -0,0 +1,19 @@ + + + + net10.0 + false + + + + + + + + + diff --git a/src/Fallout.MSBuildTasks.Engine/PackageToolingEngine.cs b/src/Fallout.MSBuildTasks.Engine/PackageToolingEngine.cs new file mode 100644 index 00000000..1e3495d5 --- /dev/null +++ b/src/Fallout.MSBuildTasks.Engine/PackageToolingEngine.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Fallout.Common.Tooling; +using Fallout.Common.Utilities; +using static Fallout.Common.IO.PathConstruction; + +namespace Fallout.MSBuildTasks.Engine; + +/// +/// MSBuild-free NuGet package discovery for self-contained embedding and tool packing. Lifted out of +/// EmbedPackagesForSelfContainedTask / PackPackageToolsTask. +/// +public static class PackageToolingEngine +{ + /// Package files carrying a tools folder, excluding the runtime pack. + public static IReadOnlyList GetEmbeddablePackageFiles(string projectAssetsFile) + { + return NuGetPackageResolver.GetLocalInstalledPackages(projectAssetsFile) + .Where(x => !x.Id.StartsWithOrdinalIgnoreCase("microsoft.netcore.app.runtime")) + .Where(x => Directory.GetDirectories(x.Directory, "tools").Any()) + .Select(x => (string)x.File) + .ToList(); + } + + /// Every file under each installed package's tools folder, with its pack metadata. + public static IReadOnlyList GetPackageToolFiles( + string projectAssetsFile, + string nuGetPackageRoot, + string targetFramework) + { + return NuGetPackageResolver.GetLocalInstalledPackages(projectAssetsFile) + .SelectMany(x => GetFiles(nuGetPackageRoot, targetFramework, x.Id, x.Version.ToString())) + .ToList(); + } + + private static IEnumerable GetFiles( + string nuGetPackageRoot, + string targetFramework, + string packageId, + string packageVersion) + { + var packageToolsDirectory = Path.Combine( + nuGetPackageRoot, packageId.ToLowerInvariant(), packageVersion.ToLowerInvariant(), "tools"); + if (!Directory.Exists(packageToolsDirectory)) + yield break; + + foreach (var file in Directory.GetFiles(packageToolsDirectory, "*", SearchOption.AllDirectories)) + { + yield return new PackagedToolFile( + File: file, + BuildAction: "None", + PackagePath: Path.Combine( + "tools", targetFramework, "any", packageId, GetRelativePath(packageToolsDirectory, file))); + } + } +} diff --git a/src/Fallout.MSBuildTasks.Engine/PackagedToolFile.cs b/src/Fallout.MSBuildTasks.Engine/PackagedToolFile.cs new file mode 100644 index 00000000..13fb6ce5 --- /dev/null +++ b/src/Fallout.MSBuildTasks.Engine/PackagedToolFile.cs @@ -0,0 +1,4 @@ +namespace Fallout.MSBuildTasks.Engine; + +/// A tool file discovered inside a NuGet package, plus the MSBuild item metadata it needs. +public sealed record PackagedToolFile(string File, string BuildAction, string PackagePath); diff --git a/src/Fallout.MSBuildTasks.Protocol/Fallout.MSBuildTasks.Protocol.csproj b/src/Fallout.MSBuildTasks.Protocol/Fallout.MSBuildTasks.Protocol.csproj new file mode 100644 index 00000000..0b93b71a --- /dev/null +++ b/src/Fallout.MSBuildTasks.Protocol/Fallout.MSBuildTasks.Protocol.csproj @@ -0,0 +1,15 @@ + + + + netstandard2.0;net10.0 + false + + + + + diff --git a/src/Fallout.MSBuildTasks.Protocol/WorkerProtocol.cs b/src/Fallout.MSBuildTasks.Protocol/WorkerProtocol.cs new file mode 100644 index 00000000..5a5284ed --- /dev/null +++ b/src/Fallout.MSBuildTasks.Protocol/WorkerProtocol.cs @@ -0,0 +1,82 @@ +using System.IO; +using System.Runtime.Serialization.Json; +using System.Text; + +namespace Fallout.MSBuildTasks.Protocol; + +/// Worker verbs — the first CLI argument the bridge passes to the net10 worker. +public static class WorkerVerbs +{ + public const string Codegen = "codegen"; + public const string EmbedPackages = "embed-packages"; + public const string PackTools = "pack-tools"; +} + +/// +/// Shared (de)serialization so both ends use identical wire format. Uses the in-box +/// rather than System.Text.Json: the bridge loads this +/// assembly in-process into full-framework MSBuild.exe, where an STJ package reference can't bind +/// without a host binding redirect (the packaged STJ asm version never matches the compile-time +/// reference). DataContractJsonSerializer resolves to the framework's in-box +/// System.Runtime.Serialization on net472 — no NuGet dependency, no redirect. See ADR-0009. +/// +public static class WorkerJson +{ + public static string Serialize(T value) + { + var serializer = new DataContractJsonSerializer(typeof(T)); + using var stream = new MemoryStream(); + serializer.WriteObject(stream, value); + return Encoding.UTF8.GetString(stream.ToArray()); + } + + public static T Deserialize(string json) + { + var serializer = new DataContractJsonSerializer(typeof(T)); + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); + return (T)serializer.ReadObject(stream); + } +} + +public sealed class CodegenRequest +{ + public string[] SpecificationFiles { get; set; } + public string BaseDirectory { get; set; } + public bool UseNestedNamespaces { get; set; } + public string BaseNamespace { get; set; } + public bool UpdateReferences { get; set; } +} + +public sealed class EmbedPackagesRequest +{ + public string ProjectAssetsFile { get; set; } +} + +public sealed class PackToolsRequest +{ + public string ProjectAssetsFile { get; set; } + public string NuGetPackageRoot { get; set; } + public string TargetFramework { get; set; } +} + +/// One discovered tool file plus the MSBuild item metadata the bridge re-attaches. +public sealed class PackagedToolFileDto +{ + public string File { get; set; } + public string BuildAction { get; set; } + public string PackagePath { get; set; } +} + +/// +/// Item outputs the worker writes to its output JSON for the bridge to read back. Diagnostics and +/// success do NOT travel here — messages go to stdout, errors to stderr, and the exit code signals +/// success, so the bridge's ToolTask surfaces them natively. +/// +public sealed class WorkerResponse +{ + /// embed-packages output: package file paths. + public string[] Files { get; set; } = []; + + /// pack-tools output: tool files with pack metadata. + public PackagedToolFileDto[] ToolFiles { get; set; } = []; +} diff --git a/src/Fallout.MSBuildTasks.Worker/Fallout.MSBuildTasks.Worker.csproj b/src/Fallout.MSBuildTasks.Worker/Fallout.MSBuildTasks.Worker.csproj new file mode 100644 index 00000000..aaf33b7f --- /dev/null +++ b/src/Fallout.MSBuildTasks.Worker/Fallout.MSBuildTasks.Worker.csproj @@ -0,0 +1,20 @@ + + + + Exe + net10.0 + false + + + + + + + + + diff --git a/src/Fallout.MSBuildTasks.Worker/Program.cs b/src/Fallout.MSBuildTasks.Worker/Program.cs new file mode 100644 index 00000000..2e1f8dad --- /dev/null +++ b/src/Fallout.MSBuildTasks.Worker/Program.cs @@ -0,0 +1,57 @@ +using System; +using System.IO; +using System.Linq; +using Fallout.MSBuildTasks.Engine; +using Fallout.MSBuildTasks.Protocol; + +// Invoked by the net472 bridge as: +// Diagnostics flow through the streams the bridge's ToolTask reads natively — messages to stdout, +// errors to stderr — and the exit code signals success (0 = ok, 1 = failure, 2 = bad invocation). +// The output JSON carries only item outputs (embed-packages / pack-tools). +if (args.Length != 3) +{ + Console.Error.WriteLine("usage: "); + return 2; +} + +var (verb, inputPath, outputPath) = (args[0], args[1], args[2]); + +try +{ + var inputJson = File.ReadAllText(inputPath); + var response = new WorkerResponse(); + switch (verb) + { + case WorkerVerbs.Codegen: + var cg = WorkerJson.Deserialize(inputJson); + CodeGenerationEngine.Generate( + cg.SpecificationFiles, cg.BaseDirectory, cg.UseNestedNamespaces, + cg.BaseNamespace, cg.UpdateReferences, Console.Out.WriteLine); + break; + + case WorkerVerbs.EmbedPackages: + var ep = WorkerJson.Deserialize(inputJson); + response.Files = PackageToolingEngine.GetEmbeddablePackageFiles(ep.ProjectAssetsFile).ToArray(); + break; + + case WorkerVerbs.PackTools: + var pt = WorkerJson.Deserialize(inputJson); + response.ToolFiles = PackageToolingEngine + .GetPackageToolFiles(pt.ProjectAssetsFile, pt.NuGetPackageRoot, pt.TargetFramework) + .Select(x => new PackagedToolFileDto { File = x.File, BuildAction = x.BuildAction, PackagePath = x.PackagePath }) + .ToArray(); + break; + + default: + Console.Error.WriteLine($"Unknown verb '{verb}'."); + return 2; + } + + File.WriteAllText(outputPath, WorkerJson.Serialize(response)); + return 0; +} +catch (Exception exception) +{ + Console.Error.WriteLine(exception.Message); + return 1; +} diff --git a/src/Fallout.MSBuildTasks/CodeGenerationTask.cs b/src/Fallout.MSBuildTasks/CodeGenerationTask.cs index 042b73cd..b18e46e7 100644 --- a/src/Fallout.MSBuildTasks/CodeGenerationTask.cs +++ b/src/Fallout.MSBuildTasks/CodeGenerationTask.cs @@ -1,10 +1,6 @@ -using System; using System.Linq; using Microsoft.Build.Framework; -using Fallout.CodeGeneration; -using Fallout.CodeGeneration.Model; -using Fallout.Common.IO; -using Fallout.Common.Utilities.Collections; +using Fallout.MSBuildTasks.Engine; namespace Fallout.MSBuildTasks; @@ -24,27 +20,13 @@ public class CodeGenerationTask : ContextAwareTask protected override bool ExecuteInner() { - var specificationFiles = SpecificationFiles.Select(x => x.GetMetadata("Fullpath")).ToList(); - - string GetFilePath(Tool tool) - => (AbsolutePath) BaseDirectory - / (UseNestedNamespaces ? tool.Name : ".") - / tool.DefaultOutputFileName; - - string GetNamespace(Tool tool) - => !UseNestedNamespaces - ? BaseNamespace - : string.IsNullOrEmpty(BaseNamespace) - ? tool.Name - : $"{BaseNamespace}.{tool.Name}"; - - specificationFiles - .ForEachLazy(x => LogMessage(message: $"Handling {x} ...")) - .ForEach(x => CodeGenerator.GenerateCode(x, GetFilePath, GetNamespace)); - - if (UpdateReferences) - ReferenceUpdater.UpdateReferences(specificationFiles); - + CodeGenerationEngine.Generate( + SpecificationFiles.Select(x => x.GetMetadata("FullPath")).ToList(), + BaseDirectory, + UseNestedNamespaces, + BaseNamespace, + UpdateReferences, + message => LogMessage(message: message)); return true; } } diff --git a/src/Fallout.MSBuildTasks/EmbedPackagesForSelfContainedTask.cs b/src/Fallout.MSBuildTasks/EmbedPackagesForSelfContainedTask.cs index 6304e11e..7becf2ed 100644 --- a/src/Fallout.MSBuildTasks/EmbedPackagesForSelfContainedTask.cs +++ b/src/Fallout.MSBuildTasks/EmbedPackagesForSelfContainedTask.cs @@ -1,10 +1,7 @@ -using System; -using System.IO; using System.Linq; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; -using Fallout.Common.Tooling; -using Fallout.Common.Utilities; +using Fallout.MSBuildTasks.Engine; namespace Fallout.MSBuildTasks; @@ -21,11 +18,8 @@ public class EmbedPackagesForSelfContainedTask : ContextAwareTask protected override bool ExecuteInner() { - var packages = NuGetPackageResolver.GetLocalInstalledPackages(ProjectAssetsFile); - TargetOutputs = packages - .Where(x => !x.Id.StartsWithOrdinalIgnoreCase("microsoft.netcore.app.runtime")) - .Where(x => Directory.GetDirectories(x.Directory, "tools").Any()) - .Select(x => new TaskItem(x.File)).ToArray(); + TargetOutputs = PackageToolingEngine.GetEmbeddablePackageFiles(ProjectAssetsFile) + .Select(x => (ITaskItem)new TaskItem(x)).ToArray(); return true; } } diff --git a/src/Fallout.MSBuildTasks/Fallout.MSBuildTasks.csproj b/src/Fallout.MSBuildTasks/Fallout.MSBuildTasks.csproj index 4a9ebce1..1814d9ec 100644 --- a/src/Fallout.MSBuildTasks/Fallout.MSBuildTasks.csproj +++ b/src/Fallout.MSBuildTasks/Fallout.MSBuildTasks.csproj @@ -2,16 +2,16 @@ false - net10.0;net472 + + net10.0 - - - - - - + diff --git a/src/Fallout.MSBuildTasks/Fallout.MSBuildTasks.targets b/src/Fallout.MSBuildTasks/Fallout.MSBuildTasks.targets index dec28b91..16c62d7b 100644 --- a/src/Fallout.MSBuildTasks/Fallout.MSBuildTasks.targets +++ b/src/Fallout.MSBuildTasks/Fallout.MSBuildTasks.targets @@ -12,6 +12,12 @@ --> $(NukeTasksAssembly) + + $(MSBuildThisFileDirectory)\Fallout.MSBuildTasks.Bridge.dll $(MSBuildThisFileDirectory)\$(MSBuildThisFileName).dll $(NukeContinueOnError) diff --git a/src/Fallout.MSBuildTasks/PackPackageToolsTask.cs b/src/Fallout.MSBuildTasks/PackPackageToolsTask.cs index 4de298be..2d4bc20f 100644 --- a/src/Fallout.MSBuildTasks/PackPackageToolsTask.cs +++ b/src/Fallout.MSBuildTasks/PackPackageToolsTask.cs @@ -1,11 +1,7 @@ -using System; -using System.Collections.Generic; -using System.IO; using System.Linq; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; -using Fallout.Common.Tooling; -using static Fallout.Common.IO.PathConstruction; +using Fallout.MSBuildTasks.Engine; namespace Fallout.MSBuildTasks; @@ -25,29 +21,15 @@ public class PackPackageToolsTask : ContextAwareTask protected override bool ExecuteInner() { - var packages = NuGetPackageResolver.GetLocalInstalledPackages(ProjectAssetsFile); - TargetOutputs = packages.SelectMany(x => GetFiles(x.Id, x.Version.ToString())).ToArray(); + TargetOutputs = PackageToolingEngine + .GetPackageToolFiles(ProjectAssetsFile, NuGetPackageRoot, TargetFramework) + .Select(x => + { + var item = new TaskItem(x.File); + item.SetMetadata("BuildAction", x.BuildAction); + item.SetMetadata("PackagePath", x.PackagePath); + return (ITaskItem)item; + }).ToArray(); return true; } - - private IEnumerable GetFiles(string packageId, string packageVersion) - { - var packageToolsDirectory = Path.Combine(NuGetPackageRoot, packageId.ToLowerInvariant(), packageVersion.ToLowerInvariant(), "tools"); - if (!Directory.Exists(packageToolsDirectory)) - yield break; - - foreach (var file in Directory.GetFiles(packageToolsDirectory, "*", SearchOption.AllDirectories)) - { - var taskItem = new TaskItem(file); - taskItem.SetMetadata("BuildAction", "None"); - taskItem.SetMetadata("PackagePath", - Path.Combine( - "tools", - TargetFramework, - "any", - packageId, - GetRelativePath(packageToolsDirectory, file))); - yield return taskItem; - } - } } diff --git a/src/Fallout.Tooling.Generator/Fallout.Tooling.Generator.csproj b/src/Fallout.Tooling.Generator/Fallout.Tooling.Generator.csproj index 8f9534c6..13d5ff46 100644 --- a/src/Fallout.Tooling.Generator/Fallout.Tooling.Generator.csproj +++ b/src/Fallout.Tooling.Generator/Fallout.Tooling.Generator.csproj @@ -1,7 +1,12 @@  - netstandard2.0 + + net10.0 Fallout.CodeGeneration diff --git a/src/Fallout.Tooling/Fallout.Tooling.csproj b/src/Fallout.Tooling/Fallout.Tooling.csproj index 2200aa63..f2ed1d26 100644 --- a/src/Fallout.Tooling/Fallout.Tooling.csproj +++ b/src/Fallout.Tooling/Fallout.Tooling.csproj @@ -1,7 +1,14 @@ - net10.0;netstandard2.0 + + net8.0;net10.0 diff --git a/tests/Fallout.MSBuildTasks.Protocol.Specs/Fallout.MSBuildTasks.Protocol.Specs.csproj b/tests/Fallout.MSBuildTasks.Protocol.Specs/Fallout.MSBuildTasks.Protocol.Specs.csproj new file mode 100644 index 00000000..762eaa0c --- /dev/null +++ b/tests/Fallout.MSBuildTasks.Protocol.Specs/Fallout.MSBuildTasks.Protocol.Specs.csproj @@ -0,0 +1,11 @@ + + + + net10.0 + + + + + + + diff --git a/tests/Fallout.MSBuildTasks.Protocol.Specs/WorkerProtocolSpecs.cs b/tests/Fallout.MSBuildTasks.Protocol.Specs/WorkerProtocolSpecs.cs new file mode 100644 index 00000000..3ecc512e --- /dev/null +++ b/tests/Fallout.MSBuildTasks.Protocol.Specs/WorkerProtocolSpecs.cs @@ -0,0 +1,100 @@ +using System.Linq; +using FluentAssertions; +using Xunit; + +namespace Fallout.MSBuildTasks.Protocol.Specs; + +/// +/// The wire contract between the net472 bridge and the net10 worker. Both ends share +/// , so these guard the two things that can break the bridge: +/// (1) the DTOs must survive a serialize→deserialize round-trip, and (2) the Protocol must carry +/// no System.Text.Json dependency — STJ can't bind when the bridge is loaded in-process into +/// full-framework MSBuild.exe (no host binding redirect). See ADR-0009 and +/// docs/agents/msbuild-bridge-smoke-test.md. +/// +public class WorkerProtocolSpecs +{ + [Fact] + public void CodegenRequest_round_trips() + { + var original = new CodegenRequest + { + SpecificationFiles = ["a/Tool.json", "b/Other.json"], + BaseDirectory = @"C:\src\gen", + UseNestedNamespaces = true, + BaseNamespace = "My.Tools", + UpdateReferences = false, + }; + + var roundTripped = WorkerJson.Deserialize(WorkerJson.Serialize(original)); + + roundTripped.Should().BeEquivalentTo(original); + } + + [Fact] + public void EmbedPackagesRequest_round_trips() + { + var original = new EmbedPackagesRequest { ProjectAssetsFile = @"obj\project.assets.json" }; + + var roundTripped = WorkerJson.Deserialize(WorkerJson.Serialize(original)); + + roundTripped.Should().BeEquivalentTo(original); + } + + [Fact] + public void PackToolsRequest_round_trips() + { + var original = new PackToolsRequest + { + ProjectAssetsFile = @"obj\project.assets.json", + NuGetPackageRoot = @"C:\Users\x\.nuget\packages", + TargetFramework = "net10.0", + }; + + var roundTripped = WorkerJson.Deserialize(WorkerJson.Serialize(original)); + + roundTripped.Should().BeEquivalentTo(original); + } + + [Fact] + public void WorkerResponse_with_data_round_trips() + { + var original = new WorkerResponse + { + Files = ["pkg/one.dll", "pkg/two.dll"], + ToolFiles = + [ + new PackagedToolFileDto + { + File = "tool.dll", + BuildAction = "Content", + PackagePath = "tools/net10.0/any/tool.dll", + }, + ], + }; + + var roundTripped = WorkerJson.Deserialize(WorkerJson.Serialize(original)); + + roundTripped.Should().BeEquivalentTo(original); + } + + [Fact] + public void WorkerResponse_defaults_round_trip_to_empty_collections() + { + var roundTripped = WorkerJson.Deserialize(WorkerJson.Serialize(new WorkerResponse())); + + roundTripped.Files.Should().BeEmpty(); + roundTripped.ToolFiles.Should().BeEmpty(); + } + + [Fact] + public void Protocol_has_no_System_Text_Json_dependency() + { + var referenced = typeof(WorkerJson).Assembly.GetReferencedAssemblies().Select(x => x.Name); + + referenced.Should().NotContain( + "System.Text.Json", + because: "the net472 bridge loads this assembly in-process into full-framework MSBuild.exe, " + + "where STJ can't bind without a host binding redirect (ADR-0009)"); + } +} diff --git a/tests/Fallout.SourceGenerators.Specs/StronglyTypedSolutionGeneratorSpecs.Test#Solution.g.verified.cs b/tests/Fallout.SourceGenerators.Specs/StronglyTypedSolutionGeneratorSpecs.Test#Solution.g.verified.cs index 33249a2c..e74f6daf 100644 --- a/tests/Fallout.SourceGenerators.Specs/StronglyTypedSolutionGeneratorSpecs.Test#Solution.g.verified.cs +++ b/tests/Fallout.SourceGenerators.Specs/StronglyTypedSolutionGeneratorSpecs.Test#Solution.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: Solution.g.cs +//HintName: Solution.g.cs // using Fallout.Persistence.Solution.Model; @@ -27,6 +27,11 @@ internal class Solution(SolutionModel model, AbsolutePath path) : Fallout.Soluti public Fallout.Solutions.Project Fallout_Migrate_Analyzers_Specs => this.GetProject("Fallout.Migrate.Analyzers.Specs"); public Fallout.Solutions.Project Fallout_Migrate_Specs => this.GetProject("Fallout.Migrate.Specs"); public Fallout.Solutions.Project Fallout_MSBuildTasks => this.GetProject("Fallout.MSBuildTasks"); + public Fallout.Solutions.Project Fallout_MSBuildTasks_Bridge => this.GetProject("Fallout.MSBuildTasks.Bridge"); + public Fallout.Solutions.Project Fallout_MSBuildTasks_Engine => this.GetProject("Fallout.MSBuildTasks.Engine"); + public Fallout.Solutions.Project Fallout_MSBuildTasks_Protocol => this.GetProject("Fallout.MSBuildTasks.Protocol"); + public Fallout.Solutions.Project Fallout_MSBuildTasks_Protocol_Specs => this.GetProject("Fallout.MSBuildTasks.Protocol.Specs"); + public Fallout.Solutions.Project Fallout_MSBuildTasks_Worker => this.GetProject("Fallout.MSBuildTasks.Worker"); public Fallout.Solutions.Project Fallout_Persistence_Solution => this.GetProject("Fallout.Persistence.Solution"); public Fallout.Solutions.Project Fallout_Persistence_Solution_Benchmarks => this.GetProject("Fallout.Persistence.Solution.Benchmarks"); public Fallout.Solutions.Project Fallout_ProjectModel => this.GetProject("Fallout.ProjectModel");