diff --git a/.github/workflows/main-cd.yml b/.github/workflows/main-cd.yml index f3c1c05933..6b27b1fc8f 100644 --- a/.github/workflows/main-cd.yml +++ b/.github/workflows/main-cd.yml @@ -1055,12 +1055,55 @@ jobs: # csproj (project-local — a global -p:RuntimeIdentifiers breaks referenced libs, see above). # `latest` (the tag the plugins repo pins as MW_TEST_IMAGE) and the GHCR mirror both moved # to `promote`: they are consumer-visible, so they must not appear until every leg is green. + # 🚨 THE RAZOR COMPILER, STAGED FOR BOTH IMAGE ARCHITECTURES. + # `mw-plugin-test build-project` compiles .razor/.cshtml, and Razor compilation in the .NET + # SDK is a Roslyn SOURCE GENERATOR. This image ships the runtime and no SDK, so the generator + # has to travel with the builder — and it CANNOT be copied once: the SDK crossgens it for the + # SDK's own RID, so the same 10.0.400 file carries PE machine 0xFD1D on linux-x64 and 0xD11D + # on linux-arm64, and the wrong one throws BadImageFormatException at run time and nowhere + # else. This image publishes BOTH architectures from ONE x64 host, so both copies are staged + # and the builder picks the one for the RID it is running on. + # + # The arm64 copy comes from the dotnet/sdk image of the EXACT SAME SDK VERSION the runner is + # using — one source of truth, no second pin to drift — and `docker create` + `docker cp` + # reads it out without ever executing an arm64 binary, so no qemu is needed. + - name: Stage the Razor source generator for linux-x64 and linux-arm64 + id: razor + run: | + set -euo pipefail + test "$(uname -m)" = "x86_64" \ + || { echo "::error::this step assumes an x64 runner (it takes the host SDK's copy as the linux-x64 one)"; exit 1; } + SDK_VERSION="$(dotnet --version)" + SDK_ROOT="$(dotnet --list-sdks | sed -n "s|^${SDK_VERSION} \[\(.*\)\]$|\1|p" | head -1)" + HOST_SOURCE="$SDK_ROOT/$SDK_VERSION/Sdks/Microsoft.NET.Sdk.Razor/source-generators" + ROOT="$RUNNER_TEMP/razor-generators" + echo "SDK $SDK_VERSION; host source-generators: $HOST_SOURCE" + + mkdir -p "$ROOT/linux-x64" "$ROOT/linux-arm64" + cp "$HOST_SOURCE"/*.dll "$ROOT/linux-x64/" + + IMAGE="mcr.microsoft.com/dotnet/sdk:$SDK_VERSION" + docker pull --platform linux/arm64 "$IMAGE" + CID="$(docker create --platform linux/arm64 "$IMAGE")" + trap 'docker rm -f "$CID" >/dev/null 2>&1 || true' EXIT + docker cp "$CID:/usr/share/dotnet/sdk/$SDK_VERSION/Sdks/Microsoft.NET.Sdk.Razor/source-generators/." \ + "$ROOT/linux-arm64/" + + # 🚨 Fail closed. An empty or half-staged RID ships an image whose Razor builds fail on + # exactly one architecture — which nothing in this pipeline would notice. + for rid in linux-x64 linux-arm64; do + test -f "$ROOT/$rid/Microsoft.CodeAnalysis.Razor.Compiler.dll" \ + || { echo "::error::no Razor compiler staged for $rid"; exit 1; } + echo "$rid: $(ls "$ROOT/$rid" | tr '\n' ' ')" + done + echo "root=$ROOT" >> "$GITHUB_OUTPUT" - name: Build + push mw-plugin-test (staging tag, ACR) run: > dotnet publish tools/MeshWeaver.PluginTester/MeshWeaver.PluginTester.csproj -c Release --no-self-contained -t:PublishContainer -p:PublishProfile= -p:ContainerRuntimeIdentifiers='"linux-x64;linux-arm64"' -p:CIRun=true + -p:MeshWeaverRazorGeneratorRoot=${{ steps.razor.outputs.root }} -p:ContainerRegistry=${{ env.ACR }} -p:ContainerRepository=mw-plugin-test -p:ContainerImageTags=${{ needs.gate.outputs.staging_tag }} diff --git a/src/MeshWeaver.Cli/README.md b/src/MeshWeaver.Cli/README.md index a24527af44..ed91d47345 100644 --- a/src/MeshWeaver.Cli/README.md +++ b/src/MeshWeaver.Cli/README.md @@ -75,10 +75,17 @@ assembly the image already carries. See | `--output ` | where the emitted assemblies land (default: a temp dir) | | `--root ` | directory mounted as `/repo` (default: the nearest `Directory.Build.props` ancestor) | | `--extra-refs ` | libraries ADDITIONAL to the platform — the only way to satisfy a `PackageReference` the image does not supply. Repeatable | -| `--accept ` | acknowledge one construct the evaluator cannot reproduce (`target:`, `embedded-resource`, `conditions`). Repeatable | +| `--accept ` | acknowledge one construct the evaluator cannot reproduce (`target:`, `embedded-resource`, `conditions`, `razor-css-scope`, `razor-not-compiled`). Repeatable | | `--no-warn` / `--allow-warnings` | warnings fail the build (default); `--no-warn=false` or `--allow-warnings` opts out | | `--no-pull` | use the image the docker daemon already has, for one built locally | +**Razor/Blazor compiles** (2026-08-31): the image ships the SDK's Razor source generator beside the +builder, per RID, and `build-project` runs it for any project whose `Sdk` processes Razor items — +`MeshWeaver.Blazor` (31 `.cs` + 42 `.razor`) builds green against `memex-portal-ai`. What it will +not do quietly is skip a `.razor` file: a project with Razor input and no generator fails by name, +and CSS isolation (`*.razor.css`) needs `--accept razor-css-scope` because the `b-…` scope comes +from an MSBuild task this builder does not run. + 🚨 **Nothing is dropped in silence.** A project construct this builder cannot reproduce — an unknown element, an unevaluatable `Condition`, a ``, an `` — FAILS the run naming the construct and the file, and a `PackageReference` the container does not supply is reported by diff --git a/src/MeshWeaver.Documentation/Data/Architecture/InMeshBuildAndTest.md b/src/MeshWeaver.Documentation/Data/Architecture/InMeshBuildAndTest.md index 97798c037b..fed8e43937 100644 --- a/src/MeshWeaver.Documentation/Data/Architecture/InMeshBuildAndTest.md +++ b/src/MeshWeaver.Documentation/Data/Architecture/InMeshBuildAndTest.md @@ -392,17 +392,92 @@ with `--accept targets --accept embedded-resource` and ClosedXML + CsvHelper as | | count | why | |---|---|---| | **green** | **12** | incl. `MeshWeaver.Import` — 90 source files, 0 warnings under warnings-as-errors | -| Razor/Blazor (CS0115) | 15 | `.razor` components are not compiled — see below | +| Razor/Blazor (CS0115) | 15 | *fixed 2026-08-31 — see below* | | source generators (CS8795) | 15 | `[GeneratedRegex]` / `[LoggerMessage]` / `[JsonSerializable]` | | gRPC `` | 3 | protoc codegen is a build task, not a compile | | additional libraries | 5 | Snowflake.Data, Microsoft.Data.Sqlite, Azure.Cosmos, … — supply with `--extra-refs` | | portal hosts | 3 | an `` above the mount; Aspire's `` ELEMENT | +## The container compiles Razor (2026-08-31) + +Razor was the biggest single category above, and it was one missing file rather than a missing +feature: **Razor compilation in the .NET SDK is a Roslyn source generator** +(`Microsoft.CodeAnalysis.Razor.Compiler`) that turns each `.razor` into the partial class carrying +its generated `BuildRenderTree` override. A runtime image ships no SDK, so without the generator +every component compiled to a class with nothing to override — a wall of CS0115 that reads exactly +like broken source. + +The image now carries it, in `razor-generators/` beside the builder, and `build-project` finds and +runs it automatically for any project whose `Sdk` processes Razor items. + +### The dependency closure, measured + +Exactly **two** assemblies, established by reading the compiler's own assembly references and then +proving it by deleting one: + +| assembly | why | +|---|---| +| `Microsoft.CodeAnalysis.Razor.Compiler` | carries `RazorSourceGenerator`, the `[Generator]` type | +| `Microsoft.AspNetCore.Razor.Utilities.Shared` | its one private dependency — with it absent the compiler loads, its types enumerate, and every call into it throws | + +Everything else it references — `netstandard`, `Microsoft.CodeAnalysis(.CSharp)`, +`System.Collections.Immutable`, `System.Memory`, `System.Buffers` — binds to the assemblies the +image already has. 🚨 **`Microsoft.Extensions.ObjectPool` is NOT needed**, though an older Razor +compiler build referenced it; the belief that it was is exactly why the closure was measured rather +than assumed. + +### Two traps, both of which fail at run time and nowhere else + +🚨 **The generator is built against the SDK's Roslyn, not the image's.** SDK 10.0.400's copy +references `Microsoft.CodeAnalysis` **5.9.0.0** while the image carries this repo's pin, **5.6.0**. +The default load context binds by name *and refuses a lower version*, so a plain `Assembly.LoadFrom` +fails — and a generator loader that treats "cannot load" as "not a generator" then produces a build +indistinguishable from one where nobody asked for Razor. The generator is therefore loaded into a +context that binds every assembly the HOST already has to the host's copy, version ignored. That is +also what keeps `ISourceGenerator` ONE type: a second Roslyn in that context would give the +generator a different interface than the driver expects. + +🚨 **The generator is ReadyToRun-compiled for the SDK's own RID.** The same SDK 10.0.400 file carries +PE machine `0xFD1D` on linux-x64, `0xD11D` on linux-arm64 and `0xEC20` on osx-arm64 (the target +machine XOR'd with the operating system's R2R marker), and the wrong one throws +`BadImageFormatException`. `mw-plugin-test` publishes BOTH architectures from ONE x64 build host, so +copying the build machine's SDK once would have shipped an arm64 image that cannot compile a single +Blazor project. CD stages one directory per RID (`razor-generators//`, the arm64 copy read out +of the dotnet/sdk image of the same SDK version with `docker create` + `docker cp`, so no emulation +is involved), and the builder picks the directory for the RID it is running on. + +### What it refuses, and why + +- **A project with Razor files and no generator.** One named failure that says what the CS0115 wall + would have been, not the wall. +- **A generator that ran and emitted nothing.** Not a compile error — a generator that did not + recognise its input, which otherwise fails four steps later as CS0115. +- **CSS isolation (`*.razor.css`).** The `b-…` scope identifier comes from the SDK's + `ComputeCssScope`/`ApplyCssScopes` tasks, and this builder runs no MSBuild task, so the components + would compile without their scope attributes. `--accept razor-css-scope` builds them anyway (the + assembly is valid; only the isolated stylesheet stops applying). Reproducing the scope hash from + memory is the guess this evaluator exists to avoid. +- **`.razor` under a project whose `Sdk` does not process Razor.** The SDK's build ignores them too, + so the outcome matches — but it is stated rather than skipped. `--accept razor-not-compiled`. + +### Measured, 2026-08-31 — 10 of the 11 Razor projects + +Every `Microsoft.NET.Sdk.Razor` project in `MeshWeaver.Plugins/src`, built against +`memex-portal-ai@sha256:6f38db08…` with the builder mounted, `--accept targets --accept +razor-css-scope --accept embedded-resource`: + +| | count | | +|---|---|---| +| **green, no extra help** | **7** | Blazor (31 `.cs` + **42 `.razor`**), Blazor.EntityViews, Blazor.Graph, Blazor.Analysis, Blazor.OpenStreetMap, Blazor.GoogleMaps, Blazor.AppleMaps | +| green with `--generators` (the SDK's regex generator) | +2 | Blazor.Views, Blazor.Portal — they were blocked by `[GeneratedRegex]` in `MeshWeaver.Markdown.Collaboration`, not by Razor | +| green with `--extra-refs` | +1 | Blazor.Radzen — `Radzen.Blazor` is an additional library | +| still red | 1 | `Memex.Portal.Gui` — a transitive `MeshWeaver.Hosting.Grpc` carries ``; protoc is a build task, not a compile | + +**Razor is no longer what blocks any of them.** The two remaining categories are the pre-existing +ones — SDK source generators and ``. + ### What it does NOT do, and what each would take -- **Razor/Blazor.** `.razor` and `.cshtml` are not compiled at all. Doing so needs - `Microsoft.CodeAnalysis.Razor.Compiler`, `Microsoft.AspNetCore.Razor.Utilities.Shared` and - `Microsoft.Extensions.ObjectPool` present in the image — today it ships none of them. - **Source generators.** They live in the .NET **SDK**, and a MeshWeaver image ships the **runtime**. `--generators ` loads them when supplied; with none, the builder names the generator that did not run rather than leaving a wall of CS8795 that reads like broken source. diff --git a/test/MeshWeaver.PluginTester.Test/MeshWeaver.PluginTester.Test.csproj b/test/MeshWeaver.PluginTester.Test/MeshWeaver.PluginTester.Test.csproj index 69cd9ed65d..e86e553ea5 100644 --- a/test/MeshWeaver.PluginTester.Test/MeshWeaver.PluginTester.Test.csproj +++ b/test/MeshWeaver.PluginTester.Test/MeshWeaver.PluginTester.Test.csproj @@ -11,6 +11,15 @@ + + + + + + + $(NetCoreRoot)sdk/$(NETCoreSdkVersion)/Sdks/Microsoft.NET.Sdk.Razor/source-generators + $(NETCoreSdkRuntimeIdentifier) + + + + + + <_MeshWeaverRazorGenerator Include="$(MeshWeaverRazorGeneratorRoot)/*/*.dll" /> + <_MeshWeaverRazorGenerator> + $([System.IO.Path]::GetFileName($([System.IO.Path]::GetDirectoryName('%(FullPath)')))) + + + + <_MeshWeaverRazorGenerator Include="$(MeshWeaverRazorGeneratorSource)/*.dll" RidFolder="$(MeshWeaverRazorGeneratorRid)" /> + + + + + + + + <_MeshWeaverRazorGeneratorNames>@(_MeshWeaverRazorGenerator->'%(RidFolder)/%(Filename)%(Extension)', '", "') + <_MeshWeaverRazorGeneratorOrigin Condition="'$(MeshWeaverRazorGeneratorRoot)' != ''">$(MeshWeaverRazorGeneratorRoot) + <_MeshWeaverRazorGeneratorOrigin Condition="'$(MeshWeaverRazorGeneratorRoot)' == ''">$(MeshWeaverRazorGeneratorSource) + + + + + + + +