diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 40810e4..bcc71e4 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" ], diff --git a/.editorconfig b/.editorconfig index f8b3050..a9b39cb 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,4 +1,4 @@ -# Purview.BuildSdk 1.3.0 - 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/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0408ca3..ad827fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,9 +15,32 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + # global.json pins the SDK this repository builds with, and every dotnet command resolves + # against it, so installing a different band fails the first command with "A compatible .NET + # SDK was not found". Resolve it here rather than repeating the version in two workflows. + # The fallback only applies if global.json ever stops pinning one. + - name: Resolve the .NET SDK version + id: resolve-sdk + shell: bash + run: | + set -euo pipefail + + pinned="" + if [ -f global.json ]; then + pinned="$(jq -r '.sdk.version // empty' global.json)" + fi + + if [ -n "$pinned" ]; then + echo "global.json pins $pinned; installing that." + echo "dotnet-version=$pinned" >> "$GITHUB_OUTPUT" + else + echo "global.json pins no sdk.version; using the default (10.0.x)." + echo "dotnet-version=10.0.x" >> "$GITHUB_OUTPUT" + fi + - uses: actions/setup-dotnet@0f4a354dd5547a28c70b7853dbd95d5b67886e73 # v6 with: - dotnet-version: "10.0.x" + dotnet-version: ${{ steps.resolve-sdk.outputs.dotnet-version }} - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 @@ -71,3 +94,91 @@ jobs: uses: ./actions/nuget-sources with: mode: remove + + # NativeAOT is the only gate that catches reflection-based System.Text.Json: an AOT build throws on + # the first call instead of degrading, and neither the compiler nor an ordinary test run notices - + # release-explain --format=json passed every test while being dead in an AOT publish. + # `just publish-aot` is the same check locally; keep the two in step. + aot: + name: NativeAOT (${{ matrix.os }}) + strategy: + # One platform failing must not hide the other two: a Windows-only linker or toolchain problem + # is exactly the kind of thing this matrix exists to distinguish. + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + defaults: + run: + # bash everywhere rather than branching on runner.os, so the steps below are identical on all + # three platforms. macOS and Windows runners both ship bash. + shell: bash + env: + # Used by the package source's %GITHUB_TOKEN% credential, so restores can read the feed. + GITHUB_TOKEN: ${{ github.token }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + + - name: Resolve the .NET SDK version + id: resolve-sdk + shell: bash + # See the build job: global.json wins, and the fallback only applies if it pins nothing. + run: | + set -euo pipefail + + pinned="" + if [ -f global.json ]; then + pinned="$(jq -r '.sdk.version // empty' global.json)" + fi + + if [ -n "$pinned" ]; then + echo "global.json pins $pinned; installing that." + echo "dotnet-version=$pinned" >> "$GITHUB_OUTPUT" + else + echo "global.json pins no sdk.version; using the default (10.0.x)." + echo "dotnet-version=10.0.x" >> "$GITHUB_OUTPUT" + fi + + - uses: actions/setup-dotnet@0f4a354dd5547a28c70b7853dbd95d5b67886e73 # v6 + with: + dotnet-version: ${{ steps.resolve-sdk.outputs.dotnet-version }} + + # The MSBuild SDK itself comes from the organisation feed, so this job restores just like the + # build job does. Fork pull requests cannot read that feed and restore from NuGet.org instead. + - name: Configure NuGet sources + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false + uses: ./actions/nuget-sources + with: + namespace: ${{ github.repository_owner }} + token: ${{ secrets.PURVIEW_PACKAGES_READ_TOKEN || github.token }} + source-mapping: ${{ vars.PURVIEW_NUGET_SOURCE_MAPPING }} + + # One framework because publish refuses a multi-targeted project (NETSDK1129), and + # --use-current-runtime so each runner compiles for its own architecture. + - name: Publish with NativeAOT + run: | + set -euo pipefail + + dotnet publish src/src/Build/Build.csproj \ + --configuration Release \ + --output .tools/aot \ + -p:PurviewTargetFrameworkSet=Latest \ + -p:PublishAot=true \ + --use-current-runtime + + # release-explain exits 1 whenever the current ref is ineligible, which is a verdict rather than + # a failure. What is asserted is that the process emitted the JSON contract at all: under AOT a + # reflection-based serializer throws and stdout stays empty. + - name: Smoke-test the native executable + run: | + set -euo pipefail + + .tools/aot/Purview.Build --version + json="$(.tools/aot/Purview.Build release-explain --format=json || true)" + printf '%s' "$json" | jq -e '.verdict' + + - name: Remove temporary NuGet sources + if: always() + uses: ./actions/nuget-sources + with: + mode: remove diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a91f8e7..ed37763 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,9 +35,30 @@ jobs: fetch-depth: 0 fetch-tags: true + # See ci.yml: global.json is the single source of truth for the SDK, and a run that installs + # a different band fails before the pipeline starts. + - name: Resolve the .NET SDK version + id: resolve-sdk + shell: bash + run: | + set -euo pipefail + + pinned="" + if [ -f global.json ]; then + pinned="$(jq -r '.sdk.version // empty' global.json)" + fi + + if [ -n "$pinned" ]; then + echo "global.json pins $pinned; installing that." + echo "dotnet-version=$pinned" >> "$GITHUB_OUTPUT" + else + echo "global.json pins no sdk.version; using the default (10.0.x)." + echo "dotnet-version=10.0.x" >> "$GITHUB_OUTPUT" + fi + - uses: actions/setup-dotnet@0f4a354dd5547a28c70b7853dbd95d5b67886e73 # v6 with: - dotnet-version: "10.0.x" + dotnet-version: ${{ steps.resolve-sdk.outputs.dotnet-version }} - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 diff --git a/AGENTS.md b/AGENTS.md index d7d4ec2..7f552a9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,6 +27,7 @@ This file is the primary instruction set for human and AI agents working in this - `docs/wiki/Release-Flow.md` — versioning, branch models, and release strategy. - `docs/wiki/Release-Models.md` — the three release trigger models, as raw Mermaid source. - `docs/wiki/Local-Development.md` — `just` recipes, `release-explain`, simulation, and the local rehearsal. + - `docs/wiki/Net-11.md` — supported runtimes (and the .NET 8/9 removal) and the .NET 11 feature evaluation. - `docs/wiki/Migration-*.md` — per-consumer migration guides. - `README.md` — user-facing overview and minimal consumer setup. - `.config/purview-build.json` — this repository's own pipeline configuration. @@ -53,7 +54,7 @@ CI builds with `--warnaserror`. ## Code conventions - C# is formatted with **CSharpier** (pinned in `.config/dotnet-tools.json`); `just lint-check` must pass. `.pre-commit` (Lefthook, `.config/lefthook.yml`) runs it. -- Target `net10.0`; the codebase uses C# modern idioms (primary constructors, collection expressions, file-scoped namespaces). +- Target `net10.0` and `net11.0` (the `PurviewTargetFrameworkSet` the SDK defaults to); the codebase uses C# modern idioms (primary constructors, collection expressions, file-scoped namespaces). Shared code must compile against the **oldest** target's API surface — see `docs/wiki/Net-11.md` for why a .NET 11-only API needs a `#if NET11_0_OR_GREATER` guard. - Commits must follow [Conventional Commits](https://www.conventionalcommits.org) (enforced by commitlint via Lefthook). Examples: `fix:`, `feat:`, `build:`, `docs:`, `chore:`. - Do not add code comments unless they explain non-obvious rationale (see existing comments in `Settings/` and `Modules/` for the style). - Do not commit secrets or modify secret-handling settings without review. diff --git a/CHANGELOG.md b/CHANGELOG.md index 11a1456..236356e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,52 @@ that version to nuget.org. This file was reconstructed from the conventional-commit history at 0.6.1, so entries before that release summarise what the commits recorded rather than what was written at the time. +## [Unreleased] + +### Changed + +- **.NET 8 and .NET 9 are no longer supported.** Both reached end of support on 2026-11-10, so + neither is a defensible baseline. Nothing in the repository targets, documents or exemplifies + `net8.0`/`net9.0` any more; the `$(TFM)` examples and the pack-validation tests now use `net11.0`. + The tool itself still targets `net10.0;net11.0`, because a single-target `net11.0` dotnet tool + would be uninstallable on every consumer running the LTS. +- **The `net11.0` leg is held to the .NET 11 analyzer set.** CA1307 now requires + `string.StartsWith(char, StringComparison)` and friends, which do not exist on `net10.0`, while + CA1865 rejects the portable single-character-string workaround. The suppression is scoped to the + newest target in `Build.csproj`, so the rule stays live on the older one. +- **`ci.yml` and `release.yml` resolve the SDK from `global.json`** instead of hardcoding a band, the + way the shared workflows already did. A pinned `global.json` and a hardcoded `dotnet-version` fail + the job at the first `dotnet` command as soon as one of them moves. +- **`PublishLocalNuGetModule` uses the asynchronous DataAnnotations validation** on the `net11.0` + target, honouring the module's cancellation token, with the synchronous call kept on `net10.0`. +- **The tool survives a NativeAOT publish.** `release-explain --format=json` threw under AOT + (`Reflection-based serialization has been disabled`) while the text rendering beside it worked, + because AOT disables reflection-based `System.Text.Json` instead of degrading. `ReleaseExplanation` + and `string[]` now serialise through the source-generated `BuildJsonContext`; the JSON output is + byte-identical and the golden file is unchanged. +- **`LangVersion` is `preview` on the `net11.0` target.** System.Text.Json's source generator emits + `safe extern` accessors, which is C# 15 syntax the compiler only accepts at `preview`. The SDK's + tier-2 memory-safety switch enables the rules but leaves the language version at 15.0, so a project + with a `JsonSerializerContext` failed with CS8652. + +### Fixed + +- **`dotnet restore` failed with NU1010.** `System.Security.Cryptography.Pkcs` kept its + `PackageReference` but lost its `PackageVersion`. Restoring it also stops the vulnerable 8.0.1 + package that `NuGet.Packaging` pulls transitively from being resolved. +- **`just build-local-tool`, and therefore every `pipeline-*` recipe, failed with NETSDK1129.** + `dotnet publish` refuses a multi-targeted project without a single framework. The recipe now pins + `PurviewTargetFrameworkSet=Latest`; the packaged tool still ships both targets. + +### Added + +- `docs/wiki/Net-11.md` — the supported-runtime table, what the SDK already enables, and the .NET 11 + features evaluated per area with the decision and its reason. +- `just publish-aot` — publishes the tool as a NativeAOT executable for the current platform and + smoke-tests `release-explain --format=json`, the path where reflection-based serialisation fails + silently at runtime rather than at build time. CI runs the same check on Windows, Linux and macOS as + a `fail-fast: false` matrix. + ## [0.7.0] - 2026-10-21 ### Changed diff --git a/Directory.Packages.props b/Directory.Packages.props index e935565..1fa6eaf 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,10 +1,8 @@ - true 5.9.0 - 1.78.0 - 10.0.14 - 10.10.0 + 1.83.0 + 11.0.0 3.2.8 7.9.0 @@ -18,7 +16,7 @@ - + diff --git a/Justfile b/Justfile index a08dd70..184575e 100644 --- a/Justfile +++ b/Justfile @@ -24,6 +24,7 @@ pipeline_tool := local_tool golden_test_project := tests_root / "Build.UnitTests" / "Build.UnitTests.csproj" dogfood_artifacts := "./artifacts/dogfood" dogfood_tool_path := "./.tools/dogfood" +aot_dir := "./.tools/aot" current_version := `bun -p "require('./package.json').version"` @@ -42,6 +43,7 @@ ensure-pipeline-tool: build-local-tool: echo "Building {{ BLUE }}{{ tool_project }}{{ NORMAL }} to {{ GREEN }}{{ local_tool_dir }}{{ NORMAL }} (Release)" dotnet publish {{ tool_project }} -c Release -o "{{ local_tool_dir }}" \ + -p:PurviewTargetFrameworkSet=Latest \ -p:Version={{ current_version }} -p:PackageVersion={{ current_version }} # Run the PR pipeline (restore, build, lint, tests) @@ -105,6 +107,21 @@ pipeline-dogfood *args: echo "Running the freshly packed tool against this repository..." "{{ dogfood_tool_path }}/purview-build" {{ args }} +# Publish the tool as a native executable (NativeAOT) for the current platform and smoke-test it +# A published dotnet tool ships IL and picks a runtime at install time; this recipe is the separate +# question of whether the same source survives AOT, where reflection-based serialisation throws +# instead of degrading. It is the gate for the JSON path, which the smoke test exercises. +[group('Build and Test')] +publish-aot: + echo "Publishing {{ BLUE }}{{ tool_project }}{{ NORMAL }} as NativeAOT to {{ GREEN }}{{ aot_dir }}{{ NORMAL }}" + rm -rf "{{ aot_dir }}" + dotnet publish {{ tool_project }} -c Release -o "{{ aot_dir }}" \ + -p:PurviewTargetFrameworkSet=Latest \ + -p:PublishAot=true --use-current-runtime \ + -p:Version={{ current_version }} -p:PackageVersion={{ current_version }} + "{{ aot_dir }}/Purview.Build" --version + "{{ aot_dir }}/Purview.Build" release-explain --format=json | jq -e '.verdict' + # Explain the release decision for the working tree, without running any module or mutating anything [group('Release')] release-explain *args: @@ -234,7 +251,7 @@ lint-fix: [group('Utilities')] scrub: find . -type d \( -name bin -o -name obj \) -exec rm -rf {} + - rm -rf "{{ local_tool_dir }}" "{{ dogfood_tool_path }}" "{{ dogfood_artifacts }}" + rm -rf "{{ local_tool_dir }}" "{{ dogfood_tool_path }}" "{{ dogfood_artifacts }}" "{{ aot_dir }}" just clean just restore --force-evaluate dotnet build-server shutdown diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md index dc95523..f13d190 100644 --- a/docs/wiki/Home.md +++ b/docs/wiki/Home.md @@ -29,6 +29,7 @@ The same implementation is available three ways: - [Local Development](Local-Development.md) - [Secrets and Environment Variables](Secrets-and-Environment-Variables.md) - [Repository CI/CD](Repository-CI-CD.md) +- [.NET 11 support and feature evaluation](Net-11.md) - [Migration: aspire-resourcekit](Migration-aspire-resourcekit.md) ## Pipeline diff --git a/docs/wiki/Local-Development.md b/docs/wiki/Local-Development.md index 9b51f19..3a2e248 100644 --- a/docs/wiki/Local-Development.md +++ b/docs/wiki/Local-Development.md @@ -20,6 +20,19 @@ The output goes to `.tools/local/` (gitignored) rather than `bin/Release/`, beca rebuilds the solution underneath itself and would otherwise overwrite the binary it is executing. `.config/dotnet-tools.json` pins only CSharpier and `dotnet-inspect`. +`build-local-tool` publishes with `-p:PurviewTargetFrameworkSet=Latest`. `dotnet publish` refuses a +multi-targeted project without a single framework (`NETSDK1129`), and the local tool only ever runs on +this machine, so pinning to the newest target is the honest choice; the *packaged* tool keeps both +targets so consumers on the LTS can still install it. + +`just publish-aot` is the separate question of whether the source survives NativeAOT, where +reflection-based serialisation throws instead of degrading. It publishes to `.tools/aot` for the +current platform and then runs `release-explain --format=json` through `jq`, which is the path that +exercises the source-generated serializer. The same check runs in CI on Windows, Linux and macOS +(`fail-fast: false`, so one platform's toolchain problem does not mask the other two). A published +dotnet tool ships IL and cannot itself be AOT, so this is a portability gate, not a second +distribution channel. See [.NET 11](Net-11.md) for the third-party trim warnings that remain. + `just pipeline-dogfood` is the separate, slower check that the *packaged* tool is installable: it packs from source into `artifacts/dogfood`, installs from that folder alone (`--source`, not `--add-source`), and runs the result. CI and the release workflow do the same. @@ -30,6 +43,7 @@ packs from source into `artifacts/dogfood`, installs from that folder alone (`-- | --- | --- | | `just build` | Build `src/Build.slnx` (Debug) | | `just build-local-tool` | Publish the tool from source (Release) to `.tools/local`, which every `pipeline-*` recipe then runs | +| `just publish-aot` | Publish the tool as a NativeAOT executable to `.tools/aot` for the current platform and smoke-test it | | `just test` / `just test-unit` | `just test` runs every suite, the same set CI runs; `just test-unit` narrows to `/*/*/*/*[Category=Unit]` | | `just lint-check` / `just lint-fix` | CSharpier check / format | | `just pack` | `dotnet pack` with the current `package.json` version | diff --git a/docs/wiki/Net-11.md b/docs/wiki/Net-11.md new file mode 100644 index 0000000..5e798eb --- /dev/null +++ b/docs/wiki/Net-11.md @@ -0,0 +1,221 @@ +# .NET 11: supported runtimes and feature evaluation + +.NET 11 reached general availability on **10 November 2026**. It is a Standard Term Support +release, supported from that date until **9 November 2028**. .NET 10 remains the current LTS, supported +until **14 November 2028**. + +This page records which runtimes `Purview.Build` supports, what the pinned `Purview.BuildSdk` already +turns on, and the .NET 11 features that were evaluated but not adopted — with the reason, so the +decision is not re-litigated from scratch. + +## Supported runtimes + +| Target | Release type | Support ends | Status | +| --- | --- | --- | --- | +| `net8.0` | LTS | 10 November 2026 | **Dropped** | +| `net9.0` | STS | 10 November 2026 | **Dropped** | +| `net10.0` | LTS | 14 November 2028 | Supported | +| `net11.0` | STS | 9 November 2028 | Supported | + +.NET 8 and .NET 9 reached end of support on the same day, so neither is a defensible baseline for a new +package. Dropping them here means: + +- nothing in this repository targets, documents or exemplifies `net8.0`/`net9.0`; +- `System.Security.Cryptography.Pkcs` — a direct reference added to override the vulnerable 8.0.1 + package that `NuGet.Packaging` pulls transitively — is pinned to the runtime band + (`$(DotNetRuntimeVersion)`), so no .NET 8-era asset is resolved; +- `netstandard2.0` remains in the examples it appears in, because Roslyn components still target it. + That is a different axis from the .NET version and is not affected by this change. + +The tool itself targets whatever `PurviewTargetFrameworkSet` resolves to, which the SDK defaults to +`Current` → `net10.0;net11.0`. Two consequences follow from that, and they matter more than any single +feature below: + +1. **Shared code must compile against the oldest target's API surface.** A .NET 11-only API needs a + `#if NET11_0_OR_GREATER` guard with a documented fallback, not an unconditional call. See + `PublishLocalNuGetModule` for the pattern. +2. **The published tool must stay installable on .NET 10.** `PackAsTool` ships a + `tools//any/` folder per target, and NuGet selects the best one for the consumer's runtime. A + single `net11.0` tool would remove every consumer running on the LTS, so `Current` is the right set + even though it costs the guard above. + +## What the SDK already enables + +`Purview.BuildSdk` 2.0.0 opts a `net11.0` target into the rest of .NET 11 without any per-repository +configuration: + +| Switch | Effect | +| --- | --- | +| `Features=runtime-async=on` | Runtime Async V2 — the runtime tracks suspension instead of the compiler emitting state machines | +| `Features=updated-memory-safety-rules`, `MemorySafetyRules=2` | The C# 15 memory-safety model | +| `LangVersion` | 15.0 on `net11.0` | +| `AnalysisLevel=latest` | The .NET 11 analyzer set, per target | + +`PurviewRuntimeAsync=false` or `UseRuntimeAsync=false` opts a project out; `MemorySafetyRules` selects +the tier. + +### The language version tier 2 still needs + +Tier 2 turns on `updated-memory-safety-rules` and leaves `LangVersion` at 15.0, on the stated basis that +the rules are independent of the language version. That holds for hand-written code, but not for +generated code: `System.Text.Json`'s source generator emits `safe extern` accessors for init-only +setters, which is C# 15 *syntax* and needs a `preview` language version. A project with a +`JsonSerializerContext` on `net11.0` therefore fails with **CS8652** unless it sets one. + +`Build.csproj` sets `preview` on the newest target only, so the `net10.0` +leg keeps the released language version. If the SDK later couples tier 2 to `preview`, the override can +go. + +## NativeAOT + +A published dotnet tool ships IL and picks a runtime at install time, so the package itself cannot be +AOT-compiled. What *is* achievable — and now verified — is that the same source compiles and runs as a +NativeAOT executable: + +```shell +just publish-aot +``` + +That publishes for the current platform and then runs `release-explain --format=json` through `jq`, +because an AOT build **disables reflection-based `System.Text.Json` rather than degrading**: the first +`JsonSerializer.Serialize` call throws `NotSupportedException`. Before this change the text +rendering worked and `--format=json` failed at runtime, which no compile-time check catches. + +CI runs the same check as the `NativeAOT` job in `ci.yml`, across `ubuntu-latest`, `windows-latest` and +`macos-latest` with `fail-fast: false`. A matrix rather than one runner because the failure modes are +platform-specific: the linker, the C toolchain and the RID all differ, and a Windows-only breakage must +not be reported as "the AOT check is red". Each runner publishes with `--use-current-runtime`, so it +compiles for its own architecture. + +| Change | Why | +| --- | --- | +| `BuildJsonContext` (source-generated metadata) | Serialising `ReleaseExplanation` and deserialising `string[]` now go through a `JsonSerializerContext`. The JSON output is byte-identical — the golden file is unchanged. | +| `JsonSerializerOptions.GetTypeInfo()` on `net11.0` | The .NET 11 generic overload; the `net10.0` leg casts the non-generic result. | +| Suppressions in `ConfigSchemaGenerator` | The only reflection-heavy code in the assembly. It is reached from `just schema` and the test suite only, never from a published binary, so each call site is suppressed with that justification rather than rewritten. | +| Suppressions for DataAnnotations | `ValidationContext` has no trim-safe constructor (it reflects for `DisplayNameAttribute`). No settings type carries that attribute and validation goes through `IValidatableObject`, so the reflection resolves nothing. | + +Four dependencies still report column-level trim and AOT warnings and cannot be fixed here: +`ModularPipelines`, `Vertical.SpectreLogger`, `Octokit`, and +`Initialization.Microsoft.Extensions.DependencyInjection` (`IL2104`/`IL3053`). They are warnings, not +errors, and the smoke test is what proves the paths this tool actually takes. Treat a growing +`IL2104` list as a prompt to re-run `just publish-aot` rather than as noise. + +A full pipeline run under AOT is **not** claimed: ModularPipelines reflects over modules, and the +smoke test covers the CLI surface and the release-explanation path, not module discovery. + +## Per-target analyzer divergence + +The .NET 11 analyzer set is applied to the `net11.0` leg only, and it now suggests APIs that do not +exist on `net10.0`. Two rules are in direct conflict: + +- **CA1307** (net11) asks for `string.StartsWith(char, StringComparison)` and friends, which .NET 11 + added. Those overloads do not exist on `net10.0`. +- **CA1865** (both) forbids the portable workaround — passing a single-character *string* to + `StartsWith`/`EndsWith`/`LastIndexOf`. + +There is no expression that satisfies both while compiling for both targets. Because a `char` +comparison is ordinal by definition, CA1307's suggestion buys clarity of intent and nothing else, so it +is suppressed on the newest target only (`Build.csproj`), keeping the rule live on the older one. The +suppression is conditioned on `$(PurviewTargetFrameworksLatest)` rather than a literal `net11.0`, so it +follows the SDK when `Latest` moves to the next major. + +If the tool ever collapses to a single target, delete the suppression and adopt the overloads. + +## Evaluation by area + +Legend: **Adopted** — in this change. **Candidate** — plausible, not done. **Rejected** — deliberately +not done, with the reason. **N/A** — does not apply to this tool. + +### Runtime + +| Feature | Decision | Notes | +| --- | --- | --- | +| Runtime Async V2 | Adopted (SDK) | On for `net11.0`. Lower overhead in async-heavy code; the runtime libraries are themselves compiled with it, so the whole dependency graph benefits. | +| x86-64-v2 minimum, R2R targeting x86-64-v3 | **Rollout risk** | .NET 11 refuses to start on pre-2013 CPUs ("The current CPU is missing one or more of the baseline instruction sets"). Self-hosted runners and long-lived build agents need auditing before consumers move. Windows Arm64 also moves to an `armv8.2-a + RCPC` R2R baseline. | +| More than 1024 logical processors | N/A | Fixes a startup failure on very large servers; no action. | +| In-process crash reports | Candidate | `DOTNET_EnableCrashReport=1` captures a managed stack trace from inside the dying process. Useful for diagnosing a tool crash on a CI runner where an out-of-process dump is not collected. | + +### Async + +| Feature | Decision | Notes | +| --- | --- | --- | +| Runtime Async V2 | Adopted (SDK) | See above. | +| Continuations skip `ExecutionContext` capture when there is nothing to restore | Adopted (runtime) | Free; benefits `Task`/`ValueTask` paths this tool uses heavily. | +| Async DataAnnotations validation | Adopted | `Validator.TryValidateObjectAsync` in `PublishLocalNuGetModule`, guarded by `#if NET11_0_OR_GREATER` with the synchronous fallback on `net10.0`. It also honours the module's `CancellationToken`. | + +### Performance + +| Feature | Decision | Notes | +| --- | --- | --- | +| NativeAOT `dotnet` CLI host | Adopted (runtime) | `dotnet --version/--info/--help`, `dotnet --help`, `dotnet sln list` and tool invocations run without starting the managed CLI, saving roughly 600–700 ms per invocation. A pipeline that shells out dozens of times gains the most of any single item here. Opt out with `DOTNET_CLI_ENABLEAOT=false`. | +| MSBuild server enabled by default | **Review the shutdown step** | .NET 11 keeps a warm MSBuild worker between invocations. `PublishLocalNuGet:ShutdownDotnetBuilderServer=true` tears that down after a local publish; with .NET 11 that discards a resource the next run would otherwise reuse. Worth measuring before changing, since a stale server during feed replacement was the original reason for it. | +| JIT: devirtualization, bounds-check elimination, constant-folding `SequenceEqual`, exact-type `Activator.CreateInstance` | Adopted (runtime) | Free and unconditional on `net11.0`. | +| `ReadyToRun` for the tool | Rejected | A dotnet tool ships portable IL; `PublishReadyToRun` would pin it to a platform and remove the single-package cross-platform property. | +| NativeAOT for the tool | Adopted as a verified publish — `just publish-aot` | The package still ships IL, but the same source now compiles and runs as a native executable. See [NativeAOT](#nativeaot). | +| `Comparer.Default` specialised in R2R (up to 20×) | N/A | Only relevant to R2R images. | +| Collection expression arguments (`[with(capacity: …), .. items]`) | Candidate | Micro-optimisation for the several list-building sites in the release and schema code. Low value, zero risk. | + +### Testing + +| Feature | Decision | Notes | +| --- | --- | --- | +| `dotnet test --no-dependencies` | **Rejected for now** | Would skip the redundant project-to-project builds `RunTestsModule` currently repeats per test project after `BuildModule` has already built the solution. It is an .NET 11-only CLI option, and the tool must keep working on a consumer's .NET 10 SDK — the shared workflows document `10.0.x` as their fallback. Gate it on a probed SDK version before adopting. | +| `--results-directory-layout per-module` | Candidate | Prevents same-named reports from overwriting each other across multi-targeted test projects. Default is `flat`. | +| `--artifacts-path` in MTP mode | Candidate | Aligns test output with the SDK artifacts layout. | +| Run-level policies: `--timeout`, `--maximum-failed-tests` | Candidate | `--timeout` counts only while a test application is running and returns exit code 3; `--maximum-failed-tests` returns 13. Both are .NET 11-only, so the same SDK-version gate as `--no-dependencies` applies. | +| Two-stage Ctrl+C cancellation | Adopted (runtime) | First press stops scheduling, second force-kills. Behaviour change with no code needed. | +| Live in-flight test display, protocol 1.1.0 output forwarding | Adopted (runtime) | Interactive terminals only; no effect on CI output. | +| `DOTNET_TEST_RUNNER` environment variable | Rejected | `global.json` already pins `test.runner` for this repository, and the environment variable would let an ambient value override it silently. | + +### Build + +| Feature | Decision | Notes | +| --- | --- | --- | +| Fast wildcard matching during evaluation | Adopted (SDK 11.0.100) | 10–13% faster project evaluation and 16–34% less allocation where include/exclude patterns overlap — the common case in SDK-style projects. | +| SDK resolved from `global.json` in CI | Adopted | `ci.yml` and `release.yml` now derive `dotnet-version` from `global.json` instead of hardcoding a band, matching the shared workflows. A hardcoded band and a pinned `global.json` disagree the moment the pin moves, and the job fails at the first `dotnet` command. | +| `dotnet sln` support for solution filters (`.slnf`) | Candidate | Useful for very large consumer repositories; no effect on this one. | +| `Configuration` environment variable feeds `-c` | **Risk noted** | The `dotnet` CLI now uses `Configuration` as the default for the shared `--configuration` option. Every module passes `Configuration` explicitly, so the pipeline is unaffected, but an ambient `Configuration` value in a consumer's CI can now change a bare `dotnet` invocation. | +| `DOTNET_CLI_USE_MSBUILD_SERVER` no longer forced to `0` | Adopted (runtime) | The CLI stops overriding a user-set `MSBUILDUSESERVER`. | + +### Debug + +| Feature | Decision | Notes | +| --- | --- | --- | +| Runtime Async live stack traces | Adopted (SDK) | `new StackTrace()`, profilers and the debugger see the real call chain (5 frames instead of 13) with no state-machine frames. | +| Breakpoints bind inside runtime-async methods | Adopted (SDK) | Stepping over `await` no longer jumps into compiler infrastructure. | +| `Process` API expansion | **Candidate, highest value** | `ReleaseContextProvider.RunGit` redirects stdout *and* stderr and then calls `StandardOutput.ReadToEnd()` followed by `WaitForExit()`. If `git` fills the stderr pipe buffer the call deadlocks, and neither pipe is drained concurrently. `Process.RunAndCaptureText` (or `ReadAllLinesAsync`, which distinguishes stdout from stderr without parsing) removes the whole class of problem, and `ProcessRunAndCaptureTextAsync` accepts a timeout. Requires a `#if NET11_0_OR_GREATER` guard. | +| `ProcessStartInfo.KillOnParentExit`, `TryGetProcessById`, suspended start | Candidate | Relevant to a pipeline that spawns child processes which must not outlive it. | +| `Console` `FORCE_COLOR` support | Adopted (runtime) | Does **not** change wrapping, so the `CLIConsole` width invariant — output must not be reflowed for `jq`/`grep` — is unaffected. `NO_COLOR` still wins. | + +### Packaging + +| Feature | Decision | Notes | +| --- | --- | --- | +| NETSDK1235: `PackAsTool` with a custom `NuspecFile` | **Unaffected** | This package rewrites the `.nupkg` after packing with an inline MSBuild task; it does not set `NuspecFile`. The new warning correctly does not fire. | +| `ZipArchive` CRC32 validation on read | Adopted (runtime) | The pack task rewrites the package zip; a corrupt or truncated archive now throws `InvalidDataException` instead of being silently accepted. | +| `ZipArchiveEntry.Open(FileAccess)` / async overloads | Candidate | Would let the PDB-stripping task stream entries with explicit access instead of re-creating every entry. | +| `dotnet nuget add/update source --min-publish-age-hours` | Candidate | Supply-chain hardening for floating references. This repository pins exact versions, so it is only worth adopting as a repository-wide policy. | +| Reproducible container publishing (`SOURCE_DATE_EPOCH`) | N/A | No containers are published. | +| Multi-arch container builds with Podman | N/A | As above. | + +### Language features (C# 15) + +| Feature | Decision | Notes | +| --- | --- | --- | +| Union types | Rejected for now | Tempting for the eligibility verdict shape, but the verdict is part of the `release-explain` JSON contract and the golden file. A shape change needs its own decision, not a language upgrade. | +| Closed hierarchies (+ inferred `System.Text.Json` polymorphism) | Rejected for now | Changes the serialized contract in the same way. | +| Labelled `break`/`continue` | Candidate | `EligibilityPolicies.ApplyRules` and the test-project filters both use flag-and-`goto`-style control flow that this would simplify. | +| Extension indexers, static interface members on older runtimes | N/A | No site in this codebase would get clearer. | +| Memory safety preview | Adopted (SDK) | `updated-memory-safety-rules` at tier 2, via the SDK. | + +## Adopting anything new: the checklist + +1. Does the API exist on `net10.0`? If not, guard it with `#if NET11_0_OR_GREATER` and a documented + fallback — or reject it until the tool is single-target. +2. Is it a `dotnet` CLI option? Then it is a function of the **consumer's** SDK, not the tool's own + build. Probe or gate it; never assume .NET 11. +3. Run `just release-matrix` and `just config-matrix` as well as `just test` — several of the + candidates above (process capture, test reporting) touch release eligibility. +4. Run `just publish-aot` if the change adds a serialisation path or any reflection. An AOT build + throws on reflection-based `System.Text.Json` instead of degrading, and nothing in the normal + build tells you. diff --git a/docs/wiki/Pack-Validation.md b/docs/wiki/Pack-Validation.md index 984f4cf..7cc6ecc 100644 --- a/docs/wiki/Pack-Validation.md +++ b/docs/wiki/Pack-Validation.md @@ -25,7 +25,7 @@ Both maps are keyed by package-id glob (case-insensitive; `"*"` matches every pa ### Target-framework partials (`$(TFM)`) -An entry may contain the literal token `$(TFM)`, e.g. `lib/$(TFM)/Purview.Telemetry.dll`. The token is expanded into one entry per target framework the package actually ships (short folder name, e.g. `net8.0`, `net48`, `netstandard2.0`, discovered via the package's own `lib`/`ref`/`build`/`tools`/`frameworkAssemblies` groups), so the rule must be satisfied independently for every one of the package's target frameworks. If the package has no detectable target frameworks, a `$(TFM)` entry is reported as an error rather than silently skipped. +An entry may contain the literal token `$(TFM)`, e.g. `lib/$(TFM)/Purview.Telemetry.dll`. The token is expanded into one entry per target framework the package actually ships (short folder name, e.g. `net11.0`, `net48`, `netstandard2.0`, discovered via the package's own `lib`/`ref`/`build`/`tools`/`frameworkAssemblies` groups), so the rule must be satisfied independently for every one of the package's target frameworks. If the package has no detectable target frameworks, a `$(TFM)` entry is reported as an error rather than silently skipped. NuGet's framework discovery also reports the pseudo-frameworks `any`, `agnostic`, and `unsupported` for some layouts — most commonly the `any` target framework a package's `contentFiles` group contributes. They are not target frameworks and are ignored: substituting one would expand the token to an entry such as `lib/any/Foo.dll` that can never match, failing a package that is actually correct. diff --git a/docs/wiki/_Sidebar.md b/docs/wiki/_Sidebar.md index 91127bf..6143969 100644 --- a/docs/wiki/_Sidebar.md +++ b/docs/wiki/_Sidebar.md @@ -9,4 +9,5 @@ - [Local Development](Local-Development.md) - [Secrets and Environment Variables](Secrets-and-Environment-Variables.md) - [Repository CI/CD](Repository-CI-CD.md) +- [.NET 11 support and feature evaluation](Net-11.md) - [Migration: aspire-resourcekit](Migration-aspire-resourcekit.md) \ No newline at end of file diff --git a/global.json b/global.json index 6e18a79..a09ffc3 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,9 @@ { + "sdk": { + "version": "11.0.100" + }, "msbuild-sdks": { - "Purview.BuildSdk": "1.3.2" + "Purview.BuildSdk": "2.0.0" }, "test": { "runner": "Microsoft.Testing.Platform" diff --git a/package.json b/package.json index 0587b1f..633b9d4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "build", - "version": "0.8.2", + "version": "0.9.0", "description": "Shared build/test/release pipeline for the purview-dev organisation, packaged as a pinned .NET tool with a GitHub composite action and reusable workflows.", "license": "MIT", "author": { diff --git a/skills/purview-build-pack-validation/SKILL.md b/skills/purview-build-pack-validation/SKILL.md index d189c9a..6144ff8 100644 --- a/skills/purview-build-pack-validation/SKILL.md +++ b/skills/purview-build-pack-validation/SKILL.md @@ -55,7 +55,7 @@ the `.snupkg` keeps them for source-link validation. ### The `$(TFM)` token An entry may contain the literal `$(TFM)`, e.g. `lib/$(TFM)/Purview.Telemetry.dll`. It expands into -one entry **per target framework the package actually ships** — the short folder name (`net8.0`, +one entry **per target framework the package actually ships** — the short folder name (`net11.0`, `net48`, `netstandard2.0`), discovered from the package's own `lib`/`ref`/`build`/`tools`/ `frameworkAssemblies` groups — so the rule must be satisfied **independently for every one of them**. diff --git a/src/src/Build/Build.csproj b/src/src/Build/Build.csproj index dc17718..6303412 100644 --- a/src/src/Build/Build.csproj +++ b/src/src/Build/Build.csproj @@ -7,6 +7,21 @@ Shared build/test/release pipeline for purview-dev. A Modular Pipelines based .NET tool (Purview.Build) with a GitHub composite action and reusable workflows; consumers configure, the pipeline runs. $(NoWarn);CA1515;CA1873; + + $(NoWarn);CA1307; + $(PurviewPolicyExemptNoWarn);CA1307; + + preview true diff --git a/src/src/Build/Configuration/ConfigurationPreflight.cs b/src/src/Build/Configuration/ConfigurationPreflight.cs index ccc98e3..e7bc126 100644 --- a/src/src/Build/Configuration/ConfigurationPreflight.cs +++ b/src/src/Build/Configuration/ConfigurationPreflight.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; namespace Purview.Build.Configuration; @@ -116,6 +117,16 @@ List problems } } + // DataAnnotations has no trim-safe entry point: constructing a ValidationContext is annotated + // RequiresUnreferencedCode because it reflects for DisplayNameAttribute. No settings type carries + // that attribute, and PublishLocalNuGetSettings validates through IValidatableObject, so the + // reflection it warns about resolves nothing here. The check runs before every pipeline, including + // in a NativeAOT publish, and is covered by the preflight tests. + [UnconditionalSuppressMessage( + "Trimming", + "IL2026", + Justification = "Settings types carry no DisplayNameAttribute; validation resolves no reflected metadata beyond the types themselves." + )] static void ValidateLocalNuGet( BuildSettings build, ReleaseSettings release, diff --git a/src/src/Build/Helpers/PackageInspector.cs b/src/src/Build/Helpers/PackageInspector.cs index 815f178..608602a 100644 --- a/src/src/Build/Helpers/PackageInspector.cs +++ b/src/src/Build/Helpers/PackageInspector.cs @@ -14,7 +14,7 @@ static class PackageInspector static readonly Guid CompilerFlagsDebugInfoGuid = new("B5FEEC05-8CD0-4A83-96DA-466284BB4BD8"); // Target-framework partial token, e.g. "lib/$(TFM)/Foo.dll" expands to one entry per - // target framework the package supports (short folder name, e.g. "net8.0", "net48"). + // target framework the package supports (short folder name, e.g. "net11.0", "net48"). const string TfmToken = "$(TFM)"; public static bool MatchesAny(string pattern, IEnumerable paths) diff --git a/src/src/Build/Modules/PublishLocalNuGetModule.cs b/src/src/Build/Modules/PublishLocalNuGetModule.cs index c8d9916..a741440 100644 --- a/src/src/Build/Modules/PublishLocalNuGetModule.cs +++ b/src/src/Build/Modules/PublishLocalNuGetModule.cs @@ -34,6 +34,13 @@ protected override ModuleConfiguration Configure() => ) .Build(); + // See ConfigurationPreflight.ValidateLocalNuGet: DataAnnotations has no trim-safe entry point, and + // the settings type carries no DisplayNameAttribute for it to reflect over. + [UnconditionalSuppressMessage( + "Trimming", + "IL2026", + Justification = "PublishLocalNuGetSettings carries no DisplayNameAttribute; validation resolves no reflected metadata beyond the type itself." + )] protected override async Task ExecuteAsync( [NotNull] IModuleContext context, CancellationToken cancellationToken @@ -46,14 +53,28 @@ CancellationToken cancellationToken List validationResults = []; ValidationContext validationContext = new(settings); - if ( - !Validator.TryValidateObject( - settings, - validationContext, - validationResults, - validateAllProperties: true - ) - ) + + // .NET 11 added asynchronous DataAnnotations validation, and CA1849 asks this module to use it + // because the module runs inside an async method. The net10.0 leg keeps the synchronous call: + // the settings type declares no asynchronous validators, so the synchronous form cannot block. +#if NET11_0_OR_GREATER + var settingsAreValid = await Validator.TryValidateObjectAsync( + settings, + validationContext, + validationResults, + validateAllProperties: true, + cancellationToken: cancellationToken + ); +#else + var settingsAreValid = Validator.TryValidateObject( + settings, + validationContext, + validationResults, + validateAllProperties: true + ); +#endif + + if (!settingsAreValid) { foreach (var validationResult in validationResults) context.Logger.LogError("{Message}", validationResult.ErrorMessage); diff --git a/src/src/Build/Release/BuildJsonContext.cs b/src/src/Build/Release/BuildJsonContext.cs new file mode 100644 index 0000000..6c18059 --- /dev/null +++ b/src/src/Build/Release/BuildJsonContext.cs @@ -0,0 +1,17 @@ +using System.Text.Json.Serialization; + +namespace Purview.Build.Release; + +/// +/// Source-generated serialization metadata for every type the tool reads or writes as JSON. +/// +/// +/// The tool is publishable with NativeAOT, and an AOT build disables reflection-based serialization +/// outright rather than degrading: JsonSerializer.Serialize<T> throws +/// NotSupportedException at the first call. That is how release-explain --format=json +/// failed, while the text rendering beside it worked. Source generation also removes the reflection +/// cost from the ordinary framework-dependent tool, so the metadata is worth having either way. +/// +[JsonSerializable(typeof(ReleaseExplanation))] +[JsonSerializable(typeof(string[]))] +partial class BuildJsonContext : JsonSerializerContext; diff --git a/src/src/Build/Release/ReleaseContextProvider.cs b/src/src/Build/Release/ReleaseContextProvider.cs index 195819a..d78ed4b 100644 --- a/src/src/Build/Release/ReleaseContextProvider.cs +++ b/src/src/Build/Release/ReleaseContextProvider.cs @@ -118,7 +118,7 @@ static string[] ReadList(string path, string origin) { try { - return JsonSerializer.Deserialize(content) ?? []; + return JsonSerializer.Deserialize(content, BuildJsonContext.Default.StringArray) ?? []; } catch (JsonException exception) { diff --git a/src/src/Build/Release/ReleaseExplanation.cs b/src/src/Build/Release/ReleaseExplanation.cs index 30286c7..3e22d68 100644 --- a/src/src/Build/Release/ReleaseExplanation.cs +++ b/src/src/Build/Release/ReleaseExplanation.cs @@ -73,9 +73,29 @@ sealed record ReleaseExplanation // to ', which is valid JSON but makes the contract unreadable in a workflow log and in // the golden file that pins it. This output goes to a console and to jq, never into HTML. Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + // Attaching the source-generated resolver keeps every option above while giving the serializer + // metadata it can see at compile time, which is the only form an AOT publish supports. + TypeInfoResolver = BuildJsonContext.Default, }; - public string ToJson() => JsonSerializer.Serialize(this, JsonOptions); + /// + /// Renders the report as the JSON contract. + /// + /// + /// The type info comes from the source-generated resolver, so this works in a NativeAOT publish, + /// where reflection-based serialization throws. .NET 11 added the generic + /// GetTypeInfo<T>(); the net10.0 leg casts the non-generic result instead. + /// + public string ToJson() + { +#if NET11_0_OR_GREATER + var typeInfo = JsonOptions.GetTypeInfo(); +#else + var typeInfo = (System.Text.Json.Serialization.Metadata.JsonTypeInfo) + JsonOptions.GetTypeInfo(typeof(ReleaseExplanation)); +#endif + return JsonSerializer.Serialize(this, typeInfo); + } } sealed record ConfigurationReport diff --git a/src/src/Build/Schema/ConfigSchemaGenerator.cs b/src/src/Build/Schema/ConfigSchemaGenerator.cs index ea9bcab..81d55ac 100644 --- a/src/src/Build/Schema/ConfigSchemaGenerator.cs +++ b/src/src/Build/Schema/ConfigSchemaGenerator.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text.Json; using System.Text.Json.Nodes; @@ -12,6 +13,12 @@ namespace Purview.Build.Schema; /// The settings types are the single source of truth: a key the tool can bind is a key the schema /// describes, and a key the tool cannot bind fails validation. just schema regenerates the /// committed file, and ConfigSchemaTests fails the build when the two drift. +/// +/// This type is the one part of the assembly that is not NativeAOT-safe, and deliberately so: +/// it is reached only from the test suite and just schema, never from a published binary, and +/// it reflects over every settings type by design. The suppressions below say exactly that, per call +/// site, so an AOT publish stays quiet without hiding a reflection risk the tool actually runs. +/// /// static class ConfigSchemaGenerator { @@ -37,6 +44,11 @@ static readonly (string Section, Type Type)[] Sections = (ReleaseSettings.SectionName, typeof(ReleaseSettings)), ]; + [UnconditionalSuppressMessage( + "Aot", + "IL3050", + Justification = "Test-only: JsonStringEnumConverter is reflection-based, which is fine where only the test host generates the schema. The generic form needs the enum type per converter, and this option set is shared by every settings type." + )] static readonly JsonSerializerOptions ValueOptions = new() { Converters = { new JsonStringEnumConverter() }, @@ -94,6 +106,11 @@ public static string Render(JsonObject? shippedDefaults = null) => public static JsonObject? LoadShippedDefaults(string path) => File.Exists(path) ? JsonNode.Parse(File.ReadAllText(path)) as JsonObject : null; + [UnconditionalSuppressMessage( + "Trimming", + "IL2067", + Justification = "Test-only: schema generation instantiates each settings type directly and is never called from a published binary." + )] static JsonObject BuildObject( Type type, JsonObject? shippedDefaults, @@ -231,11 +248,26 @@ IReadOnlyDictionary documentation /// SectionName/Default/Stable helpers and computed properties such as /// IsSimulated, neither of which the configuration binder can set. /// + [UnconditionalSuppressMessage( + "Trimming", + "IL2070", + Justification = "Test-only: schema generation reflects over each settings type by design and is never called from a published binary." + )] static IEnumerable SettingsProperties(Type type) => type.GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(property => property.CanWrite) .OrderBy(property => property.MetadataToken); + [UnconditionalSuppressMessage( + "Trimming", + "IL2026", + Justification = "Test-only: the default values serialized here are the settings types reflected over directly, which are rooted in the test host." + )] + [UnconditionalSuppressMessage( + "Aot", + "IL3050", + Justification = "Test-only: schema generation never runs under NativeAOT, where the dynamic code this would need is unavailable." + )] static JsonNode? ToJsonValue(object? value) => value is null ? null : JsonSerializer.SerializeToNode(value, value.GetType(), ValueOptions); diff --git a/src/src/Build/Settings/PackValidationSettings.cs b/src/src/Build/Settings/PackValidationSettings.cs index 5548c33..e3e08bd 100644 --- a/src/src/Build/Settings/PackValidationSettings.cs +++ b/src/src/Build/Settings/PackValidationSettings.cs @@ -51,7 +51,7 @@ public sealed record PackValidationSettings /// /// An entry may contain the literal token $(TFM) as a target-framework partial, e.g. /// "lib/$(TFM)/Foo.dll". The token is expanded into one required entry per target framework - /// the package actually supports (short folder name, e.g. "net8.0", "net48", "netstandard2.0"), + /// the package actually supports (short folder name, e.g. "net11.0", "net48", "netstandard2.0"), /// so every one of the package's target frameworks must independently satisfy the entry. /// Pseudo-frameworks reported by NuGet's package discovery ("any", "agnostic", "unsupported" - /// notably the "any" target framework a package's contentFiles group contributes) are not diff --git a/src/tests/Build.IntegrationTests/Infra/GitFixture.cs b/src/tests/Build.IntegrationTests/Infra/GitFixture.cs index a937b3f..ec6c250 100644 --- a/src/tests/Build.IntegrationTests/Infra/GitFixture.cs +++ b/src/tests/Build.IntegrationTests/Infra/GitFixture.cs @@ -107,8 +107,8 @@ public string Git(params string[] arguments) foreach (var argument in arguments) startInfo.ArgumentList.Add(argument); - using var process = - Process.Start(startInfo) ?? throw new InvalidOperationException("git could not be started."); + using var process = Process.Start(startInfo) + ?? throw new InvalidOperationException("git could not be started."); var output = process.StandardOutput.ReadToEnd(); var error = process.StandardError.ReadToEnd(); diff --git a/src/tests/Build.IntegrationTests/Infra/TestHelper.cs b/src/tests/Build.IntegrationTests/Infra/TestHelper.cs index a1872a7..8dee38e 100644 --- a/src/tests/Build.IntegrationTests/Infra/TestHelper.cs +++ b/src/tests/Build.IntegrationTests/Infra/TestHelper.cs @@ -63,12 +63,10 @@ public static byte[] BuildPortablePdb(params (Guid Kind, byte[] Value)[] debugIn ); } - PortablePdbBuilder pdbBuilder = new( - builder, - new int[64].ToImmutableArray(), - default, - _ => new BlobContentId(Guid.NewGuid(), 0) - ); + PortablePdbBuilder pdbBuilder = new(builder, new int[64].ToImmutableArray(), default, _ => new BlobContentId( + Guid.NewGuid(), + 0 + )); BlobBuilder blob = new(); pdbBuilder.Serialize(blob); diff --git a/src/tests/Build.UnitTests/GlobTests.cs b/src/tests/Build.UnitTests/GlobTests.cs index d958467..b88e3b9 100644 --- a/src/tests/Build.UnitTests/GlobTests.cs +++ b/src/tests/Build.UnitTests/GlobTests.cs @@ -147,7 +147,7 @@ public async Task ExactPackageId_WinsOverWildcard() public async Task TfmToken_ExpandsAndSatisfiesEachTargetFramework() { List errors = []; - string[] files = ["lib/net8.0/Foo.dll", "lib/netstandard2.0/Foo.dll"]; + string[] files = ["lib/net11.0/Foo.dll", "lib/netstandard2.0/Foo.dll"]; PackageInspector.ValidateContentRules( files, @@ -155,7 +155,7 @@ public async Task TfmToken_ExpandsAndSatisfiesEachTargetFramework() new() { ["purview.build"] = ["lib/$(TFM)/Foo.dll"] }, [], errors, - targetFrameworkFolderNames: ["net8.0", "netstandard2.0"] + targetFrameworkFolderNames: ["net11.0", "netstandard2.0"] ); await Assert.That(errors).IsEmpty(); @@ -165,7 +165,7 @@ public async Task TfmToken_ExpandsAndSatisfiesEachTargetFramework() public async Task TfmToken_MissingForOneFramework_ReportsError() { List errors = []; - string[] files = ["lib/net8.0/Foo.dll"]; + string[] files = ["lib/net11.0/Foo.dll"]; PackageInspector.ValidateContentRules( files, @@ -173,7 +173,7 @@ public async Task TfmToken_MissingForOneFramework_ReportsError() new() { ["purview.build"] = ["lib/$(TFM)/Foo.dll"] }, [], errors, - targetFrameworkFolderNames: ["net8.0", "netstandard2.0"] + targetFrameworkFolderNames: ["net11.0", "netstandard2.0"] ); await Assert.That(errors).Count().IsEqualTo(1); diff --git a/src/tests/Repository.UnitTests/Repository.UnitTests.csproj b/src/tests/Repository.UnitTests/Repository.UnitTests.csproj index ade4cc0..a32dcfd 100644 --- a/src/tests/Repository.UnitTests/Repository.UnitTests.csproj +++ b/src/tests/Repository.UnitTests/Repository.UnitTests.csproj @@ -1,7 +1,5 @@ - diff --git a/src/tests/scenarios/find-tool.sh b/src/tests/scenarios/find-tool.sh index c7b53bd..c008ecd 100644 --- a/src/tests/scenarios/find-tool.sh +++ b/src/tests/scenarios/find-tool.sh @@ -20,8 +20,12 @@ if [ -z "${PURVIEW_BUILD_TOOL:-}" ]; then for candidate in \ ".tools/local/Purview.Build.exe" \ ".tools/local/Purview.Build" \ + "src/src/Build/bin/Release/net11.0/Purview.Build.exe" \ + "src/src/Build/bin/Release/net11.0/Purview.Build" \ "src/src/Build/bin/Release/net10.0/Purview.Build.exe" \ "src/src/Build/bin/Release/net10.0/Purview.Build" \ + "src/src/Build/bin/Debug/net11.0/Purview.Build.exe" \ + "src/src/Build/bin/Debug/net11.0/Purview.Build" \ "src/src/Build/bin/Debug/net10.0/Purview.Build.exe" \ "src/src/Build/bin/Debug/net10.0/Purview.Build"; do if [ -x "$candidate" ]; then