Repository navigation
feat(image): the container compiles Razor — the SDK-free build covers Blazor - #2856
Conversation
… Blazor Razor was the biggest single gap in `memex build project` (#2850): 15 of the 42 failures in its first no-SDK sweep of MeshWeaver.Plugins/src were CS0115, more than any other category. The cause was one missing file, not a missing feature — Razor compilation in the .NET SDK is a Roslyn SOURCE GENERATOR, and a runtime image ships no SDK, so every component compiled to a class with nothing to override. The image now carries the generator in razor-generators/<rid>/ beside the builder, and build-project finds and runs it for any project whose Sdk processes Razor items. The closure was MEASURED, not assumed: exactly Microsoft.CodeAnalysis.Razor.Compiler plus Microsoft.AspNetCore.Razor.Utilities.Shared. Microsoft.Extensions.ObjectPool, believed to be needed, is not referenced by this compiler build at all. Two traps that fail at run time and nowhere else, both handled and both tested: * The generator is built against the SDK's Roslyn (10.0.400 wants Microsoft.CodeAnalysis 5.9.0.0) and the image carries this repo's pin (5.6.0). The default load context refuses the lower version, and a loader that reads "cannot load" as "not a generator" produces a build indis- tinguishable from one nobody asked Razor for. Generators therefore load into a context that binds every assembly the host already has to the host's copy, version ignored — which is also what keeps ISourceGenerator ONE type. * The generator is ReadyToRun-compiled for the SDK's own RID: the same 10.0.400 file carries PE machine 0xFD1D on linux-x64, 0xD11D on linux-arm64 and 0xEC20 on osx-arm64. mw-plugin-test publishes both architectures from one x64 host, so a single copy would have shipped an arm64 image that cannot compile a Blazor project. CD stages one directory per RID, reading the arm64 copy out of the dotnet/sdk image of the SAME SDK version with docker create + docker cp — no emulation, no second pin. Nothing is skipped in silence: a project with Razor files and no generator fails by name (saying what the CS0115 wall would have been), a generator that runs and emits nothing fails by name, CSS isolation is refused with --accept razor-css-scope because the b-… scope comes from an MSBuild task this builder does not run, and .razor under a non-Razor Sdk is refused with --accept razor-not-compiled. No ProjectReference was added to tools/MeshWeaver.PluginTester — the assemblies ride along as Content and are loaded at run time, so the framework build identity is untouched (FrameworkBuildIdentityTest, 23 cases, still green). Measured against memex-portal-ai@sha256:6f38db08…, all 11 Microsoft.NET.Sdk.Razor projects in MeshWeaver.Plugins/src: 7 green with no extra help (incl. MeshWeaver.Blazor — 31 .cs + 42 .razor, zero warnings under warnings-as-errors), +2 with --generators for a [GeneratedRegex] dependency, +1 with --extra-refs for Radzen.Blazor. The one still red is Memex.Portal.Gui, blocked by <Protobuf> in a transitive ProjectReference. Razor blocks none of them. Tests: 13 new in test/MeshWeaver.PluginTester.Test/RazorBuildTest.cs, executed — 188 passing, up from 175, no regressions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…or parameters check-record-signatures.py is right and the fix is not an allow entry: adding a parameter to a record's primary constructor — even with a default — REPLACES the signature, so every assembly compiled against the old arity calls a constructor that no longer exists. ProjectFile.Model's four Razor members and ProjectResult.RazorCount become init properties instead, which is additive. RazorItems defaults to [] rather than default(ImmutableArray<T>), which throws on enumeration. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
I merged this, and I should say plainly that I did not check its base firstMerged at 04:51:25Z by an automated drain of the green PR backlog. This PR's base is The check I ran before merging was ConsequenceSmall, and not a loss: this is a deliberately stacked PR, so its content was always going to reach What it is not is progress toward main — If this was not what you wantedSay so and I will revert the merge on Every other PR I queued in the same batch (#2848, #2849, #2852, and my own #2867/#2868/#2872/#2873) has |
Razor was the biggest single gap in #2850's first no-SDK sweep of
MeshWeaver.Plugins/src:15 of 42 failures were CS0115, more than any other category. The cause was one missing file, not
a missing feature — Razor compilation in the .NET SDK is a Roslyn source generator
(
Microsoft.CodeAnalysis.Razor.Compiler) that turns each.razorinto the partial class carryingits
BuildRenderTreeoverride. A runtime image ships no SDK, so every component compiled to a classwith nothing to override.
The image now carries the generator in
razor-generators/<rid>/beside the builder, andbuild-projectfinds and runs it automatically for any project whoseSdkprocesses Razor items.The dependency closure, established rather than assumed
The brief named three assemblies. Two are needed; the third is not.
Microsoft.CodeAnalysis.Razor.CompilerRazorSourceGeneratorMicrosoft.AspNetCore.Razor.Utilities.SharedMicrosoft.Extensions.ObjectPoolHow it was established. Read the compiler's own
AssemblyReftable (netstandard,Microsoft.CodeAnalysis5.9.0.0,Microsoft.CodeAnalysis.CSharp5.9.0.0,System.Collections.Immutable,System.Memory,System.Buffers,Microsoft.AspNetCore.Razor.Utilities.Shared— no ObjectPool), then proved it by deleting:with
Utilities.Sharedremoved the compiler assembly still loads and its types still enumerate, andevery call into it throws — the exact "generator silently produces nothing" shape this design exists
to prevent.
Microsoft.Extensions.ObjectPoolis referenced by the older NuGet build of the Razorcompiler (
10.0.0-preview.25277.114), which is presumably where the belief came from.Two traps that 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 wants
Microsoft.CodeAnalysis5.9.0.0; the image carries this repo's pin, 5.6.0. The default loadcontext binds by name and refuses a lower version, so a plain
Assembly.LoadFromfails withCould not load … Version=5.9.0.0— andSourceGeneratorLoadercorrectly reads that as "not ausable generator" and moves on, which is how a missing Razor compiler ends up indistinguishable from
a
.razorfile nobody asked to compile. Generators therefore load into a context that binds everyassembly the host already has to the host's copy, version ignored (the same thing Roslyn's own
analyzer loader does). That is also what keeps
ISourceGeneratorone type: a second Roslyn inthat context would hand the generator a different interface than the driver expects.
🚨 The generator is ReadyToRun-compiled for the SDK's own RID. Measured on SDK 10.0.400, the same
Microsoft.CodeAnalysis.Razor.Compiler.dllcarries PE machine0xFD1Don linux-x64,0xD11Don linux-arm64 and
0xEC20on osx-arm64 (the target machine XOR'd with the OS's R2R marker),and the wrong one throws
BadImageFormatException.mw-plugin-testpublishes both architecturesfrom one x64 host, so "copy the build machine's SDK" would have shipped an arm64 image that
cannot compile a single Blazor project. CD now stages one directory per RID — the arm64 copy read out
of
mcr.microsoft.com/dotnet/sdk:<the runner's own SDK version>withdocker create+docker cp,so no emulation runs and there is no second pin to drift.
The
.razoritem glob, and what is refusedProjectFilenow evaluates the Razor SDK's default items (Content Include="**\*.razor"/"**\*.cshtml", promoted toRazorComponent/RazorGenerate) plus the project's ownContent/RazorComponent/RazorGenerateInclude/Remove, and assigns each theTargetPathAssignTargetPathwould have (relative to the project directory — that is what decides the generatedtype's namespace, and getting it wrong compiles into a namespace nothing references).
Nothing is skipped in silence. Four named failures, all tested:
*.razor.cssCSS isolationb-…scope comes from the SDK'sComputeCssScope/ApplyCssScopestasks; this builder runs no MSBuild task, so the components would compile without their scope attributes.--accept razor-css-scopebuilds them anyway. Reproducing the scope hash from memory is the guess this evaluator exists to avoid.razorunder a non-RazorSdk--accept razor-not-compiledMeasured, 2026-08-31 — 10 of the 11 Razor projects
Every
Microsoft.NET.Sdk.Razorproject inMeshWeaver.Plugins/src, built insidememex-portal-ai@sha256:6f38db08…with this branch's builder mounted:MeshWeaver.Blazor(31.cs+ 42.razor, 0 warnings under warnings-as-errors),.EntityViews,.Graph,.Analysis,.OpenStreetMap,.GoogleMaps,.AppleMaps--generators.Views,.Portal— blocked by[GeneratedRegex]inMeshWeaver.Markdown.Collaboration, not by Razor--extra-refs.Radzen—Radzen.Blazoris an additional libraryMemex.Portal.Gui— a transitiveMeshWeaver.Hosting.Grpccarries<Protobuf>; protoc is a build task, not a compileRazor blocks none of them. The two remaining categories are the pre-existing ones.
linux/arm64, natively on an Apple Silicon host. Thelinux/amd64runs onthat host crash under Rosetta — and the control proves it is the emulator, not this change: the
same emulated container dies with
AccessViolationExceptionbuildingMeshWeaver.Speech, a projectwith no Razor in it that #2850 verified green. CI runs on real x64 and is the x64 evidence.
The framework build identity is untouched
No
ProjectReferencewas added totools/MeshWeaver.PluginTester— whose closure is theframework identity. The assemblies ride along as
ContentwithCopyToPublishDirectoryand areloaded at run time.
FrameworkBuildIdentityTest(23 cases) still passes.OptionsFingerprintandEmitPipeline.CreateCompilationOptionsare not touched either;MeshWeaver.Compileris notmodified at all by this PR.
Tests — 13 new, EXECUTED
dotnet test test/MeshWeaver.PluginTester.Test -c Release→ Passed! Failed: 0, Passed: 188(175 before, no regressions).
loaded generator is
RazorSourceGenerator;provenance manifest;
.razor(with@inherits, markup and@code) compiles and the emitted assembly'scomponent type is loaded and its generated
BuildRenderTreeoverride found on it;TargetPathdictates;_Imports.razorcontributes its usings to a sibling (it would not compile otherwise);.razorunder a plainSdk, are each refused by name and build under their--accepttoken;Content Removetakes a component out of the build;--razor-generatorsreplaces the searchpath rather than heading it (no silent fallback to the image's own copy);
Docs
Doc/Architecture/InMeshBuildAndTest.mdgains a "The container compiles Razor" section — themeasured closure, both traps, the refusals and the sweep table — and the two READMEs
(
MeshWeaver.Cli,MeshWeaver.PluginTester) are updated.🤖 Generated with Claude Code