diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index cafa6cb..f043030 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "csharpier": { - "version": "1.3.0", + "version": "1.4.0", "commands": [ "csharpier" ], @@ -17,7 +17,7 @@ "rollForward": false }, "purview.build": { - "version": "0.8.2", + "version": "0.8.1", "commands": [ "purview-build" ], diff --git a/.config/purview-build.json b/.config/purview-build.json index 732f2bd..97b2f99 100644 --- a/.config/purview-build.json +++ b/.config/purview-build.json @@ -23,6 +23,8 @@ "Sdk/Sdk.props", "Sdk/Sdk.targets", "Sdk/Targets/Blazor.targets", + "Sdk/Targets/Modern.targets", + "Sdk/Targets/ServerShared.targets", "Sdk/Targets/VersionDetection.targets" ], "purview.skills.testing": [ diff --git a/.editorconfig b/.editorconfig index 138c5a4..a9b39cb 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,4 +1,4 @@ -# Purview.BuildSdk 1.3.2 - managed file, do not edit +# Purview.BuildSdk 2.0.0 - managed file, do not edit # # This file is written by Purview.BuildSdk and is replaced whenever the SDK version changes. The version # on the first line is how the SDK recognises a file it owns: a .editorconfig without that marker is diff --git a/.gitignore b/.gitignore index 23793e5..ac539eb 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,7 @@ project.lock.json project.fragment.lock.json artifacts/ +*.rsp *_i.c *_p.c *_i.h @@ -60,6 +61,9 @@ artifacts/ *.pgc *.pgd *.rsp +# The SDK bootstraps a managed response file at the repository root; it is tracked so the first build +# in a fresh clone already runs MSBuild's multithreaded mode. +!/Directory.Build.rsp *.sbr *.tlb *.tli diff --git a/CHANGELOG.md b/CHANGELOG.md index 323f4c3..8a49fbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,35 @@ release summarise what the commits recorded rather than what was written at the ## [Unreleased] +### Added + +- **`ServerShared` projects.** A `ServerShared*` project is referenced automatically by every + non-client-side project, and never by a WebAssembly client, `PurviewBrowserSafe` library, Razor + class library or `*.Client`/`*.Browser`/`*.Wasm`/`*.WebAssembly`/`*.Components` project. + `IsClientSideProject` names the classification and can be set explicitly; `PRSHR0001` (client-side + reference) and `PRSHR0002` (Shared reference) catch a reference that would still reach a browser. + Opt the automatic reference out with `PurviewAutoServerSharedReference=false`. +- **.NET 11 compiler opt-ins.** A `net11.0`+ target compiles with Runtime Async + (`runtime-async=on`) and the C# 15 updated memory-safety rules + (`MemorySafetyRules=2`), gated per-TFM so the `net10.0` leg is unchanged. `AllowUnsafeBlocks` is + left at the compiler default (`false`). Opt out with `PurviewRuntimeAsync=false`, + `UseRuntimeAsync=false`, or `MemorySafetyRules=0`; an unrecognised tier fails with `PRCFG0001`. +- **Multithreaded builds.** A managed `Directory.Build.rsp` at the repository root enables MSBuild's + `-mt` on .NET 11, bootstrapped like `.editorconfig` and `global.json` and refreshed only when its + marker line is present. `PurviewMultiThreadedBuild=false` rewrites it to `-mt:false`. + +### Changed + +- **The default target frameworks are now `net10.0;net11.0`.** `Current` — the set that applies when a + project selects none — resolves to the current LTS plus the latest release, so a project that declares + neither `TargetFramework` nor `TargetFrameworks` now multi-targets. This is a breaking change: pin + `PurviewTargetFrameworksCurrent`, select another set, or declare the TFM explicitly to keep the previous + single-target output. `Supported` still names the same pair so a repository can pin the meaning rather + than depend on where `Current` moves next. +- **`LangVersion` defaults to `latest` (C# 15)** instead of `preview`. C# 15 is the latest shipped + language version and includes union declarations; `preview` remains available per project when a + repository deliberately wants unshipped features. Roslyn components keep `LangVersion=latest`. + ## [1.3.2] - 2026-11-09 ### Changed diff --git a/Directory.Build.rsp b/Directory.Build.rsp new file mode 100644 index 0000000..f7123f5 --- /dev/null +++ b/Directory.Build.rsp @@ -0,0 +1,4 @@ +# Purview.BuildSdk - managed file +# Managed by Purview.BuildSdk. Delete the marker line to take ownership of this file. +-mt +-nr:false diff --git a/Directory.Packages.props b/Directory.Packages.props index 7a2f40c..5259269 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,6 +1,6 @@ - 1.76.0 + 1.83.0 5.9.0 diff --git a/README.md b/README.md index ff272a6..3adc17d 100644 --- a/README.md +++ b/README.md @@ -12,18 +12,20 @@ A reusable MSBuild SDK NuGet package that delivers standardised .NET project def | Feature | Detail | | -- | -- | -| **Project type detection** | `IsCSharpProject`, `IsTestProject`, `IsSharedTestingProject`, `IsContainerProject`, `IsWebSdkProject`, `IsAspireHostProject`, `IsBlazorWebAssemblyProject`, `IsBffProject`, … | +| **Project type detection** | `IsCSharpProject`, `IsTestProject`, `IsSharedTestingProject`, `IsServerSharedProject`, `IsClientSideProject`, `IsContainerProject`, `IsWebSdkProject`, `IsAspireHostProject`, `IsBlazorWebAssemblyProject`, `IsBffProject`, … | +| **.NET 11 opt-ins** | A `net11.0` target compiles with Runtime Async (`runtime-async=on`) and the C# 15 updated memory-safety rules (`MemorySafetyRules=2`), while `AllowUnsafeBlocks` stays at its compiler default (`false`); builds use MSBuild's multithreaded mode through a bootstrapped `Directory.Build.rsp` (see [.NET 11 features](#net-11-features)) | +| **ServerShared** | A `ServerShared*` project is referenced automatically by every non-client-side project, and never by a WebAssembly client, browser-safe library, Razor class library or `*.Client`-style project; `PRSHR0001`/`PRSHR0002` report a reference that would still put it in a browser download (see [ServerShared](#servershared)) | | **Blazor and BFF** | Blazor WebAssembly clients, Blazor Web App hosts and BFF hosts are detected; `PurviewBrowserSafe` makes a library the client downloads fail the build on trim, AOT and browser-unsupported APIs, and the client checks every reference sets it (see [Blazor WebAssembly and BFF](#blazor-webassembly-and-bff)) | -| **C# defaults** | `net10.0` TFM (overridable), `LangVersion=preview`, `Nullable=enable`, `ImplicitUsings=enable`, deterministic builds | +| **C# defaults** | `net10.0;net11.0` TFMs (overridable), `LangVersion=latest` (C# 15), `Nullable=enable`, `ImplicitUsings=enable`, deterministic builds | | **Code style** | `.editorconfig` baked into the package, applied via `EditorConfigFilePath`, and auto-bootstrapped to repo root if missing; `EnforceCodeStyleInBuild=true`, `EnableNETAnalyzers=true`, `AnalysisLevel=latest`, `AnalysisMode=All`. Style policy: `dotnet_style_require_accessibility_modifiers=omit_if_default`, private instance fields named `_camelCase`, and `IDE0040`/`CA1515`/`CA1852`/`CA1034` at warning or above, enforced by `ValidatePurviewStylePolicy` (opt out with `DisablePurviewStylePolicyValidation=true`) | | **NuGet packaging** | `AssemblyName`/`PackageId` default to the fully evaluated `RootNamespace`; packable projects get `GenerateDocumentationFile=true`, `PublishRepositoryUrl=true`, `IncludeSymbols=true`, `SymbolPackageFormat=snupkg`, `EmbedUntrackedSources=true`, and portable PDBs delivered via `.snupkg` (not the `.nupkg`) | -| **Repo bootstrap** | Missing repo-root `.editorconfig` and `global.json` are auto-copied/created by default (disable via `DisableAutoCopySdkFiles=true`) | +| **Repo bootstrap** | Missing repo-root `.editorconfig`, `global.json` and `Directory.Build.rsp` are auto-created by default (disable via `DisableAutoCopySdkFiles=true`) | | **CI detection** | `ContinuousIntegrationBuild` set automatically when `CI`, `GITHUB_ACTIONS`, or `TF_BUILD` env vars are present | | **SourceLink** | `Microsoft.SourceLink.GitHub` added to all packable projects (configurable via `SourceLinkPackageName`) | | **Purview Telemetry** | `Purview.Telemetry.SourceGenerator` + `Microsoft.Extensions.Telemetry.Abstractions` added by default (opt-out) | | **Assembly info** | Auto-generated `static partial class AssemblyInfo` with `RootNamespace`, `Version`, `Company`, etc., plus an embedded `Microsoft.CodeAnalysis.EmbeddedAttribute` (can be excluded via `PURVIEW_SDK_EXCLUDE_EMBEDDED`). | | **InternalsVisibleTo** | Generated for all `TestType` variants and shared testing projects, using the resolved `$(AssemblyName)` so explicit, generated, and default naming are all handled | -| **Namespace management** | `NamespacePrefix.ProjectName` pattern with suffix stripping (`.Core`, `.Shared`, `.EF`, …) | +| **Namespace management** | `NamespacePrefix.ProjectName` pattern with suffix stripping (`.Core`, `.Shared`, `.ServerShared`, `.EF`, …) | | **Testing framework** | `TestingFramework`: **TUnit** (default), `Xunit`, or `None` | | **Mocking provider** | `SubstituteFramework`: **TUnitMocks** (default), `NSubstitute`, or `None` | | **Test data provider** | `TestDataFramework`: **Bogus** (default) or `None` | @@ -42,7 +44,7 @@ A reusable MSBuild SDK NuGet package that delivers standardised .NET project def "runner": "Microsoft.Testing.Platform" }, "msbuild-sdks": { - "Purview.BuildSdk": "1.3.2" + "Purview.BuildSdk": "2.0.0" } } ``` @@ -201,7 +203,7 @@ to a low-importance message) and only a copy that still fails after every retry The bootstrapped `.editorconfig` opens with a version marker: ```editorconfig -# Purview.BuildSdk 1.3.2 - managed file, do not edit +# Purview.BuildSdk 2.0.0 - managed file, do not edit ``` That line is how the SDK recognises a file **it** wrote. When the SDK version changes, a file carrying @@ -363,7 +365,7 @@ Version detection logging is disabled by default. Set `VersionDetectionLogEnable | `PurviewTestContextNoWarn` | `CA1002;CA1012;CA1034;CA1047;CA1050;CA1051;CA1062;CA1064;CA1515;CA1707` | Semicolon-fenced rule set exempted in test and shared-testing projects, so test classes stay public, `Method_Scenario_Expectation` names keep their underscores, and helpers need no null guards. Override before the SDK import to narrow or extend it. | | `DisablePurviewTestContextRuleSet` | `false` | Set to `true` to make test and shared-testing projects enforce the production API-surface rules as well (the strict style contract applies either way). | | `PurviewSharedTestingOutputType` | `Library` | Output type forced on `IsSharedTestingProject` projects. `Library` (default) keeps them helper libraries - with `IsTestProject`/`IsTestingPlatformApplication` cleared - which also keeps their fixtures out of `CA1515`'s Exe-only scope. Set to `Exe` before the SDK import to keep the test packages' executable/test-host shape instead. | -| `TargetFramework` | `net10.0` | Override the default TFM per-project or globally. Defaults to `netstandard2.0` for projects declaring `IsRoslynComponent=true`. | +| `TargetFramework` | `net10.0;net11.0` | Override the default TFMs per-project or globally. Defaults to `netstandard2.0` for projects declaring `IsRoslynComponent=true`. | | `IsRoslynComponent` | `false` | When explicitly `true`, applies source-generator defaults: a single `netstandard2.0` target, `LangVersion=latest`, `Nullable=enable`, `TreatWarningsAsErrors=true`, `Deterministic=true`, extended analyzer rules, SourceLink with `EmbedUntrackedSources=true`, compiler-generated output under the intermediate directory, no dependency file, telemetry exclusion, and package build output. Pack-time validation (`ValidateRoslynComponentCompilerSettings`) fails the pack if the compiler defaults are missing unless `DisableRoslynCompilerDefaultsValidation=true`. Roslyn development dependencies (`Microsoft.CodeAnalysis.*`, `Microsoft.CodeAnalysis.Analyzers`) default to `PrivateAssets="all"`. | | `IsRoslynComponentOnly` | `true` for Roslyn components | Produces an analyzer-only package: `IncludeBuildOutput=false`, `IncludeSymbols=false`, and the analyzer assembly and portable PDB are packed under `analyzers/dotnet/cs/`. Set to `false` for a dual-role Roslyn component that uses normal library symbol packaging. | | `PackProjectReferencedSourceGenerators` | `true` | Automatically packs analyzer `ProjectReference` outputs and their runtime dependencies under `analyzers/dotnet/cs/`. Set to `false` to opt out; set `Pack="false"` on an individual reference to exclude only that generator. | @@ -375,6 +377,12 @@ Version detection logging is disabled by default. Set `VersionDetectionLogEnable | `DisableGenerateAssemblyIdentifierClass` | `false` (`true` for test and shared-testing projects) | Set to `true` to disable the generated assembly identifier marker class. | | `AssemblyIdentifierClassName` | `Assembly` | Overrides the name of the generated assembly identifier marker class. | | `AutoIncludeUsings` | `true` | Controls SDK-added global usings for `NamespacePrefix` and `RootNamespace`. | +| `MemorySafetyRules` | `2` | C# 15 memory-safety tier for `net11.0`+ targets: `0` off, `1` preview pointer syntax, `2` updated rules (`updated-memory-safety-rules`, the default). `AllowUnsafeBlocks` is never changed. | +| `PurviewRuntimeAsync` | `true` | Compiles `net11.0`+ targets with `runtime-async=on`. `UseRuntimeAsync=false` also opts out. | +| `IsClientSideProject` | *(detected)* | `true` for a project whose assemblies are downloaded to a browser — a WebAssembly client, `PurviewBrowserSafe` library, Razor class library, or `*.Client`/`*.Browser`/`*.Wasm`/`*.WebAssembly`/`*.Components` name. Set explicitly to override; a `ServerShared` project is never added to a client-side project. | +| `PurviewAutoServerSharedReference` | `true` | Adds `../ServerShared*/ServerShared*.csproj` to a non-client-side project. `false` removes the automatic reference. | +| `DisablePurviewServerSharedReferenceCheck` | `false` | Suppresses `PRSHR0001`/`PRSHR0002`. | +| `PurviewServerSharedReferenceCheckAsError` | `false` | Makes `PRSHR0001`/`PRSHR0002` errors instead of warnings. | ### Packable project defaults @@ -407,6 +415,9 @@ Non-packable projects (including web applications) default `WarnOnPackingNonPack | `RepositoryEditorConfigFilePath` | *(auto-detected)* | Override the destination path for the bootstrapped `.editorconfig`. | | `BootstrapGlobalJsonToRepoRoot` | `true` | Creates a `global.json` at the repository root when missing. | | `RepositoryGlobalJsonFilePath` | *(auto-detected)* | Override the destination path for the bootstrapped `global.json`. | +| `BootstrapMultiThreadedBuildRspToRepoRoot` | `true` | Creates a managed `Directory.Build.rsp` at the repository root that enables MSBuild's multithreaded build (`-mt`) on .NET 11. | +| `RepositoryDirectoryBuildRspFilePath` | *(auto-detected)* | Override the destination path for the bootstrapped `Directory.Build.rsp`. | +| `PurviewMultiThreadedBuild` | `true` | When `false`, a managed `Directory.Build.rsp` is rewritten to `-mt:false` so the mode is actually off. | | `PurviewBuildSdkVersionForGlobalJson` | *(restored package version, else the version it was packed as)* | Version written to the `msbuild-sdks.Purview.BuildSdk` entry in a bootstrapped `global.json`. | | `PurviewRepoBootstrapMode` | `IfMissing` | `IfMissing` never touches an existing file, `Always` overwrites it, `WarnOnDrift` reports a file that differs from the SDK-provided one, and `Never` skips bootstrapping. | | `PurviewRepoBootstrapCopyRetries` | `3` | Write attempts before a bootstrap write failure is reported. | @@ -565,6 +576,81 @@ The SDK now exports its properties via `CompilerVisibleProperty`, so analyzers a --- +## .NET 11 features + +.NET 11 is the baseline this SDK targets. The switches below are applied only to a `net11.0` (or +later) target, so the `net10.0` leg of the default `net10.0;net11.0` set is unaffected. + +| Feature | Applied | Why | +| -- | -- | -- | +| Runtime Async V2 | `runtime-async=on` | .NET 11 no longer requires `EnablePreviewFeatures`, and the runtime libraries ship compiled with it. Cleaner live stack traces, lower await overhead, and continuations that skip `ExecutionContext` when nothing is ambient. Opt out with `PurviewRuntimeAsync=false` or `UseRuntimeAsync=false`. | +| C# 15 updated memory-safety rules | `updated-memory-safety-rules` (`MemorySafetyRules=2`) | `unsafe` marks requires-unsafe APIs and the compiler records the choice in `MemorySafetyRulesAttribute`. Independent of `LangVersion`, so the default `latest` is enough. | +| Unsafe code | **not** enabled | `AllowUnsafeBlocks` stays at the compiler default (`false`). The updated model is strictest with unsafe code disallowed; a project that needs pointers opts in itself. | +| Multithreaded MSBuild | `Directory.Build.rsp` with `-mt` | Runs a build's projects concurrently in one process, which is 20–26% faster on large solutions per the MSBuild measurements. `-mt` is a host switch, so it cannot be set from a project; the response file is the only mechanism. `-nr:false` accompanies it because a node left by a non-`-mt` build can cache an `RoslynCodeTaskFactory` task with no assembly path and fail the next `-mt` build (`dotnet/msbuild#15213`); the short-lived server still gives Server GC. | + +`MemorySafetyRules` tiers: `0` (off), `1` (preview pointer syntax, sets `LangVersion=preview` unless the +project named its own version), `2` (updated rules, the default). Anything else fails with `PRCFG0001`. + +### Evaluated and deliberately not defaulted + +These improve a specific workload but would change behaviour or cost for every project, so they stay +opt-in: + +- **Server GC / `TieredPGO` / ReadyToRun** — the runtime and SDK already choose sensible defaults per + app type; a blanket `ServerGarbageCollection=true` is wrong for desktop apps and tests. +- **Native AOT and `PublishAot`** — a publish property, not a build default, and it changes the + artifact and the libraries an app may use. +- **`UseArtifactsOutput`** — a much faster, cache-friendly layout, but a breaking change to every + output path a repository may reference. +- **`EnablePackageValidation`** — needs a baseline package, so it is a per-package decision. +- **`NuGetAuditMode=all`** — a good security default, but it turns transitive vulnerability warnings + on for every repository, which is a policy decision rather than a build default. +- **`CheckForOverflowUnderflow`** — improves correctness at a measurable runtime cost; opt in where + arithmetic overflow is a real risk. + +The SDK does adopt the .NET 11 behaviour that needs no switch: `AnalysisLevel=latest` now resolves to +the .NET 11 rule set, the MSBuild server is on by default in the SDK, and the default C# version is +`latest` (C# 15). + +--- + +## ServerShared + +`ServerShared` is the server-side counterpart of `Shared`. A project is recognised by the same +name list shape (`ServerShared`, `ServerSharedFramework`, `ServerSharedInfrastructure`, +`ServerSharedInfra`, `ServerSharedUtilities`, `ServerSharedUtils`, `ServerSharedLibrary`, +`ServerSharedLib`, `ServerSharedHelpers`) and is referenced automatically by every project that +cannot run in a browser. + +`Shared` is referenced by almost everything, which is right for helpers that run anywhere, but it +leaks server-only code the moment a WebAssembly client reaches anything in that chain. So the +automatic `ServerShared` reference is skipped for anything that might be downloaded: + +| Excluded | Why | +| -- | -- | +| `Microsoft.NET.Sdk.BlazorWebAssembly` | the client itself is downloaded | +| `PurviewBrowserSafe=true` | by definition downloaded | +| `Microsoft.NET.Sdk.Razor` | a WebAssembly client can reference a Razor class library, and the build cannot know in advance whether it will | +| `*.Client`, `*.Browser`, `*.Wasm`, `*.WebAssembly`, `*.Components`, `Components` | the shape a Blazor Web App puts on the client | +| `Shared` and `SharedTesting` | a `Shared` project is itself referenced widely, so inheriting the reference would defeat the guarantee | + +`IsClientSideProject=true`/`false` overrides the detection either way, including restoring the +automatic reference to a server-only Razor class library (the conservative default would otherwise +hide it). + +Two diagnostics protect the guarantee when a reference is deliberate: + +- `PRSHR0001` — a client-side project references a `ServerShared` project. +- `PRSHR0002` — a `Shared` project references a `ServerShared` project. + +Both are warnings; `PurviewServerSharedReferenceCheckAsError=true` makes them errors and +`DisablePurviewServerSharedReferenceCheck=true` suppresses them. `PurviewBrowserSafe="true"` on the +`ProjectReference` is accepted as the explicit statement that the reference was judged safe, the same +escape hatch `PRBLZ0001` uses. `PurviewAutoServerSharedReference=false` removes the automatic +reference entirely. + +--- + ## Blazor WebAssembly and BFF Every behaviour here applies only to the project type it names. A project that is not a WebAssembly @@ -591,8 +677,8 @@ opts out). A host referencing a client under another name is resolved during the | -- | -- | | `IsPackable=false` (the project can still set `true`) | packing an app's `wwwroot` as a package nobody can consume | | no container defaults, even with a `Dockerfile` | `PublishAot` on a client is a publish that fails | -| no automatic `../Shared*` reference | a server-side library joining the browser download because of a folder name | -| `PRBLZ0001` for each `ProjectReference` that does not set `PurviewBrowserSafe` | a library joining the download without anyone deciding it was safe there | +| no automatic `../Shared*` or `../ServerShared*` reference | a server-side library joining the browser download because of a folder name | +| `PRBLZ0001` for each `ProjectReference` that does not set `PurviewBrowserSafe`, and `PRSHR0001` for a `ServerShared` reference | a library joining the download without anyone deciding it was safe there | Globalisation, trimming mode and the Blazor runtime settings are left to the Blazor SDK. `DisablePurviewBlazorWebAssemblyDefaults=true` (before the SDK import) restores the generic diff --git a/docs/wiki/Configuration-Reference.md b/docs/wiki/Configuration-Reference.md index d8a8f05..0e56da2 100644 --- a/docs/wiki/Configuration-Reference.md +++ b/docs/wiki/Configuration-Reference.md @@ -24,7 +24,7 @@ See [Version Detection](Version-Detection.md) for the full resolution rules. | `PurviewTestContextNoWarn` | `CA1002;CA1012;CA1034;CA1047;CA1050;CA1051;CA1062;CA1064;CA1515;CA1707` | Production API-surface rules exempted in test and shared-testing projects (they keep the strict style contract). Override before the SDK import to narrow or extend the list. | | `DisablePurviewTestContextRuleSet` | `false` | Set to `true` to make test and shared-testing projects enforce the production API-surface rules as well. | | `PurviewSharedTestingOutputType` | `Library` | Output type forced on `IsSharedTestingProject` projects; `Library` also clears `IsTestProject`/`IsTestingPlatformApplication`. Set to `Exe` before the SDK import to keep the test packages' executable/test-host shape. | -| `TargetFramework` | `net10.0` | Override the default TFM per-project or globally. Defaults to `netstandard2.0` for projects declaring `IsRoslynComponent=true`. To multi-target from a central, overridable definition rather than a hard-coded list, see [Target framework sets](#target-framework-sets). | +| `TargetFramework` | `net10.0;net11.0` | Override the default TFMs per-project or globally. Defaults to `netstandard2.0` for projects declaring `IsRoslynComponent=true`. To multi-target from a central, overridable definition rather than a hard-coded list, see [Target framework sets](#target-framework-sets). | | `DisableUnconditionalProjectDeclarationCheck` | `false` | Set to `true` to suppress `PurviewConditionedProjectDeclaration`. The SDK reads `TargetFramework`, `TargetFrameworks`, `IsRoslynComponent`, `IsRoslynComponentOnly`, `IsPackable` and `PurviewTargetFrameworkSet` from the project XML before the project body is evaluated, so it cannot evaluate a `Condition` on them and will not see the declaration — the SDK default applies instead. Declare those properties unconditionally, or set them in a file imported before this SDK. | | `IsRoslynComponent` | `false` | When explicitly `true`, applies source-generator defaults: a single `netstandard2.0` target, `LangVersion=latest`, `Nullable=enable`, `TreatWarningsAsErrors=true`, `Deterministic=true`, extended analyzer rules, SourceLink with `EmbedUntrackedSources=true`, compiler-generated output under the intermediate directory, no dependency file, symbol packaging (`IncludeSymbols=false` by default), telemetry exclusion, and package build output. Packable Roslyn components automatically pack the built analyzer assembly and its PDB into `analyzers/dotnet/cs/` (`PurviewPackAnalyzerPdb=true`; set `false` only when symbols are delivered another way — NuGet's `.snupkg` cannot host `analyzers/dotnet/cs` symbols). Pack-time validation (`ValidateRoslynComponentCompilerSettings`) fails the pack if the compiler defaults are missing unless `DisableRoslynCompilerDefaultsValidation=true`. Roslyn development dependencies (`Microsoft.CodeAnalysis.*`, `Microsoft.CodeAnalysis.Analyzers`) default to `PrivateAssets="all"`. | | `PackProjectReferencedSourceGenerators` | `true` | Automatically packs analyzer `ProjectReference` outputs and their runtime dependencies under `analyzers/dotnet/cs/`. Set to `false` to opt out; set `Pack="false"` on an individual reference to exclude only that generator. | @@ -36,6 +36,12 @@ See [Version Detection](Version-Detection.md) for the full resolution rules. | `DisableGenerateAssemblyIdentifierClass` | `false` (`true` for test and shared-testing projects) | Set to `true` to disable the generated assembly identifier marker class. See [Assembly identifier class](Assembly-Name-Generation.md#assembly-identifier-class). | | `AssemblyIdentifierClassName` | `Assembly` | Overrides the name of the generated assembly identifier marker class. | | `AutoIncludeUsings` | `true` | Controls SDK-added global usings for `NamespacePrefix` and `RootNamespace`. | +| `MemorySafetyRules` | `2` | C# 15 memory-safety tier on `net11.0`+ targets: `0` off, `1` preview pointer syntax (sets `LangVersion=preview` when the project has not chosen one), `2` updated rules (`updated-memory-safety-rules`). An unrecognised value fails with `PRCFG0001`. `AllowUnsafeBlocks` is never changed. | +| `PurviewRuntimeAsync` | `true` | Adds `runtime-async=on` on `net11.0`+ targets. `PurviewRuntimeAsync=false` or `UseRuntimeAsync=false` opts out. | +| `IsClientSideProject` | *(detected)* | `true` when the project is downloaded to a browser: a WebAssembly client, a `PurviewBrowserSafe` library, a Razor class library, or a `*.Client`/`*.Browser`/`*.Wasm`/`*.WebAssembly`/`*.Components` name. Set explicitly to override the detection either way. | +| `PurviewAutoServerSharedReference` | `true` | Adds `../ServerShared*/ServerShared*.csproj` to every non-client-side project. `false` disables it. | +| `DisablePurviewServerSharedReferenceCheck` | `false` | Turns off `PRSHR0001` and `PRSHR0002`. | +| `PurviewServerSharedReferenceCheckAsError` | `false` | Reports `PRSHR0001`/`PRSHR0002` as errors. | | `NamespaceRemoveSuffix` | *(built-in list)* | Item type listing the suffixes stripped from `RootNamespace`. Remove an entry **after** the `Sdk.props` import to keep that suffix in the namespace, e.g. ``. **Roslyn component names are not in the list** — `SourceGenerator`, `SourceGenerators`, `SourceGeneration`, `Generators`, `Analyzers`, `CodeFixers` and `CodeFixes` are part of a component's identity, and a generator, its analyzers and its code fixes are separate assemblies that each need their own namespace. Add one with `` if a repository really wants it collapsed. See [Namespace stripping](Assembly-Name-Generation.md#namespace-stripping). | ## Target framework sets @@ -53,7 +59,7 @@ moves every consumer at once: | Set | Resolves to | Use for | | -- | -- | -- | -| `Current` | `net10.0` | The default. A single target, stated explicitly. | +| `Current` | `net10.0;net11.0` | The default. The current LTS plus the latest release. | | `Latest` | `net11.0` | Newest release only. | | `Supported` | `net10.0;net11.0` | A package that should work on anything still in support. | | `Broad` | `net8.0;net9.0;net10.0` | Every shipped release — widest reach without committing to the newest major while it is pre-GA. | @@ -65,10 +71,9 @@ package should not imply a commitment to them. Choose `Broad` to keep them. `Broad` and `All` differ by one real product decision — whether the package commits to the newest release — so they are separate names rather than one set that silently widens every consumer of it. -A set is **always** in play: `Current` applies when nothing is selected, which resolves to the same -single TFM the SDK used to hard-code. The out-of-box result is therefore unchanged — but the value is -named and overridable in one place, so moving a repository (or every repository) between runtimes is a -property change rather than an edit in each project. +A set is **always** in play: `Current` applies when nothing is selected, and resolves to every release +still in support. The value is named and overridable in one place, so moving a repository (or every +repository) between runtimes is a property change rather than an edit in each project. Overrides are layered, narrowest first: @@ -86,8 +91,8 @@ collapse the project to a single target and break every multi-targeting consumer | Property | Default | Description | | -- | -- | -- | -| `PurviewTargetFrameworkSet` | `Current` | `Current`, `Latest`, `Supported`, `Broad` or `All`. An unrecognised name fails the build with `PurviewInvalidTargetFrameworkSet` rather than silently falling back to the single-TFM default. A single-entry set resolves to `TargetFramework`, so it does not pay for an outer multi-targeting build. | -| `PurviewTargetFrameworksCurrent` | `net10.0` | Redefines the `Current` set. | +| `PurviewTargetFrameworkSet` | `Current` | `Current`, `Latest`, `Supported`, `Broad` or `All`. An unrecognised name fails the build with `PurviewInvalidTargetFrameworkSet` rather than silently falling back to the default set. A single-entry set resolves to `TargetFramework`, so it does not pay for an outer multi-targeting build. | +| `PurviewTargetFrameworksCurrent` | `net10.0;net11.0` | Redefines the `Current` set. | | `PurviewTargetFrameworksLatest` | `net11.0` | Redefines the `Latest` set. | | `PurviewTargetFrameworksSupported` | `net10.0;net11.0` | Redefines the `Supported` set. | | `PurviewTargetFrameworksBroad` | `net8.0;net9.0;net10.0` | Redefines the `Broad` set. | @@ -152,6 +157,9 @@ operations skip them silently. | `RepositoryEditorConfigFilePath` | *(auto-detected)* | Override the destination path for the bootstrapped `.editorconfig`. | | `BootstrapGlobalJsonToRepoRoot` | `true` | Creates a `global.json` at the repository root when missing. | | `RepositoryGlobalJsonFilePath` | *(auto-detected)* | Override the destination path for the bootstrapped `global.json`. | +| `BootstrapMultiThreadedBuildRspToRepoRoot` | `true` | Creates a managed `Directory.Build.rsp` at the repository root that enables MSBuild's multithreaded build (`-mt` plus `-nr:false`) on .NET 11. Set to `false` to stop bootstrapping it. | +| `RepositoryDirectoryBuildRspFilePath` | *(auto-detected)* | Override the destination path for the bootstrapped `Directory.Build.rsp`. | +| `PurviewMultiThreadedBuild` | `true` | When `false`, a managed `Directory.Build.rsp` is rewritten to `-mt:false`, so opting out cannot leave the mode enabled. | | `PurviewBuildSdkVersionForGlobalJson` | *(restored package version, else the version it was packed as)* | Version written to the `msbuild-sdks.Purview.BuildSdk` entry in a bootstrapped `global.json`. | | `PurviewRepoBootstrapMode` | `IfMissing` | `IfMissing` never touches an existing file, `Always` overwrites it, `WarnOnDrift` reports that an existing file differs from the SDK-provided one, and `Never` skips bootstrapping entirely. | | `PurviewRepoBootstrapCopyRetries` | `3` | Copy attempts before a bootstrap write failure is reported. | @@ -160,9 +168,46 @@ operations skip them silently. | `PurviewSuppressCopyRetryWarnings` | `true` | Demotes MSB3026 copy retry notices to messages. Set to `false` to see every retry attempt. | Bootstrap writes are staged into a temporary file and renamed into place, so editors and tools never -observe a partially written `.editorconfig` or `global.json`. Because every project in a solution runs -the same bootstrapping targets, a lost race between parallel projects is a no-op: an existing file is -treated as success rather than a copy failure. +observe a partially written `.editorconfig`, `global.json` or `Directory.Build.rsp`. Because every +project in a solution runs the same bootstrapping targets, a lost race between parallel projects is a +no-op: an existing file is treated as success rather than a copy failure. + +The `Directory.Build.rsp` is SDK-managed: its first line carries `# Purview.BuildSdk - managed file`, +and only a file carrying that marker is refreshed. It contains `-mt` and `-nr:false`. `-mt` is a build +host switch, so a response file is the only way a project can turn it on; `-nr:false` avoids reusing a +node left by a non-`-mt` build, which can cache a `RoslynCodeTaskFactory` task with no assembly path +and fail the first `-mt` build (`dotnet/msbuild#15213`). `-mt` still uses the MSBuild server (short-lived) +for Server GC. + +## .NET 11 features + +A `net11.0`+ target compiles with these switches; the `net10.0` leg of a multi-targeted project is +unchanged. + +| Property | Applied | Notes | +| -- | -- | -- | +| `PurviewRuntimeAsync` | `true` | `runtime-async=on`. `UseRuntimeAsync=false` also opts out. | +| `MemorySafetyRules` | `2` | `updated-memory-safety-rules`; `1` sets `LangVersion=preview` for the pointer-syntax tier; `0` disables. `PRCFG0001` on an unknown value. | +| `AllowUnsafeBlocks` | *(compiler default: false)* | Never set by the SDK. | + +Server GC, `TieredPGO`, ReadyToRun, Native AOT, `UseArtifactsOutput`, `EnablePackageValidation`, +`NuGetAuditMode=all` and `CheckForOverflowUnderflow` were evaluated and deliberately left opt-in: +each changes behaviour or cost for every project rather than improving one workload. See +[.NET 11 features](https://github.com/purview-dev/build-sdk#net-11-features) in the README. + +## ServerShared + +A `ServerShared*` project is referenced automatically by every project that cannot run in a browser. +It is skipped for a WebAssembly client, a `PurviewBrowserSafe` library, a Razor class library, a +`*.Client`/`*.Browser`/`*.Wasm`/`*.WebAssembly`/`*.Components` project, and for `Shared`/`SharedTesting` +projects (which are themselves referenced widely). `IsClientSideProject` can override the classification +either way. + +`PRSHR0001` reports a client-side project that references a `ServerShared` project; `PRSHR0002` +reports a `Shared` project that does. Both are warnings by default, become errors with +`PurviewServerSharedReferenceCheckAsError=true`, and are suppressed by +`DisablePurviewServerSharedReferenceCheck=true`. `PurviewBrowserSafe="true"` on the `ProjectReference` +records that the reference was judged safe. ## Sdk folder packaging @@ -250,13 +295,23 @@ read them through `build_property.`: | `IsAspireHostProject` | True when SDK starts with `Aspire.Sdk.Host`. | | `IsCLIProject` | True when the project is a CLI project. | | `IsSharedProject` | True when the project is a shared project. | +| `IsServerSharedProject` | True for a `ServerShared*` project, which only non-client-side projects reference automatically. | +| `IsClientSideProject` | True when the project's assemblies are downloaded to a browser (WebAssembly client, browser-safe library, Razor class library, or a client-side name segment). | +| `PurviewAutoServerSharedReference` | When `true` (default), adds a sibling `ServerShared` reference to a non-client-side project. | +| `DisablePurviewServerSharedReferenceCheck` | When `true`, suppresses `PRSHR0001`/`PRSHR0002`. | +| `PurviewServerSharedReferenceCheckAsError` | When `true`, reports `PRSHR0001`/`PRSHR0002` as errors. | +| `MemorySafetyRules` | C# 15 memory-safety tier applied to `net11.0`+ targets. | +| `PurviewRuntimeAsync` | When `true` (default), `net11.0`+ targets compile with `runtime-async=on`. | +| `PurviewMultiThreadedBuild` | When `true` (default), a managed `Directory.Build.rsp` enables MSBuild `-mt`. | | `EditorConfigFilePath` | Path to the SDK-provided `.editorconfig` injected into `@(EditorConfigFiles)`. | | `RepositoryEditorConfigFilePath` | Destination path for bootstrapping a physical repo-level `.editorconfig`. | | `BootstrapEditorConfigToRepoRoot` | When `true` (default), copies the SDK `.editorconfig` to `RepositoryEditorConfigFilePath` if missing. | | `RepositoryGlobalJsonFilePath` | Destination path for bootstrapping a physical repo-level `global.json`. | | `BootstrapGlobalJsonToRepoRoot` | When `true` (default), creates `global.json` at `RepositoryGlobalJsonFilePath` if missing. | +| `RepositoryDirectoryBuildRspFilePath` | Destination path for bootstrapping a physical repo-level `Directory.Build.rsp`. | +| `BootstrapMultiThreadedBuildRspToRepoRoot` | When `true` (default), creates a managed `Directory.Build.rsp` enabling `-mt`. | | `PurviewBuildSdkVersionForGlobalJson` | Version used for `msbuild-sdks.Purview.BuildSdk` when bootstrapping `global.json`. | -| `DisableAutoCopySdkFiles` | When `true`, disables SDK auto-copy/bootstrap for repo files (`.editorconfig`, `global.json`). | +| `DisableAutoCopySdkFiles` | When `true`, disables SDK auto-copy/bootstrap for repo files (`.editorconfig`, `global.json`, `Directory.Build.rsp`). | | `CurrentYear` | Current year used in generated assembly metadata. | | `AutoGeneratedAssemblyInfoFile` | File name of the generated AssemblyInfo source file (written under the intermediate `Properties/` folder). | | `AutoGeneratedAssemblyIdentifierFile` | File name of the generated assembly identifier source file (written under the intermediate `Properties/` folder). | diff --git a/docs/wiki/Getting-Started.md b/docs/wiki/Getting-Started.md index 017ed63..1b3c797 100644 --- a/docs/wiki/Getting-Started.md +++ b/docs/wiki/Getting-Started.md @@ -14,7 +14,7 @@ Add `Purview.BuildSdk` to the `msbuild-sdks` section so MSBuild can resolve the "runner": "Microsoft.Testing.Platform" }, "msbuild-sdks": { - "Purview.BuildSdk": "1.3.2" + "Purview.BuildSdk": "2.0.0" } } ``` diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md index 2e3bfe5..7f48b2f 100644 --- a/docs/wiki/Home.md +++ b/docs/wiki/Home.md @@ -34,8 +34,8 @@ repo root inherits everything automatically. `.csproj` filename, the `Sdk` attribute, and on-disk markers. - **Engineering conventions** — repository layout, project naming, namespace identity, test readability, and the default boundaries between source, test, and shared test-support projects. -- **C# defaults** — `net10.0` TFM (overridable, `netstandard2.0` for Roslyn components), - `LangVersion=preview` (`latest` for Roslyn components), `Nullable=enable`, `ImplicitUsings=enable`, +- **C# defaults** — `net10.0;net11.0` TFMs (overridable, `netstandard2.0` for Roslyn components), + `LangVersion=latest` (C# 15, including unions), `Nullable=enable`, `ImplicitUsings=enable`, deterministic builds, `ManagePackageVersionsCentrally=true`. - **Code style** — an `.editorconfig` baked into the package, applied via `EditorConfigFilePath`, and auto-bootstrapped to the repo root if missing; `EnforceCodeStyleInBuild=true`, @@ -65,7 +65,7 @@ repo root inherits everything automatically. ## Requirements -- .NET SDK 10.0 or later (the default TFM for source projects is `net10.0`). +- .NET SDK 11.0 or later (the default TFMs for source projects are `net10.0` and `net11.0`). - A `NamespacePrefix` set before the SDK import. - A `Directory.Packages.props` at the repo root when Central Package Management is used (the SDK sets `ManagePackageVersionsCentrally=true`). diff --git a/docs/wiki/Project-Type-Detection.md b/docs/wiki/Project-Type-Detection.md index c0fadf7..cde3e5f 100644 --- a/docs/wiki/Project-Type-Detection.md +++ b/docs/wiki/Project-Type-Detection.md @@ -15,6 +15,8 @@ wiki. | `IsTestProject` | Project name matches `^.*\.([\w|_]\w*)Tests?$` — a target name, a dot, then any word before `Test`/`Tests`. The captured word becomes `TestingType`. | | `IsSharedTestingProject` | Project name is one of `SharedTestingFramework`, `SharedTestingInfrastructure`, `SharedTestingInfra`, `SharedTestingUtilities`, `SharedTestingUtils`, `SharedTestingLibrary`, `SharedTestingLib`, `SharedTestingHelpers`. | | `IsSharedProject` | Project name is one of `Shared`, `SharedFramework`, `SharedInfrastructure`, `SharedInfra`, `SharedUtilities`, `SharedUtils`, `SharedLibrary`, `SharedLib`, `SharedHelpers`. | +| `IsServerSharedProject` | Project name is one of `ServerShared`, `ServerSharedFramework`, `ServerSharedInfrastructure`, `ServerSharedInfra`, `ServerSharedUtilities`, `ServerSharedUtils`, `ServerSharedLibrary`, `ServerSharedLib`, `ServerSharedHelpers`. | +| `IsClientSideProject` | A WebAssembly client, a `PurviewBrowserSafe` library, a Razor class library, or a project named `*.Client`, `*.ClientShared`, `*.Browser`, `*.Wasm`, `*.WebAssembly`, `*.Components`, `Components`, or `ClientShared`. An explicit value overrides the detection. | | `IsContainerProject` | A `Dockerfile`, `dockerfile`, or `Dockerfile.dev` exists in the project directory. | | `IsSdkProject` | An `Sdk` value is parsed from the ``/`` element. | | `IsWebSdkProject` | `SdkProjectName` is `Microsoft.NET.Sdk.Web`. | @@ -87,6 +89,16 @@ The SDK ships the following full `TestType` set, in addition to the standard `Un (`../Shared*/Shared*.csproj`); test projects automatically reference `../SharedTesting*/SharedTesting*.csproj`. +### ServerShared projects (`IsServerSharedProject=true`) + +- Referenced automatically by every non-client-side project via wildcard + (`../ServerShared*/ServerShared*.csproj`). +- Never referenced automatically by a WebAssembly client, a `PurviewBrowserSafe` library, a Razor class + library, a project named with a client-side segment, or another `Shared`/`SharedTesting` project. + `IsClientSideProject` overrides the classification either way. +- `PRSHR0001` reports a client-side project that references one; `PRSHR0002` reports a `Shared` + project that does. `PurviewBrowserSafe="true"` on the reference records an accepted decision. + ### Container projects - `InvariantGlobalization=true`, `PublishAot=true`, `DockerDefaultTargetOS=Linux`, diff --git a/docs/wiki/Repository-Bootstrap.md b/docs/wiki/Repository-Bootstrap.md index 8348e36..5cc07cd 100644 --- a/docs/wiki/Repository-Bootstrap.md +++ b/docs/wiki/Repository-Bootstrap.md @@ -66,9 +66,41 @@ file, and honours `PurviewRepoBootstrapMode`: | `DisableAutoCopySdkFiles` | `false` | Master switch that disables repo-level SDK file bootstrapping. | | `PurviewRepoBootstrapMode` | `IfMissing` | `IfMissing`, `Always`, `WarnOnDrift` or `Never`. | +## `Directory.Build.rsp` bootstrapping + +`-mt` is an MSBuild **host** switch, so no project property can enable it: the host reads its switches +before evaluation. A `Directory.Build.rsp` in any ancestor of a project is the one file the CLI reads +that can, so the SDK writes one at the repository root: + +```text +# Purview.BuildSdk - managed file +# Managed by Purview.BuildSdk. Delete the marker line to take ownership of this file. +-mt +-nr:false +``` + +`-nr:false` is deliberate. A node left by a non-`-mt` build can hold a cached `RoslynCodeTaskFactory` +task with no assembly path, and the first `-mt` build that reuses it fails with `MSB0001` +([dotnet/msbuild#15213](https://github.com/dotnet/msbuild/issues/15213)) — which is exactly the +transition a repository makes when this file first appears. A short-lived server still provides the +Server GC `-mt` expects. `-mt` requires .NET 11; the bootstrap is skipped on an older SDK. + +The file is SDK-managed: a destination carrying the marker line is refreshed, one without it belongs +to the developer and is never touched. `PurviewMultiThreadedBuild=false` rewrites it to `-mt:false` +rather than deleting it, so opting out cannot leave a stale copy enabling the mode. + +Like the other bootstraps it writes atomically, retries quietly, and honours `PurviewRepoBootstrapMode`: + +| Property | Default | Description | +| -- | -- | -- | +| `BootstrapMultiThreadedBuildRspToRepoRoot` | `true` | Creates the `Directory.Build.rsp` when missing. | +| `RepositoryDirectoryBuildRspFilePath` | *(auto-detected)* | Override the destination path. | +| `PurviewMultiThreadedBuild` | `true` | `false` writes `-mt:false` instead of `-mt`. | +| `DisableAutoCopySdkFiles` | `false` | Master switch that disables repo-level SDK file bootstrapping. | + ## Retry and failure behaviour -Both bootstraps share the same retry contract: +All bootstraps share the same retry contract: | Property | Default | Description | | -- | -- | -- | @@ -82,11 +114,11 @@ error by default, including the destination path and the underlying OS error. ## Repository root discovery -Both bootstrap targets locate the repository root by: +All bootstrap targets locate the repository root by: 1. Running `git rev-parse --show-toplevel` from the `Directory.Build.props` directory. 2. Falling back to probing upward for a `.git` marker. 3. Finally falling back to the `Directory.Build.props` directory itself. -Set the `RepositoryEditorConfigFilePath` / `RepositoryGlobalJsonFilePath` properties explicitly to -override discovery. \ No newline at end of file +Set the `RepositoryEditorConfigFilePath`, `RepositoryGlobalJsonFilePath` or +`RepositoryDirectoryBuildRspFilePath` property explicitly to override discovery. \ No newline at end of file diff --git a/global.json b/global.json index dcef14c..b03f93d 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,7 @@ { - "test": { - "runner": "Microsoft.Testing.Platform" - } -} \ No newline at end of file + "sdk": { "version": "11.0.100" }, + + "test": { + "runner": "Microsoft.Testing.Platform" + } +} diff --git a/package.json b/package.json index 71d3a7e..e361a6d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "build-sdk", - "version": "1.3.2", + "version": "2.0.0", "homepage": "https://purview.dev/projects/build-sdk/", "bugs": { "url": "https://github.com/purview-dev/build-sdk/issues" @@ -9,4 +9,4 @@ "type": "git", "url": "git+https://github.com/purview-dev/build-sdk.git" } -} \ No newline at end of file +} diff --git a/skills/purview-buildsdk-configuration/SKILL.md b/skills/purview-buildsdk-configuration/SKILL.md index e081eac..b540704 100644 --- a/skills/purview-buildsdk-configuration/SKILL.md +++ b/skills/purview-buildsdk-configuration/SKILL.md @@ -41,7 +41,7 @@ in `Directory.Build.props` unless the setting is deliberately project-specific. | `TestingFramework` | `TUnit` | `TUnit`, `Xunit` or `None` | | `SubstituteFramework` | `TUnitMocks` | `TUnitMocks`, `NSubstitute` or `None` | | `TestDataFramework` | `Bogus` | `Bogus` or `None` | -| `TargetFramework` | `net10.0` | `netstandard2.0` when `IsRoslynComponent=true` | +| `TargetFramework` | `net10.0;net11.0` | `netstandard2.0` when `IsRoslynComponent=true` | | `IsPackable` | `false` | Opts a project into the packable defaults | | `UsePackageJsonVersion` | `true` | `true`, `false` or `Strict` — version from `package.json` | | `ExcludePurviewTelemetry` | `false` | Drops the telemetry source generator | @@ -50,6 +50,11 @@ in `Directory.Build.props` unless the setting is deliberately project-specific. | `PurviewBrowserSafe` | *(unset)* | On a class library a WebAssembly client downloads: trim, AOT and browser-unsupported APIs fail the build | | `IsBffProject` | name ends `Bff` | Classifies a BFF host; `false` opts out | | `PurviewBrowserSafeReferenceCheckAsError` | `false` | Makes `PRBLZ0001` (a client reference that is not browser-safe) an error | +| `MemorySafetyRules` | `2` | C# 15 memory-safety tier on `net11.0`+: `0` off, `1` preview syntax, `2` updated rules | +| `PurviewRuntimeAsync` | `true` | `runtime-async=on` on `net11.0`+ | +| `IsClientSideProject` | *(computed)* | Whether the project's assemblies can reach a browser; overrides the `ServerShared` classification | +| `PurviewAutoServerSharedReference` | `true` | Adds a sibling `ServerShared` reference to a non-client-side project | +| `PurviewMultiThreadedBuild` | `true` | Bootstraps a `Directory.Build.rsp` enabling MSBuild `-mt` | The full set, grouped by area and with the behaviour each one changes, is in [references/properties.md](references/properties.md). Read it rather than guessing a property name — @@ -89,11 +94,15 @@ is the SDK's answer, and it stays correct when the rules change. The exported se ## Repository bootstrap -The SDK bootstraps two repository-wide files from the package: `.editorconfig` and `global.json`. +The SDK bootstraps three repository-wide files from the package: `.editorconfig`, `global.json` and +`Directory.Build.rsp`. - `PurviewRepoBootstrapMode` — `IfMissing` (default), `Always`, `WarnOnDrift`, `Never` - `DisableAutoCopySdkFiles` — the master off switch - `PurviewRepoBootstrapCopyRetries` / `...RetryDelayMilliseconds` / `...FailureAsError` +- `BootstrapMultiThreadedBuildRspToRepoRoot` / `PurviewMultiThreadedBuild` — the response file that + enables MSBuild `-mt` (and `-nr:false`, which avoids a `RoslynCodeTaskFactory` cache left by a + non-`-mt` build) Every project in a solution writes those same destinations, so the write is race-tolerant by design: an existing file is never overwritten unless asked, each write is staged into a temporary diff --git a/skills/purview-buildsdk-configuration/references/properties.md b/skills/purview-buildsdk-configuration/references/properties.md index 21ddc7f..82f84d7 100644 --- a/skills/purview-buildsdk-configuration/references/properties.md +++ b/skills/purview-buildsdk-configuration/references/properties.md @@ -21,7 +21,7 @@ consuming project supplies nothing. | Property | Default | Behaviour | | -- | -- | -- | -| `TargetFramework` | `net10.0` | Applied when neither `TargetFramework` nor `TargetFrameworks` is set. A project that explicitly declares `IsRoslynComponent=true` defaults to `netstandard2.0` | +| `TargetFramework` | `net10.0;net11.0` | Applied when neither `TargetFramework` nor `TargetFrameworks` is set. A project that explicitly declares `IsRoslynComponent=true` defaults to `netstandard2.0` | | `IsRoslynComponent` | *(unset)* | When explicitly `true`: a single `netstandard2.0` target, `LangVersion=latest`, `Nullable=enable`, `TreatWarningsAsErrors=true`, `Deterministic=true`, extended analyzer rules, SourceLink with `EmbedUntrackedSources=true`, no dependency file, telemetry exclusion, and `PrivateAssets=all` on `Microsoft.CodeAnalysis.*` references | | `IsRoslynComponentOnly` | `true` for Roslyn components | Analyzer-only package: `IncludeBuildOutput=false`, `IncludeSymbols=false`, portable PDB packed beside the analyzer under `analyzers/dotnet/cs/`. Set `false` for a dual-role component using normal library symbol packaging | | `DisableRoslynCompilerDefaultsValidation` | `false` | Opts out of `ValidateRoslynComponentCompilerSettings`, which fails the pack of a packable Roslyn component missing `LangVersion`, `Nullable`, `TreatWarningsAsErrors` or `EnforceExtendedAnalyzerRules` | @@ -36,6 +36,19 @@ consuming project supplies nothing. | `DisablePurviewTestContextRuleSet` | `false` | Makes test and shared-testing projects enforce the production API-surface rules too | | `PurviewPolicyExemptNoWarn` | *(derived)* | The SDK-injected `NoWarn` entries `ValidatePurviewStylePolicy` accepts | +## .NET 11 features and ServerShared + +| Property | Default | Behaviour | +| -- | -- | -- | +| `MemorySafetyRules` | `2` | C# 15 memory-safety tier on `net11.0`+ targets: `0` off, `1` preview pointer syntax (sets `LangVersion=preview` when none was chosen), `2` `updated-memory-safety-rules`. Unknown values fail with `PRCFG0001`. Never touches `AllowUnsafeBlocks` | +| `PurviewRuntimeAsync` | `true` | `runtime-async=on` on `net11.0`+ targets. `UseRuntimeAsync=false` also opts out | +| `PurviewMultiThreadedBuild` | `true` | Bootstraps a managed `Directory.Build.rsp` with `-mt`/`-nr:false`; `false` writes `-mt:false` | +| `BootstrapMultiThreadedBuildRspToRepoRoot` | `true` | Whether that bootstrapped response file is written at all | +| `IsClientSideProject` | *(computed)* | `true` for a WebAssembly client, `PurviewBrowserSafe` library, Razor class library, or `*.Client`/`*.Browser`/`*.Wasm`/`*.WebAssembly`/`*.Components` name. Explicit value wins | +| `PurviewAutoServerSharedReference` | `true` | Adds `../ServerShared*/ServerShared*.csproj` to a non-client-side project | +| `PurviewServerSharedReferenceCheckAsError` | `false` | Makes `PRSHR0001`/`PRSHR0002` errors | +| `DisablePurviewServerSharedReferenceCheck` | `false` | Turns `PRSHR0001`/`PRSHR0002` off | + ## Testing | Property | Default | Behaviour | @@ -119,6 +132,9 @@ reference was judged browser-safe when it cannot say so itself. | `RepositoryEditorConfigFilePath` | *(derived)* | Overrides the destination `.editorconfig` | | `BootstrapGlobalJsonToRepoRoot` | `true` | | | `RepositoryGlobalJsonFilePath` | *(derived)* | Overrides the destination `global.json` | +| `BootstrapMultiThreadedBuildRspToRepoRoot` | `true` | Creates a managed `Directory.Build.rsp` enabling `-mt` | +| `RepositoryDirectoryBuildRspFilePath` | *(derived)* | Overrides the destination `Directory.Build.rsp` | +| `PurviewMultiThreadedBuild` | `true` | `false` rewrites a managed rsp to `-mt:false` | | `PurviewBuildSdkVersionForGlobalJson` | restored package version, else the version it was packed as | | | `PurviewRepoBootstrapCopyRetries` | `3` | | | `PurviewRepoBootstrapCopyRetryDelayMilliseconds` | `500` | | @@ -148,7 +164,11 @@ Exported to analyzers and generators as `build_property.`: - `ExcludePurviewTelemetry`, `ExcludeMSTelemetryExtension` - `EnableAssemblyNameGeneration`, `DisableAutoInternalsVisibleTo`, `DisableGenerateAssemblyInfoClass`, `DisableGenerateAssemblyIdentifierClass`, `AssemblyIdentifierClassName` -- `IsCSharpProject`, `IsTestProject`, `IsSharedTestingProject`, `IsSharedProject` +- `IsCSharpProject`, `IsTestProject`, `IsSharedTestingProject`, `IsSharedProject`, + `IsServerSharedProject`, `IsClientSideProject` +- `PurviewAutoServerSharedReference`, `DisablePurviewServerSharedReferenceCheck`, + `PurviewServerSharedReferenceCheckAsError` +- `MemorySafetyRules`, `PurviewRuntimeAsync`, `PurviewMultiThreadedBuild` - `TestingType`, `TargetProjectName` - `IsContainerProject`, `IsSdkProject`, `SdkProjectName`, `IsWebProject`, `IsWebSdkProject`, `IsWorkerSdkProject`, `IsAspireHostProject`, `IsCLIProject` diff --git a/skills/purview-buildsdk-project-detection/SKILL.md b/skills/purview-buildsdk-project-detection/SKILL.md index 8573ca4..bd80b3f 100644 --- a/skills/purview-buildsdk-project-detection/SKILL.md +++ b/skills/purview-buildsdk-project-detection/SKILL.md @@ -1,6 +1,6 @@ --- name: purview-buildsdk-project-detection -description: Explain why Purview.BuildSdk classified a project as test, shared, shared-testing, CLI, web, worker, Aspire host, container, Roslyn component, Blazor WebAssembly client, Razor class library, Blazor host or BFF, and why it added a package, a project reference, a namespace or an analyzer automatically. Use when SDK behaviour looks surprising, when a test project gets no runner or no InternalsVisibleTo, or when reasoning about RootNamespace, AssemblyName and TargetProjectName. +description: Explain why Purview.BuildSdk classified a project as test, shared, shared-testing, ServerShared, client-side, CLI, web, worker, Aspire host, container, Roslyn component, Blazor WebAssembly client, Razor class library, Blazor host or BFF, and why it added a package, a project reference, a namespace or an analyzer automatically. Use when SDK behaviour looks surprising, when a test project gets no runner or no InternalsVisibleTo, or when reasoning about RootNamespace, AssemblyName and TargetProjectName. license: MIT metadata: package: Purview.BuildSdk @@ -25,7 +25,8 @@ dotnet msbuild -getProperty:PurviewLogicalProjectName -getProperty:Roo The values worth inspecting first: `MSBuildProjectName`, `NamespacePrefix`, `PurviewLogicalProjectName`, `RootNamespace`, `TestingType`, `TargetProjectName`, `SdkProjectName`, -`IsTestProject`, `IsSharedProject`, `IsSharedTestingProject`, `IsContainerProject`, `IsCLIProject`, +`IsTestProject`, `IsSharedProject`, `IsServerSharedProject`, `IsClientSideProject`, +`IsSharedTestingProject`, `IsContainerProject`, `IsCLIProject`, `IsWebSdkProject`, `IsAspireHostProject`, `IsBlazorWebAssemblyProject`, `IsRazorClassLibraryProject`, `IsBlazorHostProject`, `IsBffProject`. @@ -52,11 +53,16 @@ a non-test project grants access to `{AssemblyName}.{TestType}Tests` for each na suffix outside it still builds, runs and gets a category — it just cannot see `internal` members. The 53 names are in [references/test-types.md](references/test-types.md). -### Shared and shared-testing projects +### Shared, shared-testing and ServerShared projects -Recognised by **exact name**, never by substring. The two lists are in +Recognised by **exact name**, never by substring. The three lists are in [references/detection.md](references/detection.md). Derived: `IsSharedProject`, -`IsSharedTestingProject`. +`IsSharedTestingProject`, `IsServerSharedProject`. + +`ServerShared` is separate from `Shared` for a reason: a `Shared` project is referenced by almost +everything, including code that ends up in a browser, so server-only helpers get their own name and +are referenced automatically only by a project that cannot be downloaded. `IsClientSideProject` is the +classification that decides it. ### SDK and content based diff --git a/skills/purview-buildsdk-project-detection/references/detection.md b/skills/purview-buildsdk-project-detection/references/detection.md index 85fde09..51243c9 100644 --- a/skills/purview-buildsdk-project-detection/references/detection.md +++ b/skills/purview-buildsdk-project-detection/references/detection.md @@ -5,6 +5,20 @@ `Shared`, `SharedFramework`, `SharedInfrastructure`, `SharedInfra`, `SharedUtilities`, `SharedUtils`, `SharedLibrary`, `SharedLib`, `SharedHelpers` +## ServerShared project names (exact) + +`ServerShared`, `ServerSharedFramework`, `ServerSharedInfrastructure`, `ServerSharedInfra`, +`ServerSharedUtilities`, `ServerSharedUtils`, `ServerSharedLibrary`, `ServerSharedLib`, +`ServerSharedHelpers` + +A `ServerShared` project is referenced automatically by every non-client-side project, and never by a +WebAssembly client, a `PurviewBrowserSafe` library, a Razor class library, a project named with a +client-side segment (`*.Client`, `*.ClientShared`, `*.Browser`, `*.Wasm`, `*.WebAssembly`, +`*.Components`, or exactly `Components`/`ClientShared`), or a `Shared`/`SharedTesting` project. +`IsClientSideProject` overrides the classification. `PRSHR0001` reports a client-side reference and +`PRSHR0002` a `Shared` reference; `PurviewBrowserSafe="true"` on the reference records an accepted +decision. + ## Shared testing project names (exact) `SharedTestingFramework`, `SharedTestingInfrastructure`, `SharedTestingInfra`, @@ -64,7 +78,8 @@ dual-role component that wants normal library symbol packaging. | Container | `InvariantGlobalization`, `PublishAot`, Linux Docker defaults, container tooling packages | | Web SDK | `Microsoft.AspNetCore.OpenApi.Generated` added to `InterceptorsNamespaces`, unless marked as a separate web-project mode | | Aspire host | `OutputType=Exe` | -| Blazor WebAssembly client | not packable, no container defaults, no automatic `Shared` reference, the browser-safe reference check - see [blazor.md](blazor.md) | +| Blazor WebAssembly client | not packable, no container defaults, no automatic `Shared`/`ServerShared` reference, the browser-safe reference check - see [blazor.md](blazor.md) | +| ServerShared project | referenced automatically by every non-client-side project; never by a WebAssembly client, browser-safe library, Razor class library, client-side name, or `Shared`/`SharedTesting` project | | Blazor host | an automatic reference to its `.Client` - see [blazor.md](blazor.md) | | BFF | no packages, no OpenAPI interceptor unless it exposes an API, `PDS0008`/`PDS0009` - see [blazor.md](blazor.md) | | `PurviewBrowserSafe` library | `IsAotCompatible`, `SupportedPlatform browser`, trim/AOT/`CA1416` as errors - see [blazor.md](blazor.md) | diff --git a/src/src/Analyzers/EnglishNaming/EnglishNamingAnalyzer.cs b/src/src/Analyzers/EnglishNaming/EnglishNamingAnalyzer.cs index 71dbe25..587005f 100644 --- a/src/src/Analyzers/EnglishNaming/EnglishNamingAnalyzer.cs +++ b/src/src/Analyzers/EnglishNaming/EnglishNamingAnalyzer.cs @@ -69,9 +69,8 @@ ConcurrentDictionary configCache return; } - var config = configCache.GetOrAdd( - declaringTree, - tree => EnglishNamingConfig.FromOptions(context.Options.AnalyzerConfigOptionsProvider.GetOptions(tree)) + var config = configCache.GetOrAdd(declaringTree, tree => + EnglishNamingConfig.FromOptions(context.Options.AnalyzerConfigOptionsProvider.GetOptions(tree)) ); var corrected = EnglishNamingHelper.CorrectIdentifier(symbol.Name, config); diff --git a/src/src/BuildSdk/Sdk/Props/Defaults.props b/src/src/BuildSdk/Sdk/Props/Defaults.props index 737b989..6733b4e 100644 --- a/src/src/BuildSdk/Sdk/Props/Defaults.props +++ b/src/src/BuildSdk/Sdk/Props/Defaults.props @@ -2,6 +2,10 @@ <_SharedProjectNames Condition="'$(_SharedProjectNames)' == ''" >;Shared;SharedFramework;SharedInfrastructure;SharedInfra;SharedUtilities;SharedUtils;SharedLibrary;SharedLib;SharedHelpers; + + <_ServerSharedProjectNames Condition="'$(_ServerSharedProjectNames)' == ''" + >;ServerShared;ServerSharedFramework;ServerSharedInfrastructure;ServerSharedInfra;ServerSharedUtilities;ServerSharedUtils;ServerSharedLibrary;ServerSharedLib;ServerSharedHelpers; <_SharedTestingProjectNames Condition="'$(_SharedTestingProjectNames)' == ''" >;SharedTestingFramework;SharedTestingInfrastructure;SharedTestingInfra;SharedTestingUtilities;SharedTestingUtils;SharedTestingLibrary;SharedTestingLib;SharedTestingHelpers; @@ -23,6 +27,11 @@ >true true + + true true false @@ -138,14 +147,32 @@ false false false + false false false false false false - preview + + latest true + + 2 enable @@ -160,6 +187,19 @@ TUnitMocks Bogus + + true + + false + true + + true @@ -221,14 +261,21 @@ + + + # Purview.BuildSdk - managed file Microsoft.SourceLink.GitHub snupkg - diff --git a/src/src/BuildSdk/Sdk/Sdk.props b/src/src/BuildSdk/Sdk/Sdk.props index f150f83..e8a2b73 100644 --- a/src/src/BuildSdk/Sdk/Sdk.props +++ b/src/src/BuildSdk/Sdk/Sdk.props @@ -15,11 +15,13 @@ ===================================================================== --> <_SharedProjectNames>;Shared;SharedFramework;SharedInfrastructure;SharedInfra;SharedUtilities;SharedUtils;SharedLibrary;SharedLib;SharedHelpers; + <_ServerSharedProjectNames>;ServerShared;ServerSharedFramework;ServerSharedInfrastructure;ServerSharedInfra;ServerSharedUtilities;ServerSharedUtils;ServerSharedLibrary;ServerSharedLib;ServerSharedHelpers; <_SharedTestingProjectNames>;SharedTestingFramework;SharedTestingInfrastructure;SharedTestingInfra;SharedTestingUtilities;SharedTestingUtils;SharedTestingLibrary;SharedTestingLib;SharedTestingHelpers; + @@ -41,6 +43,9 @@ repository really wants it collapsed. ===================================================================== --> + + @@ -183,7 +188,7 @@ 2. Only *unconditional* declarations are matched. A `Condition` attribute cannot be evaluated at this point, and guessing either way is worse than not matching: treating a conditioned `` as a declaration makes this SDK skip its - `netstandard2.0` Roslyn default, the generic `net10.0` default fill in, and - when the + `netstandard2.0` Roslyn default, the generic default fill in, and - when the condition does not fire - an analyzer ship on a TFM the IDE cannot load. A conditioned declaration is reported by `PurviewValidateUnconditionalProjectDeclarations` in Sdk.targets instead of being silently misread. @@ -309,6 +314,9 @@ true + true $(NamespacePrefix) $(PurviewNamespacePrefix) @@ -535,7 +543,7 @@ When true, PRBLZ0001 is reported as an error instead of a warning. Defaults to false. - When true, a Blazor WebAssembly client keeps the generic defaults (packable when asked, container detection, the automatic Shared reference). Set before the SDK import. + When true, a Blazor WebAssembly client keeps the generic defaults (packable when asked, container detection, the automatic Shared reference). Set before the SDK import. ServerShared is never added to a client-side project, whatever this says. When false, a host named Name does not automatically reference its Name.Client WebAssembly client. Defaults to true. @@ -543,6 +551,24 @@ When true, a BFF keeps the OpenAPI interceptor namespace. Defaults to true only when the BFF references Microsoft.AspNetCore.OpenApi. + + When false, a server-side project does not automatically reference a sibling ServerShared project. Client-side projects never get one. Defaults to true. + + + When true, a client-side or Shared project does not report a ServerShared project reference (PRSHR0001/PRSHR0002). + + + When true, PRSHR0001/PRSHR0002 are errors instead of warnings. Defaults to false. + + + The C# 15 memory-safety tier: 0 off, 1 preview syntax, 2 updated rules (default). AllowUnsafeBlocks is left unset. + + + When true (default), a net11.0+ project compiles with the runtime-async compiler feature. UseRuntimeAsync=false opts out. + + + When true (default), boots a Directory.Build.rsp that enables MSBuild's multithreaded build (-mt). + Path to the SDK-provided .editorconfig used for build-time style enforcement. @@ -558,6 +584,12 @@ When true, bootstraps global.json to RepositoryGlobalJsonFilePath if that file does not exist. + + Destination path for bootstrapping a repository-level Directory.Build.rsp when missing. + + + When true, bootstraps a managed Directory.Build.rsp that enables MSBuild's multithreaded build (-mt). + When true, disables SDK auto-copy bootstrapping for repository files such as .editorconfig and global.json. @@ -582,6 +614,12 @@ True when the project is a shared project. + + True for ServerShared helper projects (ServerSharedFramework, etc.), which are referenced automatically only by projects that cannot run in a browser. + + + True when the project's assemblies are downloaded to a browser: a WebAssembly client, a PurviewBrowserSafe library, a Razor class library, or a project named with a client-side suffix (*.Client, *.Browser, *.Wasm, *.WebAssembly, *.Components). Set explicitly to override. + net11.0 - + net10.0 - + >net10.0;net11.0 + net10.0;net11.0 + this file - is what decides a project's targets. It resolves to every release still in + support, so one SDK bump moves every consumer that never chose a set. --> Current <_PurviewResolvedTargetFrameworkSet Condition="'$(PurviewTargetFrameworkSet)' == 'Latest'" @@ -778,8 +817,11 @@ quietly built one TFM and every multi-targeting consumer failed to reference it with "Project 'X' targets 'net10.0'. It cannot be referenced by a project that targets '.NETCoreApp,Version=v9.0'". --> + $(PurviewTargetFrameworksCurrent) $(PurviewTargetFrameworksCurrent) true <_PurviewSharedProjectRemovePattern>$([System.Text.RegularExpressions.Regex]::Replace('$(_SharedProjectNames)', '^;|;$', '').Replace(';', '|')) + <_PurviewServerSharedProjectRemovePattern>$([System.Text.RegularExpressions.Regex]::Replace('$(_ServerSharedProjectNames)', '^;|;$', '').Replace(';', '|')) <_PurviewSharedTestingProjectRemovePattern>$([System.Text.RegularExpressions.Regex]::Replace('$(_SharedTestingProjectNames)', '^;|;$', '').Replace(';', '|')) - <_PurviewNamespaceRemovePattern>$(_PurviewSharedProjectRemovePattern)|$(_PurviewSharedTestingProjectRemovePattern)|Core|EF|Shared|ClientShared|ServiceDefaults + <_PurviewNamespaceRemovePattern>$(_PurviewServerSharedProjectRemovePattern)|$(_PurviewSharedProjectRemovePattern)|$(_PurviewSharedTestingProjectRemovePattern)|Core|EF|Shared|ClientShared|ServiceDefaults $(PurviewLogicalProjectName) diff --git a/src/src/BuildSdk/Sdk/Sdk.targets b/src/src/BuildSdk/Sdk/Sdk.targets index e7adf63..b60a2f7 100644 --- a/src/src/BuildSdk/Sdk/Sdk.targets +++ b/src/src/BuildSdk/Sdk/Sdk.targets @@ -9,6 +9,8 @@ + + + + + <_DirectoryBuildPropsDirectory>$([System.IO.Path]::GetDirectoryName('$(DirectoryBuildPropsPath)')) + + <_RepositoryProbe0>$(_DirectoryBuildPropsDirectory) + <_RepositoryProbe1>$([System.IO.Path]::GetFullPath('$(_RepositoryProbe0)\..')) + <_RepositoryProbe2>$([System.IO.Path]::GetFullPath('$(_RepositoryProbe1)\..')) + <_RepositoryProbe3>$([System.IO.Path]::GetFullPath('$(_RepositoryProbe2)\..')) + <_RepositoryProbe4>$([System.IO.Path]::GetFullPath('$(_RepositoryProbe3)\..')) + <_DetectedRepositoryRoot + Condition="'$(_DetectedRepositoryRoot)' == '' AND (Exists('$(_RepositoryProbe0)/.git/HEAD') OR Exists('$(_RepositoryProbe0)/.git'))" + >$(_RepositoryProbe0) + <_DetectedRepositoryRoot + Condition="'$(_DetectedRepositoryRoot)' == '' AND (Exists('$(_RepositoryProbe1)/.git/HEAD') OR Exists('$(_RepositoryProbe1)/.git'))" + >$(_RepositoryProbe1) + <_DetectedRepositoryRoot + Condition="'$(_DetectedRepositoryRoot)' == '' AND (Exists('$(_RepositoryProbe2)/.git/HEAD') OR Exists('$(_RepositoryProbe2)/.git'))" + >$(_RepositoryProbe2) + <_DetectedRepositoryRoot + Condition="'$(_DetectedRepositoryRoot)' == '' AND (Exists('$(_RepositoryProbe3)/.git/HEAD') OR Exists('$(_RepositoryProbe3)/.git'))" + >$(_RepositoryProbe3) + <_DetectedRepositoryRoot + Condition="'$(_DetectedRepositoryRoot)' == '' AND (Exists('$(_RepositoryProbe4)/.git/HEAD') OR Exists('$(_RepositoryProbe4)/.git'))" + >$(_RepositoryProbe4) + <_EffectiveRepositoryDirectoryBuildRspFilePath>$(RepositoryDirectoryBuildRspFilePath) + <_EffectiveRepositoryDirectoryBuildRspFilePath + Condition="'$(_EffectiveRepositoryDirectoryBuildRspFilePath)' == '' AND '$(_DetectedRepositoryRoot)' != ''" + >$([System.IO.Path]::Combine('$(_DetectedRepositoryRoot)', 'Directory.Build.rsp')) + <_EffectiveRepositoryDirectoryBuildRspFilePath + Condition="'$(_EffectiveRepositoryDirectoryBuildRspFilePath)' == '' AND '$(_DirectoryBuildPropsDirectory)' != ''" + >$([System.IO.Path]::Combine('$(_DirectoryBuildPropsDirectory)', 'Directory.Build.rsp')) + <_PurviewRspEnabled Condition="'$(PurviewMultiThreadedBuild)' != 'false'">true + <_PurviewRspEnabled Condition="'$(_PurviewRspEnabled)' == ''">false + + + $(_EffectiveRepositoryDirectoryBuildRspFilePath) + + + <_RepositoryDirectoryBuildRspLines Include="$(PurviewRepositoryRspVersionMarker)" /> + <_RepositoryDirectoryBuildRspLines Include="# Managed by Purview.BuildSdk. Delete the marker line to take ownership of this file." /> + <_RepositoryDirectoryBuildRspLines Condition="'$(_PurviewRspEnabled)' == 'true'" Include="-mt" /> + + <_RepositoryDirectoryBuildRspLines Condition="'$(_PurviewRspEnabled)' == 'true'" Include="-nr:false" /> + <_RepositoryDirectoryBuildRspLines Condition="'$(_PurviewRspEnabled)' != 'true'" Include="-mt:false" /> + + + <_PurviewRspDestinationExists Condition="'$(_EffectiveRepositoryDirectoryBuildRspFilePath)' != ''" + >$([System.IO.File]::Exists('$(_EffectiveRepositoryDirectoryBuildRspFilePath)')) + <_PurviewRspDestinationText Condition="'$(_PurviewRspDestinationExists)' == 'true'" + >$([System.IO.File]::ReadAllText('$(_EffectiveRepositoryDirectoryBuildRspFilePath)')) + <_PurviewRspDestinationFirstLine Condition="'$(_PurviewRspDestinationExists)' == 'true'" + >$([System.Text.RegularExpressions.Regex]::Match($(_PurviewRspDestinationText), `^[^\r\n]*`).Value) + <_PurviewRspIsManaged + Condition="'$(_PurviewRspDestinationExists)' == 'true' AND $(_PurviewRspDestinationFirstLine.StartsWith('$(PurviewRepositoryRspVersionMarker)'))" + >true + <_PurviewRspHasEnabledSwitch + Condition="'$(_PurviewRspDestinationExists)' == 'true' AND $([System.Text.RegularExpressions.Regex]::IsMatch($(_PurviewRspDestinationText), `(?m)^\s*-mt\s*$`))" + >true + <_PurviewRspHasNoNodeReuse + Condition="'$(_PurviewRspDestinationExists)' == 'true' AND $([System.Text.RegularExpressions.Regex]::IsMatch($(_PurviewRspDestinationText), `(?m)^\s*-nr\s*:\s*false\s*$`))" + >true + <_PurviewRspHasDisabledSwitch + Condition="'$(_PurviewRspDestinationExists)' == 'true' AND $([System.Text.RegularExpressions.Regex]::IsMatch($(_PurviewRspDestinationText), `(?m)^\s*-mt\s*:\s*false\s*$`))" + >true + <_PurviewRspContentMatches + Condition="'$(_PurviewRspEnabled)' == 'true' AND '$(_PurviewRspHasEnabledSwitch)' == 'true' AND '$(_PurviewRspHasNoNodeReuse)' == 'true'" + >true + <_PurviewRspContentMatches + Condition="'$(_PurviewRspEnabled)' != 'true' AND '$(_PurviewRspHasDisabledSwitch)' == 'true'" + >true + + <_PurviewRspNeedsSync + Condition="'$(_PurviewRspDestinationExists)' != 'true' AND '$(_PurviewRspEnabled)' == 'true'" + >true + <_PurviewRspNeedsSync + Condition="'$(_PurviewRspDestinationExists)' == 'true' AND '$(_PurviewRspIsManaged)' == 'true' AND '$(_PurviewRspContentMatches)' != 'true'" + >true + <_PurviewRspNeedsSync + Condition="'$(PurviewRepoBootstrapMode)' == 'Always' OR '$(PurviewRepoBootstrapMode)' == 'WarnOnDrift'" + >true + <_PurviewRspOverwrite + Condition="'$(_PurviewRepoBootstrapOverwriteExisting)' == 'true' OR '$(_PurviewRspIsManaged)' == 'true'" + >true + <_PurviewRspOverwrite Condition="'$(_PurviewRspOverwrite)' == ''">false + + + + $(MSBuildProjectName) $(IsBlazorWebAssemblyProject) $(PurviewBrowserSafe) + + $(IsServerSharedProject) + $(IsClientSideProject) diff --git a/src/src/BuildSdk/Sdk/Targets/Modern.targets b/src/src/BuildSdk/Sdk/Targets/Modern.targets new file mode 100644 index 0000000..3a155db --- /dev/null +++ b/src/src/BuildSdk/Sdk/Targets/Modern.targets @@ -0,0 +1,95 @@ + + + + + <_PurviewTargetFrameworkMajor + Condition="$([System.Text.RegularExpressions.Regex]::IsMatch('$(TargetFramework)', '^net\d+\.'))" + >$([System.Text.RegularExpressions.Regex]::Match('$(TargetFramework)', '^net(?<major>\d+)\.').Groups['major'].Value) + <_PurviewTargetsNet11OrLater + Condition="'$(_PurviewTargetFrameworkMajor)' != '' AND $([System.Int32]::Parse('$(_PurviewTargetFrameworkMajor)')) >= 11" + >true + + + + true + 2 + <_PurviewMemorySafetyRulesNormalized>$([System.String]::Copy('$(MemorySafetyRules)').Trim().ToLowerInvariant()) + + <_PurviewMemorySafetyRulesNormalized + Condition="'$(_PurviewMemorySafetyRulesNormalized)' == 'off' OR '$(_PurviewMemorySafetyRulesNormalized)' == 'none' OR '$(_PurviewMemorySafetyRulesNormalized)' == 'false'" + >0 + <_PurviewRuntimeAsyncEnabled Condition="'$(PurviewRuntimeAsync)' != 'false' AND '$(UseRuntimeAsync)' != 'false'" + >true + + + + + <_PurviewFeatureSwitches>$(Features) + <_PurviewFeatureSwitches + Condition="'$(_PurviewRuntimeAsyncEnabled)' == 'true' AND !$([System.String]::Copy(';$(Features);').Contains(';runtime-async=on;'))" + >$(Features);runtime-async=on + <_PurviewFeatureSwitches + Condition="'$(_PurviewMemorySafetyRulesNormalized)' == '2' AND !$([System.String]::Copy(';$(_PurviewFeatureSwitches);').Contains(';updated-memory-safety-rules;'))" + >$(_PurviewFeatureSwitches);updated-memory-safety-rules + $([System.String]::Copy('$(_PurviewFeatureSwitches)').Trim(';')) + + + + + preview + + + + + + diff --git a/src/src/BuildSdk/Sdk/Targets/ServerShared.targets b/src/src/BuildSdk/Sdk/Targets/ServerShared.targets new file mode 100644 index 0000000..0805d45 --- /dev/null +++ b/src/src/BuildSdk/Sdk/Targets/ServerShared.targets @@ -0,0 +1,146 @@ + + + + <_PurviewClientSideProjectName>$([System.String]::Copy('$(MSBuildProjectName)').ToLowerInvariant()) + true + true + true + true + false + + + + + + + + + + + + + + <_PurviewServerSharedCandidate + Include="@(_PurviewBrowserProjectReference->'%(FullPath)')" + Condition="'%(_PurviewBrowserProjectReference.PurviewBrowserSafe)' != 'true'" + /> + + + + + + <_PurviewServerSharedReference Include="@(_PurviewServerSharedReferenceInfo->WithMetadataValue('IsServerSharedProject', 'true'))" /> + + + <_PurviewServerSharedReferenceCode Condition="'$(IsClientSideProject)' == 'true'" + >PRSHR0001 + <_PurviewServerSharedReferenceCode Condition="'$(IsClientSideProject)' != 'true'" + >PRSHR0002 + <_PurviewServerSharedReferenceRole Condition="'$(IsClientSideProject)' == 'true'" + >a client-side project, so its assemblies are downloaded to a browser + <_PurviewServerSharedReferenceRole Condition="'$(IsClientSideProject)' != 'true'" + >a Shared project, so every project that references it inherits the reference + <_PurviewServerSharedReferenceText>ServerShared code belongs on the server only. Remove the reference, move the code into Shared if it is browser-safe, or set PurviewBrowserSafe="true" on the ProjectReference to record that it was judged safe. Set IsClientSideProject=false on this project if the detection is wrong, or suppress with DisablePurviewServerSharedReferenceCheck=true. + + + + + diff --git a/src/src/CodeFixers/ExtensionsNamespace/SplitExtensionsClassCodeRefactoringProvider.cs b/src/src/CodeFixers/ExtensionsNamespace/SplitExtensionsClassCodeRefactoringProvider.cs index 8a16e2d..35c318c 100644 --- a/src/src/CodeFixers/ExtensionsNamespace/SplitExtensionsClassCodeRefactoringProvider.cs +++ b/src/src/CodeFixers/ExtensionsNamespace/SplitExtensionsClassCodeRefactoringProvider.cs @@ -541,10 +541,8 @@ CancellationToken cancellationToken continue; } - var renamedRoot = root.ReplaceNodes( - root.DescendantNodes().OfType(), - (node, _) => - node.Identifier.Text == oldName ? node.WithIdentifier(SyntaxFactory.Identifier(newName)) : node + var renamedRoot = root.ReplaceNodes(root.DescendantNodes().OfType(), (node, _) => + node.Identifier.Text == oldName ? node.WithIdentifier(SyntaxFactory.Identifier(newName)) : node ); if (!renamedRoot.IsEquivalentTo(root, topLevel: false)) { diff --git a/src/src/SkillsTesting/SkillCatalogue.cs b/src/src/SkillsTesting/SkillCatalogue.cs index 80d9c83..a1093d4 100644 --- a/src/src/SkillsTesting/SkillCatalogue.cs +++ b/src/src/SkillsTesting/SkillCatalogue.cs @@ -46,8 +46,7 @@ public sealed class SkillCatalogue /// No repository root could be resolved. public static SkillCatalogue ForRepository(string? repositoryRoot = null, string folderName = DefaultFolderName) { - var root = - repositoryRoot + var root = repositoryRoot ?? SkillRepositoryRoot.Find() ?? throw new InvalidOperationException( $"Could not resolve a repository root above '{AppContext.BaseDirectory}'. Expected one of " diff --git a/src/src/SkillsTesting/SkillManifestReader.cs b/src/src/SkillsTesting/SkillManifestReader.cs index d189300..b8026eb 100644 --- a/src/src/SkillsTesting/SkillManifestReader.cs +++ b/src/src/SkillsTesting/SkillManifestReader.cs @@ -91,8 +91,8 @@ public static SkillManifest Parse(string folderName, string skillDirectory, stri List errors = []; List unknown = []; - Dictionary fields = new(StringComparer.Ordinal); - Dictionary metadata = new(StringComparer.Ordinal); + Dictionary fields = [with(StringComparer.Ordinal)]; + Dictionary metadata = [with(StringComparer.Ordinal)]; for (var index = 1; index < closing; index++) { diff --git a/src/src/SkillsTesting/SkillRepositoryRoot.cs b/src/src/SkillsTesting/SkillRepositoryRoot.cs index 1600c57..f4b6fd9 100644 --- a/src/src/SkillsTesting/SkillRepositoryRoot.cs +++ b/src/src/SkillsTesting/SkillRepositoryRoot.cs @@ -119,7 +119,7 @@ public static (string? Root, string? Marker) FindWithMarker(string startDirector /// The candidate directories, nearest first. static List Ancestors(string startDirectory) { - HashSet excluded = new(StringComparer.OrdinalIgnoreCase); + HashSet excluded = [with(StringComparer.OrdinalIgnoreCase)]; try { var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); diff --git a/src/tests/Analyzers.IntegrationTests/Unions/UnionEqualitySuppressorTests.cs b/src/tests/Analyzers.IntegrationTests/Unions/UnionEqualitySuppressorTests.cs index 1c05683..fda6322 100644 --- a/src/tests/Analyzers.IntegrationTests/Unions/UnionEqualitySuppressorTests.cs +++ b/src/tests/Analyzers.IntegrationTests/Unions/UnionEqualitySuppressorTests.cs @@ -18,9 +18,10 @@ namespace Purview.BuildSdk.Analyzers.Unions; /// by it at all. /// /// -/// Every source here declares UnionAttribute and IUnion itself. Unions are a preview language -/// feature whose required runtime types are not in the net10.0 framework this test host references, and -/// without IUnion the union declaration fails to bind with CS0518 and no symbol is produced. +/// Every source here declares UnionAttribute and IUnion itself. The runtime types ship with +/// net11.0 but not net10.0, and this suite runs on both; without IUnion the union +/// declaration fails to bind with CS0518 and no symbol is produced. The compiler embeds its own copies +/// in exactly the same way. /// /// [Category("Integration")] diff --git a/src/tests/BuildSdk.IntegrationTests/CompilerVisiblePropertyTests.cs b/src/tests/BuildSdk.IntegrationTests/CompilerVisiblePropertyTests.cs index 7f306b6..cea99ce 100644 --- a/src/tests/BuildSdk.IntegrationTests/CompilerVisiblePropertyTests.cs +++ b/src/tests/BuildSdk.IntegrationTests/CompilerVisiblePropertyTests.cs @@ -62,6 +62,17 @@ public async Task CompilerVisibleProperties_Include_AllSdkProperties(Cancellatio "DisablePurviewBlazorWebAssemblyDefaults", "PurviewAutoBlazorClientReference", "PurviewBffExposesApi", + "IsSharedProject", + "IsServerSharedProject", + "IsClientSideProject", + "PurviewAutoServerSharedReference", + "DisablePurviewServerSharedReferenceCheck", + "PurviewServerSharedReferenceCheckAsError", + "MemorySafetyRules", + "PurviewRuntimeAsync", + "PurviewMultiThreadedBuild", + "RepositoryDirectoryBuildRspFilePath", + "BootstrapMultiThreadedBuildRspToRepoRoot", "EditorConfigFilePath", "CurrentYear", "AutoGeneratedAssemblyInfoFile", diff --git a/src/tests/BuildSdk.IntegrationTests/CoreDefaultsTests.cs b/src/tests/BuildSdk.IntegrationTests/CoreDefaultsTests.cs index 29b258f..8ccfb2f 100644 --- a/src/tests/BuildSdk.IntegrationTests/CoreDefaultsTests.cs +++ b/src/tests/BuildSdk.IntegrationTests/CoreDefaultsTests.cs @@ -40,8 +40,8 @@ await Assert .Because("ImplicitUsings should default to enable"); await Assert .That(eval.Properties["LangVersion"]) - .IsEqualTo("preview") - .Because("LangVersion should default to preview"); + .IsEqualTo("latest") + .Because("LangVersion should default to the latest shipped C# version (C# 15)"); await Assert .That(eval.Properties["Deterministic"]) .IsEqualTo("true") @@ -108,7 +108,6 @@ CancellationToken cancellationToken extraProps: "false", cancellationToken: cancellationToken ); - // The SDK sets LangVersion to "preview" unless explicitly overridden. await Assert .That(await h.GetPropertyAsync("WarnOnPackingNonPackableProject", cancellationToken)) .IsEqualTo("false"); diff --git a/src/tests/BuildSdk.IntegrationTests/Dotnet11FeaturesTests.cs b/src/tests/BuildSdk.IntegrationTests/Dotnet11FeaturesTests.cs new file mode 100644 index 0000000..1811df3 --- /dev/null +++ b/src/tests/BuildSdk.IntegrationTests/Dotnet11FeaturesTests.cs @@ -0,0 +1,183 @@ +using Purview.BuildSdk.Harness; + +namespace Purview.BuildSdk; + +/// +/// Pins the .NET 11 compiler feature switches the SDK applies to a net11.0 (or later) target: +/// Runtime Async and the C# 15 updated memory-safety rules. +/// +/// +/// The default target set is net10.0;net11.0, so the switches have to be per-TFM: applying +/// them in Sdk.props would enable them for the net10.0 leg as well, where +/// runtime-async still needs EnablePreviewFeatures. AllowUnsafeBlocks is deliberately +/// part of the contract: the updated model is most useful with unsafe code still disallowed. +/// +sealed class Dotnet11FeaturesTests +{ + [Test] + public async Task Net11Target_EnablesRuntimeAsyncAndUpdatedMemorySafetyRules(CancellationToken cancellationToken) + { + using var h = await ProjectHarness.CreateAsync( + "MyLibrary", + targetFramework: "net11.0", + cancellationToken: cancellationToken + ); + + var eval = await h.EvaluateAsync( + ["Features", "MemorySafetyRules", "AllowUnsafeBlocks", "LangVersion"], + cancellationToken: cancellationToken + ); + + await Assert + .That(eval.Properties["Features"]) + .Contains("runtime-async=on") + .Because("Runtime Async V2 is the .NET 11 default and no longer needs EnablePreviewFeatures"); + await Assert + .That(eval.Properties["Features"]) + .Contains("updated-memory-safety-rules") + .Because("MemorySafetyRules defaults to tier 2"); + await Assert.That(eval.Properties["MemorySafetyRules"]).IsEqualTo("2"); + await Assert + .That(eval.Properties["AllowUnsafeBlocks"]) + .IsEqualTo("false") + .Because("the updated rules must not quietly turn unsafe code on"); + await Assert + .That(eval.Properties["LangVersion"]) + .IsEqualTo("latest") + .Because("tier 2 is a feature switch and does not require the preview language version"); + } + + [Test] + public async Task Net10Target_DoesNotReceiveTheNet11Switches(CancellationToken cancellationToken) + { + using var h = await ProjectHarness.CreateAsync( + "MyLibrary", + targetFramework: "net10.0", + cancellationToken: cancellationToken + ); + + var features = await h.GetPropertyAsync("Features", cancellationToken); + + await Assert.That(features).DoesNotContain("runtime-async=on"); + await Assert.That(features).DoesNotContain("updated-memory-safety-rules"); + } + + [Test] + public async Task Net11Target_KeepsFeaturesTheProjectAlreadySet(CancellationToken cancellationToken) + { + using var h = await ProjectHarness.CreateAsync( + "MyLibrary", + targetFramework: "net11.0", + extraProps: "$(Features);project-feature=on", + cancellationToken: cancellationToken + ); + + var features = await h.GetPropertyAsync("Features", cancellationToken); + + await Assert.That(features).Contains("project-feature=on"); + await Assert.That(features).Contains("runtime-async=on"); + await Assert.That(features).Contains("updated-memory-safety-rules"); + } + + [Test] + public async Task RuntimeAsync_CanBeDisabledPerProject(CancellationToken cancellationToken) + { + using var h = await ProjectHarness.CreateAsync( + "MyLibrary", + targetFramework: "net11.0", + extraProps: "false", + cancellationToken: cancellationToken + ); + + var features = await h.GetPropertyAsync("Features", cancellationToken); + + await Assert.That(features).DoesNotContain("runtime-async=on"); + await Assert.That(features).Contains("updated-memory-safety-rules"); + } + + /// + /// The documented opt-out is UseRuntimeAsync=false; honouring it even though the SDK's own + /// switch is separate is what keeps a project compatible with a future .NET SDK that owns the name. + /// + [Test] + public async Task RuntimeAsync_HonoursTheDocumentedUseRuntimeAsyncOptOut(CancellationToken cancellationToken) + { + using var h = await ProjectHarness.CreateAsync( + "MyLibrary", + targetFramework: "net11.0", + extraProps: "false", + cancellationToken: cancellationToken + ); + + await Assert.That(await h.GetPropertyAsync("Features", cancellationToken)).DoesNotContain("runtime-async=on"); + } + + [Test] + public async Task MemorySafetyTierZero_DisablesTheUpdatedRules(CancellationToken cancellationToken) + { + using var h = await ProjectHarness.CreateAsync( + "MyLibrary", + targetFramework: "net11.0", + extraProps: "0", + cancellationToken: cancellationToken + ); + + var features = await h.GetPropertyAsync("Features", cancellationToken); + + await Assert.That(features).DoesNotContain("updated-memory-safety-rules"); + await Assert.That(features).Contains("runtime-async=on"); + } + + /// + /// Tier 1 is the preview syntax, which is a language-version change rather than a feature switch. + /// + [Test] + public async Task MemorySafetyTierOne_EnablesPreviewSyntax(CancellationToken cancellationToken) + { + using var h = await ProjectHarness.CreateAsync( + "MyLibrary", + targetFramework: "net11.0", + extraProps: "1", + cancellationToken: cancellationToken + ); + + var eval = await h.EvaluateAsync(["Features", "LangVersion"], cancellationToken: cancellationToken); + + await Assert.That(eval.Properties["LangVersion"]).IsEqualTo("preview"); + await Assert.That(eval.Properties["Features"]).DoesNotContain("updated-memory-safety-rules"); + } + + [Test] + public async Task MemorySafetyTierOne_KeepsAnExplicitLanguageVersion(CancellationToken cancellationToken) + { + using var h = await ProjectHarness.CreateAsync( + "MyLibrary", + targetFramework: "net11.0", + extraProps: "115.0", + cancellationToken: cancellationToken + ); + + await Assert.That(await h.GetPropertyAsync("LangVersion", cancellationToken)).IsEqualTo("15.0"); + } + + [Test] + public async Task UnrecognisedMemorySafetyTier_FailsTheBuild(CancellationToken cancellationToken) + { + using var h = await ProjectHarness.CreateAsync( + "MyLibrary", + targetFramework: "net11.0", + extraProps: "3", + cancellationToken: cancellationToken + ); + + // The validation target, rather than a full build: the failure under test is evaluation-time and + // a full build would also need the harness's telemetry package references restored. + var (code, output, errors) = await h.RunMSBuildAsync( + "-t:ValidatePurviewModernFeatureSettings", + cancellationToken + ); + + await Assert.That(code).IsNotZero(); + await Assert.That(output + errors).Contains("PRCFG0001"); + } +} diff --git a/src/tests/BuildSdk.IntegrationTests/Harness/ProjectHarness.cs b/src/tests/BuildSdk.IntegrationTests/Harness/ProjectHarness.cs index ccf4a3f..a6d6a37 100644 --- a/src/tests/BuildSdk.IntegrationTests/Harness/ProjectHarness.cs +++ b/src/tests/BuildSdk.IntegrationTests/Harness/ProjectHarness.cs @@ -186,6 +186,10 @@ await File.WriteAllTextAsync( {namespacePrefix} + + false {preImportBlock} @@ -240,8 +244,7 @@ async Task CreateOrUpdateSolution(string solutionPath, string projectName, Cance if (File.Exists(solutionPath)) { var document = XDocument.Load(solutionPath); - var solution = - document.Root + var solution = document.Root ?? throw new InvalidOperationException($"The solution file '{solutionPath}' has no root element."); var alreadyExists = solution diff --git a/src/tests/BuildSdk.IntegrationTests/IncrementalBuildTests.cs b/src/tests/BuildSdk.IntegrationTests/IncrementalBuildTests.cs index 2e87512..9a39b3b 100644 --- a/src/tests/BuildSdk.IntegrationTests/IncrementalBuildTests.cs +++ b/src/tests/BuildSdk.IntegrationTests/IncrementalBuildTests.cs @@ -51,8 +51,11 @@ static async Task AssertGeneratedFileIsStable(string fileName, CancellationToken cancellationToken: cancellationToken ); - var first = await harness.BuildAsync(restore: true, cancellationToken: cancellationToken); - await Assert.That(first.Success).IsTrue().Because(TestHelpers.GenerateError(first.Output, first.Errors)); + var (successFirst, outputFirst, errorsFirst) = await harness.BuildAsync( + restore: true, + cancellationToken: cancellationToken + ); + await Assert.That(successFirst).IsTrue().Because(TestHelpers.GenerateError(outputFirst, errorsFirst)); var generatedPath = FindGeneratedFile(harness, fileName); var generatedContent = await File.ReadAllTextAsync(generatedPath, cancellationToken); @@ -63,8 +66,10 @@ static async Task AssertGeneratedFileIsStable(string fileName, CancellationToken // against, so the delay cannot be removed. await Task.Delay(TimeSpan.FromMilliseconds(50), cancellationToken); - var second = await harness.BuildAsync(cancellationToken: cancellationToken); - await Assert.That(second.Success).IsTrue().Because(TestHelpers.GenerateError(second.Output, second.Errors)); + var (successSecond, outputSecond, errorsSecond) = await harness.BuildAsync( + cancellationToken: cancellationToken + ); + await Assert.That(successSecond).IsTrue().Because(TestHelpers.GenerateError(outputSecond, errorsSecond)); await Assert .That(File.GetLastWriteTimeUtc(generatedPath)) diff --git a/src/tests/BuildSdk.IntegrationTests/InternalsVisibleToTests.cs b/src/tests/BuildSdk.IntegrationTests/InternalsVisibleToTests.cs index 626886f..eb9c653 100644 --- a/src/tests/BuildSdk.IntegrationTests/InternalsVisibleToTests.cs +++ b/src/tests/BuildSdk.IntegrationTests/InternalsVisibleToTests.cs @@ -105,7 +105,7 @@ public async Task InternalsVisibleTo_NeverGrantsToAnEmptyAssemblyName(Cancellati using var h = await CreateBuildableLibraryAsync(cancellationToken); var malformed = (await ReadInternalsVisibleToGrantsAsync(h, cancellationToken)) - .Where(static grant => grant.StartsWith('.')) + .Where(static grant => grant.Length > 0 && grant[0] == '.') .ToArray(); // TargetProjectName has no default, so an unguarded '$(TargetProjectName).%(TestType)Tests' @@ -172,9 +172,11 @@ CancellationToken cancellationToken continue; start += marker.Length; - var end = line.IndexOf('"', start); - if (end > start || end == start) - grants.Add(line[start..end]); + // Search the span rather than IndexOf(char, int): CA1307 wants a StringComparison overload + // on net11, which net10 does not have, so the char overload is the one clean on both. + var length = line.AsSpan(start).IndexOf('"'); + if (length >= 0) + grants.Add(line[start..(start + length)]); } return grants; diff --git a/src/tests/BuildSdk.IntegrationTests/PackagedVersionFallbackTests.cs b/src/tests/BuildSdk.IntegrationTests/PackagedVersionFallbackTests.cs index ffb09e3..5b9e4ba 100644 --- a/src/tests/BuildSdk.IntegrationTests/PackagedVersionFallbackTests.cs +++ b/src/tests/BuildSdk.IntegrationTests/PackagedVersionFallbackTests.cs @@ -354,7 +354,7 @@ public async Task AdoptionPin_MatchesPackageJson(string relativePath, Cancellati .Select(match => (Line: index + 1, Version: match.Groups["version"].Value)) ) // The property reference in the auto-detection documentation is not a pin. - .Where(pin => !pin.Version.StartsWith('$')) + .Where(pin => pin.Version.Length == 0 || pin.Version[0] != '$') .ToArray(); await Assert.That(pins).IsNotEmpty().Because($"{relativePath} is expected to pin Purview.BuildSdk."); diff --git a/src/tests/BuildSdk.IntegrationTests/RepositoryFileSyncTests.cs b/src/tests/BuildSdk.IntegrationTests/RepositoryFileSyncTests.cs index d408ed2..4030050 100644 --- a/src/tests/BuildSdk.IntegrationTests/RepositoryFileSyncTests.cs +++ b/src/tests/BuildSdk.IntegrationTests/RepositoryFileSyncTests.cs @@ -68,10 +68,14 @@ public async Task EditorConfigBootstrap_ExistingFile_IsNeverOverwritten(Cancella [Test] public async Task DesignTimeBuild_DoesNotBootstrapTheRepositoryFiles(CancellationToken cancellationToken) { - using var h = await CreateHarnessAsync(extraProps: null, cancellationToken: cancellationToken); + using var h = await CreateHarnessAsync( + extraProps: "true", + cancellationToken: cancellationToken + ); var editorConfigPath = Path.Combine(h.SolutionDirectory, ".editorconfig"); var globalJsonPath = Path.Combine(h.SolutionDirectory, "global.json"); + var rspPath = Path.Combine(h.SolutionDirectory, "Directory.Build.rsp"); // A real build first: it bootstraps both files and, just as importantly, restores so the // design-time pass below has an assets file to read. @@ -83,10 +87,12 @@ public async Task DesignTimeBuild_DoesNotBootstrapTheRepositoryFiles(Cancellatio await Assert.That(firstSuccess).IsTrue().Because(TestHelpers.GenerateError(firstOutput, firstErrors)); await Assert.That(File.Exists(editorConfigPath)).IsTrue().Because("a real build bootstraps it"); await Assert.That(File.Exists(globalJsonPath)).IsTrue().Because("a real build bootstraps it"); + await Assert.That(File.Exists(rspPath)).IsTrue().Because("a real build bootstraps it"); // Remove them, so the design-time pass has something it could do and visibly does not. File.Delete(editorConfigPath); File.Delete(globalJsonPath); + File.Delete(rspPath); var (code, output, errors) = await h.RunMSBuildAsync( "-t:Build -p:DesignTimeBuild=true -p:SkipCompilerExecution=true", @@ -96,6 +102,7 @@ public async Task DesignTimeBuild_DoesNotBootstrapTheRepositoryFiles(Cancellatio await Assert.That(code).IsZero().Because(output + errors); await Assert.That(File.Exists(editorConfigPath)).IsFalse(); await Assert.That(File.Exists(globalJsonPath)).IsFalse(); + await Assert.That(File.Exists(rspPath)).IsFalse(); } /// @@ -274,7 +281,7 @@ await Assert public async Task RepoBootstrapModeNever_SkipsAllBootstrapWrites(CancellationToken cancellationToken) { using var h = await CreateHarnessAsync( - extraProps: "Never", + extraProps: "Nevertrue", cancellationToken: cancellationToken ); @@ -283,6 +290,75 @@ public async Task RepoBootstrapModeNever_SkipsAllBootstrapWrites(CancellationTok await Assert.That(success).IsTrue().Because(TestHelpers.GenerateError(output, errors)); await Assert.That(File.Exists(Path.Combine(h.SolutionDirectory, "global.json"))).IsFalse(); await Assert.That(File.Exists(Path.Combine(h.SolutionDirectory, ".editorconfig"))).IsFalse(); + await Assert.That(File.Exists(Path.Combine(h.SolutionDirectory, "Directory.Build.rsp"))).IsFalse(); + } + + [Test] + public async Task DirectoryBuildRspBootstrap_CreatesAManagedFileThatEnablesMultithreadedBuild( + CancellationToken cancellationToken + ) + { + using var h = await CreateHarnessAsync( + extraProps: "true", + cancellationToken: cancellationToken + ); + + var (success, output, errors) = await h.BuildAsync(restore: true, verbose: true, cancellationToken); + + await Assert.That(success).IsTrue().Because(TestHelpers.GenerateError(output, errors)); + + var rspPath = Path.Combine(h.SolutionDirectory, "Directory.Build.rsp"); + await Assert.That(File.Exists(rspPath)).IsTrue(); + + var lines = await File.ReadAllLinesAsync(rspPath, cancellationToken); + await Assert.That(lines[0]).StartsWith("# Purview.BuildSdk - managed file"); + await Assert.That(lines).Contains("-mt"); + await Assert + .That(lines) + .Contains("-nr:false") + .Because( + "switching from a non-mt build must not reuse a node with a cached RoslynCodeTaskFactory task (dotnet/msbuild#15213)" + ); + } + + [Test] + public async Task DirectoryBuildRspBootstrap_LeavesAUserOwnedFileAlone(CancellationToken cancellationToken) + { + using var h = await CreateHarnessAsync( + extraProps: "true", + cancellationToken: cancellationToken + ); + + var rspPath = Path.Combine(h.SolutionDirectory, "Directory.Build.rsp"); + const string owned = "# my own switches\n-mt:false\n"; + await File.WriteAllTextAsync(rspPath, owned, cancellationToken); + + var (success, output, errors) = await h.BuildAsync(restore: true, verbose: true, cancellationToken); + + await Assert.That(success).IsTrue().Because(TestHelpers.GenerateError(output, errors)); + await Assert.That(await File.ReadAllTextAsync(rspPath, cancellationToken)).IsEqualTo(owned); + } + + [Test] + public async Task DirectoryBuildRspBootstrap_OptOut_RewritesAManagedFileToDisableMultithreading( + CancellationToken cancellationToken + ) + { + using var h = await CreateHarnessAsync( + extraProps: "truefalse", + cancellationToken: cancellationToken + ); + + var rspPath = Path.Combine(h.SolutionDirectory, "Directory.Build.rsp"); + await File.WriteAllTextAsync(rspPath, "# Purview.BuildSdk - managed file\n-mt\n-nr:false\n", cancellationToken); + + var (success, output, errors) = await h.BuildAsync(restore: true, verbose: true, cancellationToken); + + await Assert.That(success).IsTrue().Because(TestHelpers.GenerateError(output, errors)); + + var lines = await File.ReadAllLinesAsync(rspPath, cancellationToken); + await Assert.That(lines).Contains("-mt:false"); + await Assert.That(lines).DoesNotContain("-mt"); } [Test] diff --git a/src/tests/BuildSdk.IntegrationTests/RepositorySkillsSelfTests.cs b/src/tests/BuildSdk.IntegrationTests/RepositorySkillsSelfTests.cs index d1412f1..a3e7ade 100644 --- a/src/tests/BuildSdk.IntegrationTests/RepositorySkillsSelfTests.cs +++ b/src/tests/BuildSdk.IntegrationTests/RepositorySkillsSelfTests.cs @@ -206,6 +206,7 @@ static async Task TrackedFilesUnderAsync(string repositoryRelativePath if (exitCode != 0) return []; + // Git outputs paths with forward slashes even on Windows, so we can split on '\n' and trim whitespace without worrying about the platform. return [ .. stdOut diff --git a/src/tests/BuildSdk.IntegrationTests/RoslynComponentDefaultsTests.cs b/src/tests/BuildSdk.IntegrationTests/RoslynComponentDefaultsTests.cs index 12ad6eb..c659ed7 100644 --- a/src/tests/BuildSdk.IntegrationTests/RoslynComponentDefaultsTests.cs +++ b/src/tests/BuildSdk.IntegrationTests/RoslynComponentDefaultsTests.cs @@ -132,9 +132,15 @@ public async Task CommentedOutIsRoslynComponent_DoesNotClassifyTheProject(Cancel // Misclassifying a plain library as a Roslyn component forces netstandard2.0 and // TreatWarningsAsErrors onto it. - var properties = await harness.GetPropertiesAsync(cancellationToken, "IsRoslynComponent", "TargetFramework"); + var properties = await harness.GetPropertiesAsync( + cancellationToken, + "IsRoslynComponent", + "TargetFramework", + "TargetFrameworks" + ); await Assert.That(properties["IsRoslynComponent"]).IsEqualTo("false"); - await Assert.That(properties["TargetFramework"]).IsEqualTo("net10.0"); + await Assert.That(properties["TargetFramework"]).IsEmpty(); + await Assert.That(properties["TargetFrameworks"]).IsEqualTo("net10.0;net11.0"); } [Test] diff --git a/src/tests/BuildSdk.IntegrationTests/SdkPackageConsumptionTests.cs b/src/tests/BuildSdk.IntegrationTests/SdkPackageConsumptionTests.cs index 9b84ae7..65a63ac 100644 --- a/src/tests/BuildSdk.IntegrationTests/SdkPackageConsumptionTests.cs +++ b/src/tests/BuildSdk.IntegrationTests/SdkPackageConsumptionTests.cs @@ -261,14 +261,38 @@ await Assert .Because("A shipped analyzer rule (PDS0003) must surface as a warning in the consumer build."); } + /// + /// Both test assemblies target a different framework and Microsoft.Testing.Platform runs them in + /// parallel processes, so on the tests only serializes within one + /// assembly. A cross-process file lock stops the two from packing the same project into the same + /// obj/bin at once, which otherwise fails with CS2012. + /// + static readonly string PackLockPath = Path.Combine(Path.GetTempPath(), "purview-buildsdk-sdk-pack.lock"); + + static async Task AcquirePackLockAsync(CancellationToken cancellationToken) + { + while (true) + { + try + { + return new FileStream(PackLockPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); + } + catch (IOException) + { + // Held by the other target framework's test run; wait for it to finish packing. + await Task.Delay(250, cancellationToken); + } + } + } + static async Task PackSdkAsync(string feedDirectory, CancellationToken cancellationToken) { var sdkProjectPath = Path.GetFullPath(Path.Combine(SdkPaths.SdkDirectory, "..", "BuildSdk.csproj")); - var sdkProjectDirectory = - Path.GetDirectoryName(sdkProjectPath) + var sdkProjectDirectory = Path.GetDirectoryName(sdkProjectPath) ?? throw new InvalidOperationException("Unable to determine SDK project directory."); var packageVersion = $"0.0.0-integration-test-{Guid.NewGuid():N}"; + using var packLock = await AcquirePackLockAsync(cancellationToken); var (code, stdOut, stdErr) = await ProcessRunner.RunAsync( "dotnet", $"pack \"{sdkProjectPath}\" -c Release -o \"{feedDirectory}\" -p:PackageVersion={packageVersion} -p:Version={packageVersion}", diff --git a/src/tests/BuildSdk.IntegrationTests/ServerSharedTests.cs b/src/tests/BuildSdk.IntegrationTests/ServerSharedTests.cs new file mode 100644 index 0000000..dc005b6 --- /dev/null +++ b/src/tests/BuildSdk.IntegrationTests/ServerSharedTests.cs @@ -0,0 +1,370 @@ +using Purview.BuildSdk.Harness; +using Purview.BuildSdk.Infra; + +namespace Purview.BuildSdk; + +/// +/// Pins ServerShared: a Shared-style project that is referenced automatically only by projects +/// that cannot be downloaded to a browser, plus the checks that stop it reaching one anyway. +/// +/// +/// Detection is tested by evaluation and the reference check by invoking its target, matching the +/// browser-safe suite: both ask the same question without restoring WebAssembly packages. +/// +sealed class ServerSharedTests +{ + static Task CreateAsync(string projectName, string content, CancellationToken cancellationToken) => + BlazorFixtures.CreateAsync(projectName, content, cancellationToken: cancellationToken); + + static Task AddSiblingAsync( + ProjectHarness owner, + string projectName, + string content, + CancellationToken cancellationToken + ) => BlazorFixtures.AddSiblingAsync(owner, projectName, content, cancellationToken: cancellationToken); + + static async Task> ReferencesAsync( + ProjectHarness project, + CancellationToken cancellationToken + ) => + (await project.EvaluateAsync(itemTypes: ["ProjectReference"], cancellationToken: cancellationToken)).Items[ + "ProjectReference" + ]; + + static async Task CreateWithServerSharedSiblingAsync( + string projectName, + string projectProperties, + CancellationToken cancellationToken + ) + { + var project = await CreateAsync(projectName, BlazorFixtures.Library(projectProperties), cancellationToken); + + using var serverShared = await AddSiblingAsync( + project, + "ServerShared", + BlazorFixtures.Library(), + cancellationToken + ); + + return project; + } + + [Test] + public async Task ServerProject_AutomaticallyReferencesServerShared(CancellationToken cancellationToken) + { + using var project = await CreateWithServerSharedSiblingAsync("Api", string.Empty, cancellationToken); + + var references = await ReferencesAsync(project, cancellationToken); + + await Assert.That(references.Any(path => path.Contains("ServerShared", StringComparison.Ordinal))).IsTrue(); + await Assert.That(await project.GetPropertyAsync("IsClientSideProject", cancellationToken)).IsEqualTo("false"); + } + + [Test] + public async Task ServerSharedProject_IsDetectedByName(CancellationToken cancellationToken) + { + using var project = await CreateAsync("ServerShared", BlazorFixtures.Library(), cancellationToken); + + await Assert.That(await project.GetPropertyAsync("IsServerSharedProject", cancellationToken)).IsEqualTo("true"); + } + + [Test] + public async Task ServerSharedProject_DoesNotReferenceItself(CancellationToken cancellationToken) + { + using var project = await CreateAsync("ServerShared", BlazorFixtures.Library(), cancellationToken); + + var references = await ReferencesAsync(project, cancellationToken); + + await Assert.That(references).IsEmpty(); + } + + [Test] + public async Task WebAssemblyClient_DoesNotReferenceServerShared(CancellationToken cancellationToken) + { + using var client = await CreateWithServerSharedSiblingAsync("Shop.Client", string.Empty, cancellationToken); + + await Assert.That(await client.GetPropertyAsync("IsClientSideProject", cancellationToken)).IsEqualTo("true"); + await Assert + .That( + (await ReferencesAsync(client, cancellationToken)).Any(path => + path.Contains("ServerShared", StringComparison.Ordinal) + ) + ) + .IsFalse(); + } + + [Test] + public async Task BrowserSafeLibrary_DoesNotReferenceServerShared(CancellationToken cancellationToken) + { + using var project = await CreateWithServerSharedSiblingAsync( + "Contracts", + "true", + cancellationToken + ); + + await Assert.That(await project.GetPropertyAsync("IsClientSideProject", cancellationToken)).IsEqualTo("true"); + await Assert + .That( + (await ReferencesAsync(project, cancellationToken)).Any(path => + path.Contains("ServerShared", StringComparison.Ordinal) + ) + ) + .IsFalse(); + } + + [Test] + public async Task RazorClassLibrary_DoesNotReferenceServerShared(CancellationToken cancellationToken) + { + var rcl = await BlazorFixtures.CreateAsync( + "Widgets", + """ + + + net10.0 + true + + + """, + namespacePrefix: "Acme", + cancellationToken: cancellationToken + ); + + using (rcl) + { + using var serverShared = await AddSiblingAsync( + rcl, + "ServerShared", + BlazorFixtures.Library(), + cancellationToken + ); + + await Assert.That(await rcl.GetPropertyAsync("IsClientSideProject", cancellationToken)).IsEqualTo("true"); + await Assert + .That( + (await ReferencesAsync(rcl, cancellationToken)).Any(path => + path.Contains("ServerShared", StringComparison.Ordinal) + ) + ) + .IsFalse(); + } + } + + /// + /// The conservative Razor-class-library classification is a default, not a verdict: a repository + /// that knows the library is server-only can say so and get the automatic reference back. + /// + [Test] + public async Task ExplicitNotClientSide_RestoresTheAutomaticReference(CancellationToken cancellationToken) + { + var rcl = await BlazorFixtures.CreateAsync( + "Widgets", + """ + + + net10.0 + true + false + + + """, + namespacePrefix: "Acme", + cancellationToken: cancellationToken + ); + + using (rcl) + { + using var serverShared = await AddSiblingAsync( + rcl, + "ServerShared", + BlazorFixtures.Library(), + cancellationToken + ); + + await Assert + .That( + (await ReferencesAsync(rcl, cancellationToken)).Any(path => + path.Contains("ServerShared", StringComparison.Ordinal) + ) + ) + .IsTrue(); + } + } + + /// + /// A Shared project is what a browser-bound project most often pulls in, so it must never inherit a + /// server-only reference automatically. + /// + [Test] + public async Task SharedProject_DoesNotReferenceServerShared(CancellationToken cancellationToken) + { + using var shared = await CreateWithServerSharedSiblingAsync("Shared", string.Empty, cancellationToken); + + await Assert + .That( + (await ReferencesAsync(shared, cancellationToken)).Any(path => + path.Contains("ServerShared", StringComparison.Ordinal) + ) + ) + .IsFalse(); + } + + [Test] + public async Task AutomaticReference_CanBeDisabled(CancellationToken cancellationToken) + { + using var project = await CreateWithServerSharedSiblingAsync( + "Api", + "false", + cancellationToken + ); + + await Assert + .That( + (await ReferencesAsync(project, cancellationToken)).Any(path => + path.Contains("ServerShared", StringComparison.Ordinal) + ) + ) + .IsFalse(); + } + + [Test] + public async Task ClientReferencingServerShared_Warns(CancellationToken cancellationToken) + { + using var project = await CreateAsync( + "Shop.Client", + BlazorFixtures.Library(items: """"""), + cancellationToken + ); + using var serverShared = await AddSiblingAsync( + project, + "ServerShared", + BlazorFixtures.Library(), + cancellationToken + ); + + var (code, output, errors) = await project.RunMSBuildAsync( + "-t:PurviewValidateServerSharedReferences", + cancellationToken + ); + + await Assert.That(code).IsZero().Because(TestHelpers.GenerateError(output, errors)); + await Assert.That(output).Contains("warning PRSHR0001"); + await Assert.That(output).Contains("'ServerShared'"); + } + + [Test] + public async Task SharedReferencingServerShared_Warns(CancellationToken cancellationToken) + { + using var project = await CreateAsync( + "Shared", + BlazorFixtures.Library(items: """"""), + cancellationToken + ); + using var serverShared = await AddSiblingAsync( + project, + "ServerShared", + BlazorFixtures.Library(), + cancellationToken + ); + + var (code, output, errors) = await project.RunMSBuildAsync( + "-t:PurviewValidateServerSharedReferences", + cancellationToken + ); + + await Assert.That(code).IsZero().Because(TestHelpers.GenerateError(output, errors)); + await Assert.That(output).Contains("warning PRSHR0002"); + } + + [Test] + public async Task ServerSharedReferenceCheck_AsError_Fails(CancellationToken cancellationToken) + { + using var project = await CreateAsync( + "Shop.Client", + BlazorFixtures.Library( + "true", + items: """""" + ), + cancellationToken + ); + using var serverShared = await AddSiblingAsync( + project, + "ServerShared", + BlazorFixtures.Library(), + cancellationToken + ); + + var (code, output, errors) = await project.RunMSBuildAsync( + "-t:PurviewValidateServerSharedReferences", + cancellationToken + ); + + await Assert.That(code).IsNotZero(); + await Assert.That(output).Contains("error PRSHR0001"); + } + + /// + /// The documented escape hatch is the existing browser-safe acceptance: a reference that says it was + /// judged safe is not a decision this check gets to second-guess. + /// + [Test] + public async Task ServerSharedReference_DeclaredBrowserSafe_DoesNotWarn(CancellationToken cancellationToken) + { + using var project = await CreateAsync( + "Shop.Client", + BlazorFixtures.Library( + items: """""" + ), + cancellationToken + ); + using var serverShared = await AddSiblingAsync( + project, + "ServerShared", + BlazorFixtures.Library(), + cancellationToken + ); + + var (code, output, errors) = await project.RunMSBuildAsync( + "-t:PurviewValidateServerSharedReferences", + cancellationToken + ); + + await Assert.That(code).IsZero().Because(TestHelpers.GenerateError(output, errors)); + await Assert.That(output).DoesNotContain("PRSHR0001"); + await Assert.That(output).DoesNotContain("PRSHR0002"); + } + + [Test] + public async Task Check_CanBeDisabled(CancellationToken cancellationToken) + { + using var project = await CreateAsync( + "Shop.Client", + BlazorFixtures.Library( + "true", + items: """""" + ), + cancellationToken + ); + using var serverShared = await AddSiblingAsync( + project, + "ServerShared", + BlazorFixtures.Library(), + cancellationToken + ); + + var (code, output, errors) = await project.RunMSBuildAsync( + "-t:PurviewValidateServerSharedReferences", + cancellationToken + ); + + await Assert.That(code).IsZero().Because(TestHelpers.GenerateError(output, errors)); + await Assert.That(output).DoesNotContain("PRSHR0001"); + } + + [Test] + public async Task RootNamespace_StripsTheServerSharedSuffix(CancellationToken cancellationToken) + { + using var project = await CreateAsync("ServerShared", BlazorFixtures.Library(), cancellationToken); + + await Assert.That(await project.GetPropertyAsync("RootNamespace", cancellationToken)).IsEqualTo("Acme"); + } +} diff --git a/src/tests/BuildSdk.IntegrationTests/TargetFrameworkSetTests.cs b/src/tests/BuildSdk.IntegrationTests/TargetFrameworkSetTests.cs index acd4fe5..391466f 100644 --- a/src/tests/BuildSdk.IntegrationTests/TargetFrameworkSetTests.cs +++ b/src/tests/BuildSdk.IntegrationTests/TargetFrameworkSetTests.cs @@ -6,12 +6,12 @@ namespace Purview.BuildSdk; /// /// Verifies the curated PurviewTargetFrameworkSet selection: a repository names a set instead of /// hard-coding a TFM list in every project, so a lifecycle change is one SDK bump rather than an edit in -/// every consuming repository. The feature is opt-in, and every narrower declaration still wins. +/// every consuming repository. Current is always in play, and every narrower declaration still wins. /// sealed class TargetFrameworkSetTests { [Test] - public async Task NoSetSelected_KeepsTheSingleTargetFrameworkDefault(CancellationToken cancellationToken) + public async Task NoSetSelected_UsesTheDefaultMultiTargetSet(CancellationToken cancellationToken) { using var h = await ProjectHarness .For("Library") @@ -25,13 +25,14 @@ public async Task NoSetSelected_KeepsTheSingleTargetFrameworkDefault(Cancellatio ) .BuildAsync(cancellationToken); - // An SDK upgrade must never change a package's targets on its own. + // 'Current' is the default set and now covers both releases still in support. var properties = await h.GetPropertiesAsync(cancellationToken, "TargetFramework", "TargetFrameworks"); - await Assert.That(properties["TargetFramework"]).IsEqualTo("net10.0"); - await Assert.That(properties["TargetFrameworks"]).IsEmpty(); + await Assert.That(properties["TargetFramework"]).IsEmpty(); + await Assert.That(properties["TargetFrameworks"]).IsEqualTo("net10.0;net11.0"); } [Test] + [Arguments("Current", "net10.0;net11.0")] [Arguments("Supported", "net10.0;net11.0")] [Arguments("Broad", "net8.0;net9.0;net10.0")] [Arguments("All", "net8.0;net9.0;net10.0;net11.0")] @@ -59,7 +60,6 @@ CancellationToken cancellationToken } [Test] - [Arguments("Current", "net10.0")] [Arguments("Latest", "net11.0")] public async Task SingleTargetSet_ResolvesToTargetFramework( string set, @@ -375,6 +375,7 @@ public async Task UnrecognisedSet_FailsTheBuild(CancellationToken cancellationTo /// Neither dotnet nor msbuild reproduces it - both only evaluate the outer build. /// [Test] + [Arguments("Current", "net10.0;net11.0", "net10.0")] [Arguments("Supported", "net10.0;net11.0", "net10.0")] [Arguments("All", "net8.0;net9.0;net10.0;net11.0", "net8.0")] public async Task MultiTargetSet_ResolvesIdenticallyUnderAnInnerBuildTargetFramework( diff --git a/templates/global.json b/templates/global.json index 77535b3..739d4fb 100644 --- a/templates/global.json +++ b/templates/global.json @@ -3,6 +3,6 @@ "runner": "Microsoft.Testing.Platform" }, "msbuild-sdks": { - "Purview.BuildSdk": "1.3.2" + "Purview.BuildSdk": "2.0.0" } }