From 4f1020e4ad2f210f33cf844b3b0428ce1cbad248 Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Thu, 12 Nov 2026 12:59:58 +0000 Subject: [PATCH] fix: make the memory-safety rules opt-in, fix SourceLink opt-out, and refresh the settings docs - The C# 15 updated memory-safety rules (MemorySafetyRules) are off by default again: the updated model is an experimental compiler feature, so tier 2 is opt-in. Tier 1 and the PRCFG0001 validation are unchanged, and AllowUnsafeBlocks is still never set. - DisableSourceLink=true now opts out for real. Sdk.props re-assigned DisableSourceLink to false for every non-Roslyn project, clobbering a repository pre-import value, so the documented opt-out silently did nothing and the GitHub provider was always added. - SourceLink is documented end to end: a wiki page, a README section and skill coverage explain the provider for each host (Azure Repos, Azure DevOps Server, GitLab, Bitbucket), the SourceLinkAzureDevOpsServerGitHost item, the CPM PackageVersion requirement and the CI detection. Microsoft.SourceLink.AzureDevOps.Git, which does not exist, is gone. - Required settings and versions, how to set the target framework(s), and the compiler-visible (build_property.*) properties are now consistent across the README, the wiki and the skills. - A repository-local skill, purview-buildsdk-settings-docs-sync, is tracked under .agents/skills with a .gitignore carve-out; InstalledSkillsAreNotTracked still fails if a skill the CLI installed is committed. - SourceLinkTests and Dotnet11FeaturesTests pin the new behaviour. --- .../SKILL.md | 188 ++++++++++++++++++ .gitignore | 14 +- AGENTS.md | 23 ++- CHANGELOG.md | 39 ++++ README.md | 96 ++++++++- docs/wiki/Agent-Skills.md | 14 +- docs/wiki/Configuration-Reference.md | 40 +++- docs/wiki/Getting-Started.md | 28 ++- docs/wiki/Home.md | 1 + docs/wiki/Packaging.md | 4 + docs/wiki/Project-Type-Detection.md | 10 +- docs/wiki/SourceLink.md | 117 +++++++++++ docs/wiki/_Sidebar.md | 1 + skills/purview-buildsdk-adoption/SKILL.md | 63 +++++- .../purview-buildsdk-configuration/SKILL.md | 83 +++++++- .../references/properties.md | 37 +++- .../references/sourcelink.md | 79 ++++++++ .../SKILL.md | 21 +- .../purview-buildsdk-project-layout/SKILL.md | 11 +- .../SKILL.md | 6 + skills/purview-buildsdk-testing/SKILL.md | 7 +- src/src/BuildSdk/Sdk/Props/Defaults.props | 13 +- src/src/BuildSdk/Sdk/Sdk.props | 7 +- src/src/BuildSdk/Sdk/Targets/Modern.targets | 13 +- .../Dotnet11FeaturesTests.cs | 38 +++- .../RepositorySkillsSelfTests.cs | 30 ++- .../SourceLinkTests.cs | 107 ++++++++++ 27 files changed, 1006 insertions(+), 84 deletions(-) create mode 100644 .agents/skills/purview-buildsdk-settings-docs-sync/SKILL.md create mode 100644 docs/wiki/SourceLink.md create mode 100644 skills/purview-buildsdk-configuration/references/sourcelink.md create mode 100644 src/tests/BuildSdk.IntegrationTests/SourceLinkTests.cs diff --git a/.agents/skills/purview-buildsdk-settings-docs-sync/SKILL.md b/.agents/skills/purview-buildsdk-settings-docs-sync/SKILL.md new file mode 100644 index 0000000..0933fb6 --- /dev/null +++ b/.agents/skills/purview-buildsdk-settings-docs-sync/SKILL.md @@ -0,0 +1,188 @@ +--- +name: purview-buildsdk-settings-docs-sync +description: Change or document a Purview.BuildSdk setting without leaving part of the surface behind. Use when flipping a .NET 11 compiler feature default (Runtime Async, memory-safety rules), adding or renaming a property, or making sure every skill and doc explains how to set required settings and versions, the target framework(s), and the compiler-visible (build_property.*) settings. Repository-local guidance for purview-dev/build-sdk. +license: MIT +metadata: + scope: repository-local + package: Purview.BuildSdk + repository: purview-dev/build-sdk +--- + +# Keeping settings, docs and skills in step + +> **Repository-local.** This skill lives in `.agents/skills/`, which is gitignored, not packed and not +> published. It is deliberately **not** linked from `AGENTS.md`: `RepositorySkillsSelfTests` fails any +> AGENTS.md link that points into `.agents/skills/`, because a fresh clone does not have this folder. +> The published skills are authored under `skills/` and are a different thing entirely. + +## The rule this exists for + +A property is written down in several places, and only some of them are checked by a test. The failure +mode this repository keeps hitting is a change that lands in one place and stops there — the property +declaration, its validation, `README.md`, the wiki page and the shipped skills drift apart, and the +only evidence is a consumer following a doc that still describes last release's default. + +So: **treat a settings change as a sweep, not an edit.** Work the surfaces below, then prove it with +the validation commands at the end. + +## When to use this + +- Flipping a .NET 11 compiler feature default (`MemorySafetyRules`, `PurviewRuntimeAsync`). +- Adding, renaming, re-defaulting or removing a user-facing property. +- Making required settings, versions, target frameworks or compiler-visible settings clearer in the + docs and the shipped skills. +- Auditing whether a property is documented everywhere before you change it. + +For auditing a *consumer* repository, use `purview-buildsdk-repository-review`. For what a project +detection rule does, use `purview-buildsdk-project-detection`. + +## The surfaces — one property, many homes + +Find the row and make sure every file agrees. + +| Surface | File | What changes | +| -- | -- | -- | +| Runtime default (all projects) | `src/src/BuildSdk/Sdk/Props/Defaults.props` | The default when unset, plus the explanatory comment. Sets the value in `Sdk.props`, which is also what makes it compiler-visible. | +| Per-TFM default | `src/src/BuildSdk/Sdk/Targets/Modern.targets` | The `net11.0`+ feature selection. Must agree with `Defaults.props`. | +| Compiler-visible description | `src/src/BuildSdk/Sdk/Sdk.props` | The `CompilerVisibleProperty` ``, which is what an analyzer author reads. | +| Pinning test | `src/tests/BuildSdk.IntegrationTests/Dotnet11FeaturesTests.cs` | The test that fails when the default changes silently. | +| Changelog | `CHANGELOG.md` | A `[Unreleased]` entry: `### Changed` for behaviour, `### Documentation` for docs-only. | +| README | `README.md` | The feature table row, the property table(s), the quick start, and the `.NET 11 features` section + tiers paragraph. | +| Wiki | `docs/wiki/Configuration-Reference.md`, `Getting-Started.md`, `Version-Detection.md`, `Project-Type-Detection.md` | The property tables, the required-settings and target-framework sections, and the compiler-visible section. | +| Shipped skills | `skills/purview-buildsdk-*/SKILL.md` and `references/*.md` | The property tables and the sections below. | +| AGENTS.md | `AGENTS.md` | The "authored set, and what each one answers" table, when a skill's scope grows. | + +Two traps worth naming: + +- **The default lives in two places.** `Defaults.props` defaults the property for every project (which + is what exposes it to the compiler); `Modern.targets` re-defaults it per-TFM. Change one and the + other silently disagrees. +- **`Sdk.props` vs `Sdk.targets` decides *where* a value may be set.** A property read during + `Sdk.props` evaluation must be set before the import; everything else can be set in the project. Say + which it is, in the docs, next to the property. + +A **feature** bigger than a single property gets its own guide as well. SourceLink is the worked +example, because the decision is per repository host: add a wiki page `docs/wiki/.md` and +register it in `_Sidebar.md` **and** `Home.md`, add a `## ` section to `README.md`, and — when +it is configuration — a `references/.md` under +`skills/purview-buildsdk-configuration/` that the `SKILL.md` links. Fix any wrong package or property +name you find while documenting: the SourceLink pages used to name `Microsoft.SourceLink.AzureDevOps.Git`, +which does not exist. + +## Worked example — flip a .NET 11 compiler feature default + +1. `Defaults.props` — change the default and the comment. Say **why** (an experimental compiler feature + is opt-in), not just what. +2. `Modern.targets` — change the same default in the per-TFM block and its header comment. +3. `Sdk.props` — the `CompilerVisibleProperty` `` ("0 off (default), …"). +4. `Dotnet11FeaturesTests.cs` — make the default test assert the switch is **absent**, rename it to say + so, and add an explicit opt-in test. De-couple any other test that asserted the switch was on by + default (for example, the project-features-survive test). +5. `README.md` — the `.NET 11 opt-ins` row, the `.NET 11 features` table row and the tiers paragraph, + and the property table row. +6. `docs/wiki/Configuration-Reference.md` — the General row **and** the `.NET 11 features` table. +7. Skills — the `MemorySafetyRules`/`PurviewRuntimeAsync` rows in + `skills/purview-buildsdk-configuration/SKILL.md` and `references/properties.md`. +8. `CHANGELOG.md` — a `[Unreleased]` `### Changed` entry that names the old default, the new one, how to + opt in, and what is unchanged. + +Keep `AllowUnsafeBlocks` (and anything else) at its compiler default: a feature flip must not quietly +change a second setting. + +## Required settings and versions — where it must be said + +Four values, stated as a table wherever a reader lands first: + +| Setting | Home | +| -- | -- | +| `NamespacePrefix` (required, pre-import) | `Directory.Build.props`, above the `` | +| SDK version | `global.json` → `msbuild-sdks.Purview.BuildSdk` (`PurviewBuildSdkVersionForGlobalJson` decides what the bootstrap writes) | +| Product version | repo-root `package.json` → `version` (`UsePackageJsonVersion`, `RootPackageJson`) | +| Package versions | `Directory.Packages.props` (CPM; a `PackageVersion` is a minimum) | + +Keep it identical in all four homes: `README.md` quick start, `docs/wiki/Configuration-Reference.md`, +`docs/wiki/Getting-Started.md`, and the `purview-buildsdk-adoption` and +`purview-buildsdk-configuration` skills. + +## Target framework(s) — where it must be said + +Every skill and doc that talks about configuring a project must explain how to set the target +framework(s), consistently: + +- the default set is `net10.0;net11.0` (`Current`); +- an explicit `TargetFramework`/`TargetFrameworks` must be declared **unconditionally** (the SDK reads + it from the project XML, so a `Condition` is not seen — `PurviewConditionedProjectDeclaration`); +- a curated `PurviewTargetFrameworkSet` (`Current`, `Latest`, `Supported`, `Broad`, `All`) or a + redefined `PurviewTargetFrameworks` (pre-import, in `Directory.Build.props`); +- a Roslyn component keeps `netstandard2.0` and ignores the sets (`PurviewTargetFrameworkSetIgnored`). + +The canonical home is `skills/purview-buildsdk-configuration/SKILL.md` and its +`references/properties.md`; the other skills point at it rather than restating the whole thing. + +## Compiler-visible settings — where it must be said + +The `CompilerVisibleProperty` → `build_property.*` mechanism, plus a one-line +`AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue("build_property.X", …)` example, belongs in: + +- `skills/purview-buildsdk-configuration/SKILL.md` and `references/properties.md`, +- `skills/purview-buildsdk-project-detection/SKILL.md` (the analyzers section), +- `README.md`, `docs/wiki/Configuration-Reference.md` and `docs/wiki/Project-Type-Detection.md`. + +Say both halves: **consume** the exported property instead of re-deriving SDK behaviour, and **add a +new one** by listing it as a `CompilerVisibleProperty` item (with a ``) in `Sdk.props` — +nothing else exposes an MSBuild property to the compiler. + +## Skills sweep + +Each published skill that touches project configuration gets the change; a skill that is only about +skills tooling does not. + +| Skill | What to update | +| -- | -- | +| `purview-buildsdk-configuration` | required settings, target framework(s), compiler-visible properties, the property table, `references/properties.md` | +| `purview-buildsdk-adoption` | the required-settings table and `global.json` example, the target-framework step | +| `purview-buildsdk-project-layout` | the target-framework section | +| `purview-buildsdk-project-detection` | the target-framework subsection, the `build_property.*` note | +| `purview-buildsdk-repository-review` | the read list (`global.json`, `package.json`), the target-framework finding | +| `purview-buildsdk-testing` | the test-project target-framework paragraph | +| `purview-buildsdk-skill-tests`, `-skill-installation`, `-skills-justfile` | **not** project configuration — leave them alone | + +Also update each skill's frontmatter `description` so the change makes the skill **fire** on the right +question ("setting the target framework(s)", "pinning the SDK version in global.json"). + +## Validate + +```bash +just build +just skills-validate # PRSKL0005, the pack-time check +just version-sync-check # the version pins must still match package.json +just lint-check # CSharpier over the repo + +# Targeted and fast (MTP: --treenode-filter, never --filter): +dotnet test --project src/BuildSdk.slnx --configuration Debug \ + --treenode-filter "/*/*/Dotnet11FeaturesTests/*" --ignore-exit-code 8 +dotnet test --project src/BuildSdk.slnx --configuration Debug \ + --treenode-filter "/*/*/(TargetFrameworkSetTests)|(CompilerVisiblePropertyTests)|(CoreDefaultsTests)/*" --ignore-exit-code 8 +dotnet test --project src/BuildSdk.slnx --configuration Debug \ + --treenode-filter "/*/*/(RepositorySkillTests)|(RepositorySkillsSelfTests)|(PurviewSkillValidationTests)/*" --ignore-exit-code 8 +``` + +State the commands you ran before declaring the change done. + +## Guardrails + +- **Do not publish repository-local advice.** Anything under `skills/` ships to every consumer through + `npx skills add purview-dev/build-sdk`; guidance about working *in this repository* stays here, in + `.agents/skills/`, or in `AGENTS.md`. +- **Do not add a link into `.agents/skills/` from `AGENTS.md`** — it is gitignored, so the link is + broken in a fresh clone and `RepositorySkillsSelfTests` fails the build. +- **Do not give a local skill an authored skill's name.** + `AuthoredSkillsAreNotInstalledIntoTheAgentDirectories` fails when a folder here collides with one + under `skills/`. +- **Preserve a default unless the task explicitly changes product behaviour.** A docs task is not + licence to re-default anything. +- **Do not restate a default in a consumer project file** — recommend deleting the repetition instead. +- **Never force-add `.agents/skills/`.** Everything the CLI writes is reproducible from + `skills-lock.json`; `InstalledSkillsAreNotTracked` fails if any of it is committed. +- Keep changes small, use Conventional Commits, and do not mix an unrelated refactor into the sweep. + diff --git a/.gitignore b/.gitignore index ac539eb..6cc7665 100644 --- a/.gitignore +++ b/.gitignore @@ -670,9 +670,9 @@ sketch # Agent skills installed by the skills CLI. # -# The authored copies live at skills// and are the only tracked ones. Everything below is -# written by `npx skills add` from that folder and is reproducible from the committed -# skills-lock.json, so none of it belongs in the repository: +# The published skills live at skills// and are the only ones the CLI installs. Everything the +# CLI writes below is reproducible from the committed skills-lock.json with +# `npx skills experimental_install`, so none of it belongs in the repository: # # .agents/skills/ the CLI's "universal" location, shared by most agents # .claude/skills/ per-agent links into the universal location @@ -680,6 +680,12 @@ sketch # # Note that .agents/agents/ and .agents/prompts/ are authored content and stay tracked, so the # skills folder is ignored specifically rather than the whole of .agents/. -/.agents/skills/ +# +# One exception. `.agents/skills/` is also the one location an agent reads natively here, so a +# repository-local skill - guidance about working in *this* repository, which must never ship to a +# consumer - is authored in place and tracked. It is un-ignored by name below, so everything the CLI +# writes stays out of git. +/.agents/skills/* +!/.agents/skills/purview-buildsdk-settings-docs-sync/ /.claude/skills/ /agent/skills/ diff --git a/AGENTS.md b/AGENTS.md index dc7297c..b4e5f52 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,7 @@ Use `.agents/` as the default location for **generic agentic information** share - `.agents/agents/` — canonical, cross-agent agent workflow specs (authored, tracked) - `.agents/prompts/` — canonical, cross-agent prompt specs (authored, tracked) -- `.agents/skills/` — **not authored.** Skills installed from *other* sources, written by the skills CLI and gitignored; see below. +- `.agents/skills/` — skills installed from *other* sources by the skills CLI (gitignored), plus any **repository-local** skill authored in place and tracked; see below. Use `.github/` customization files only as VS Code/Copilot wrappers or registration points when required by tooling. @@ -21,6 +21,7 @@ This distinction is the one most easily got wrong, and getting it wrong produces | `skills//` | the skills this repository **authors and publishes** | by hand | yes | | `skills-lock.json` | the record of skills installed **from other sources** | the skills CLI | yes | | `.agents/skills//` | skills installed from other sources (the CLI's "universal" location) | the CLI | no | +| `.agents/skills//` | a **repository-local** skill — guidance for working in *this* repository, never published | by hand | yes | | `.claude/skills/`, `agent/skills/` | per-agent links into the universal location | the CLI | no | ### `skills/` is published, not installed @@ -35,23 +36,25 @@ They are not installed into this repository's own agent directories. An agent wo Naming follows from the same fact. Skills install into one flat directory per agent, shared with every skill a developer has installed from anywhere, so **every skill here carries the `purview-buildsdk-` prefix**. `sdk-*`, `project-*`, `test-*` and `build-*` are far too common to use unprefixed, and the loser of a collision is whichever installed second. `RepositorySkillTests.EverySkill_IsNamedForThisProduct` enforces that. -Guidance about working *in this repository* belongs in this file, not in a skill — a skill here ships to someone else. +Guidance about working *in this repository* belongs in this file, or in a **repository-local** skill under `.agents/skills/` (which is not published) — a skill under `skills/` ships to someone else. -### `.agents/skills/` is for skills from elsewhere +### `.agents/skills/` — installed skills, plus the one local skill -To add one: +To add an installed skill: ```bash just skills-add purview-dev/engineering-playbook # append '#' to pin it ``` -Everything the CLI writes is gitignored, because it is all reproducible from the committed `skills-lock.json` with `npx skills experimental_install`. Only `.agents/skills/` is ignored, not the whole of `.agents/`, because `.agents/agents/` and `.agents/prompts/` are authored content. +Everything the CLI writes is gitignored, because it is all reproducible from the committed `skills-lock.json` with `npx skills experimental_install`. Only `.agents/skills/*` is ignored by name, not the whole of `.agents/`, because `.agents/agents/` and `.agents/prompts/` are authored content. + +One entry under `.agents/skills/` is **tracked**: `purview-buildsdk-settings-docs-sync`, a repository-local skill. It is guidance about working *in this repository*, so it must not ship to a consumer and cannot live under `skills/`, and `.agents/skills/` is the one location an agent reads natively here. `.gitignore` un-ignores it by name; `RepositorySkillsSelfTests.InstalledSkillsAreNotTracked` still fails if any skill the CLI *installed* — anything recorded in `skills-lock.json` — is tracked. **Skills have exactly one installer, and it is the skills CLI.** The SDK writes nothing into an agent directory at all — the `.agents` mirror that used to copy a package's `agents/` and `prompts/` has been removed outright. A package still ships `.agents/skills//`, but as *metadata*: it is what `ResolvePurviewSkillSources` reads to map a package to its skills and commit, not content to copy. The single-writer rule is why. The CLI's universal install location is that same folder, so a second writer would mean the mirror silently overwriting a pinned install on the next build while `PRSKL0011` compared a lockfile ref against content the mirror had already replaced. See "The agent-folder mirror was removed" below for what went with it. -Link to a skill by its **authored** path (`./skills//SKILL.md`). A link into `.agents/skills/` points at a gitignored file and is broken in a fresh clone; `RepositorySkillsSelfTests` fails the build if one reappears. +Link to a published skill by its **authored** path (`./skills//SKILL.md`). A link into `.agents/skills/` points at a CLI-installed, gitignored file and is broken in a fresh clone; `RepositorySkillsSelfTests` fails the build if one reappears. The repository-local skill needs no link here — the agent runtime finds it. ## Purpose @@ -69,10 +72,10 @@ The authored set, and what each one answers: | Skill | Answers | | -- | -- | -| [`purview-buildsdk-adoption`](./skills/purview-buildsdk-adoption/SKILL.md) | wiring the SDK into a repository, and why a property had no effect | -| [`purview-buildsdk-configuration`](./skills/purview-buildsdk-configuration/SKILL.md) | which property controls this, and where must it be set | -| [`purview-buildsdk-project-layout`](./skills/purview-buildsdk-project-layout/SKILL.md) | where does a project go, and what is it called | -| [`purview-buildsdk-project-detection`](./skills/purview-buildsdk-project-detection/SKILL.md) | why did the SDK do that automatically | +| [`purview-buildsdk-adoption`](./skills/purview-buildsdk-adoption/SKILL.md) | wiring the SDK into a repository, required settings and versions, and why a property had no effect | +| [`purview-buildsdk-configuration`](./skills/purview-buildsdk-configuration/SKILL.md) | which property controls this, where must it be set, how to set the target framework(s), and which SourceLink provider matches the repository host | +| [`purview-buildsdk-project-layout`](./skills/purview-buildsdk-project-layout/SKILL.md) | where does a project go, what is it called, and which target framework(s) it uses | +| [`purview-buildsdk-project-detection`](./skills/purview-buildsdk-project-detection/SKILL.md) | why did the SDK do that automatically, target framework included | | [`purview-buildsdk-testing`](./skills/purview-buildsdk-testing/SKILL.md) | writing TUnit tests, and running them under MTP | | [`purview-buildsdk-skill-tests`](./skills/purview-buildsdk-skill-tests/SKILL.md) | authoring a skill, and validating it with `Purview.Skills.Testing` | | [`purview-buildsdk-skill-installation`](./skills/purview-buildsdk-skill-installation/SKILL.md) | installing the skills a package ships, and the `PRSKL*` diagnostics | diff --git a/CHANGELOG.md b/CHANGELOG.md index 96e5fd7..2786bf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,45 @@ release summarise what the commits recorded rather than what was written at the ## [Unreleased] +### Changed + +- **The C# 15 updated memory-safety rules are off by default.** A `net11.0`+ target no longer compiles + with `updated-memory-safety-rules` unless asked. The updated model is an + experimental compiler feature, so it is an opt-in: set `MemorySafetyRules=2` to enable it. Tier `1` + (preview pointer syntax, which also selects `LangVersion=preview`) and tier `0` (off) are unchanged, + and an unrecognised tier still fails with `PRCFG0001`. `AllowUnsafeBlocks` remains at its compiler + default (`false`). Both .NET 11 compiler opt-ins are now opt-in, alongside Runtime Async. + +### Documentation + +- Made required settings and versions explicit in the README quick start, the wiki, and the + `purview-buildsdk-adoption` and `purview-buildsdk-configuration` skills: `NamespacePrefix` is + required and pre-import, the SDK version lives in `global.json`, the product version comes from + `package.json`, and package versions live in `Directory.Packages.props`. +- Documented how to set the target framework(s) — an explicit `TargetFramework`/`TargetFrameworks`, a + curated `PurviewTargetFrameworkSet`, or a redefined set — including the unconditional-declaration + rule. +- Documented the compiler-visible properties (`CompilerVisibleProperty` → `build_property.*`) for + analyzers and source generators. +- **Documented SourceLink end to end.** A new [SourceLink](docs/wiki/SourceLink.md) wiki page, a README + section, and skill coverage now explain how to point `SourceLinkPackageName` at the provider for the + repository host — including `Microsoft.SourceLink.AzureRepos.Git` for Azure DevOps Services and + `Microsoft.SourceLink.AzureDevOpsServer.Git` plus a `SourceLinkAzureDevOpsServerGitHost` item for + on-premises Azure DevOps Server / TFS, `Microsoft.SourceLink.GitLab` and + `Microsoft.SourceLink.Bitbucket.Git` — that the provider needs a `PackageVersion` under Central + Package Management, and how `ContinuousIntegrationBuild` follows the CI environment (`TF_BUILD` on + Azure Pipelines). The previous documentation named `Microsoft.SourceLink.AzureDevOps.Git`, which does + not exist. + +### Fixed + +- **`DisableSourceLink=true` now actually opts out of SourceLink.** `Sdk.props` re-assigned + `DisableSourceLink` to `false` for every non-Roslyn project, clobbering a repository's pre-import + value, so the documented opt-out silently had no effect and the GitHub provider was always added. The + redundant assignment is removed; `Props/Defaults.props` already defaults it. `SourceLinkTests` pins the + default provider, provider selection for another host, the test/shared-testing exclusions and the + opt-out. + ## [2.0.1] - 2026-11-12 ### Changed diff --git a/README.md b/README.md index 2717e48..589b00d 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ A reusable MSBuild SDK NuGet package that delivers standardised .NET project def | Feature | Detail | | -- | -- | | **Project type detection** | `IsCSharpProject`, `IsTestProject`, `IsSharedTestingProject`, `IsServerSharedProject`, `IsClientSideProject`, `IsContainerProject`, `IsWebSdkProject`, `IsAspireHostProject`, `IsBlazorWebAssemblyProject`, `IsBffProject`, … | -| **.NET 11 opt-ins** | A `net11.0` target compiles with the C# 15 updated memory-safety rules (`MemorySafetyRules=2`) and, when opted in with `PurviewRuntimeAsync=true`, Runtime Async (`runtime-async=on`), 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)) | +| **.NET 11 opt-ins** | A `net11.0` target can opt into the C# 15 updated memory-safety rules (`MemorySafetyRules=2`) and Runtime Async (`PurviewRuntimeAsync=true`): both are **off by default**, and `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;net11.0` TFMs (overridable), `LangVersion=latest` (C# 15), `Nullable=enable`, `ImplicitUsings=enable`, deterministic builds | @@ -21,7 +21,7 @@ A reusable MSBuild SDK NuGet package that delivers standardised .NET project def | **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`, `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`) | +| **SourceLink** | `Microsoft.SourceLink.GitHub` added to every non-test project (`PrivateAssets="all"`), configurable for Azure DevOps, GitLab, Bitbucket and more via `SourceLinkPackageName` (see [SourceLink](#sourcelink)) | | **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 | @@ -76,6 +76,42 @@ Copy [`templates/Directory.Packages.props`](https://github.com/purview-dev/build > **Note:** `ManagePackageVersionsCentrally=true` is set by the SDK. You **must** have a `Directory.Packages.props` at your repo root for CPM to work, even if it only contains the packages the SDK adds automatically. +### 5. Required settings and versions + +Four things have to be right before the SDK behaves as documented: + +| What | Where | Notes | +| -- | -- | -- | +| `NamespacePrefix` | `Directory.Build.props`, **above** the import | Required. The build fails without it (`ValidateRootNamespacePrefixTarget`) unless `DisableNamespacePrefixCheck=true`. | +| The SDK version | `global.json` → `msbuild-sdks.Purview.BuildSdk` | Pinned to a released version, e.g. `2.0.1`. The SDK bootstraps the file when it is missing and records the version it was packed as (`PurviewBuildSdkVersionForGlobalJson`). | +| The product version | repo-root `package.json` → `version` | Applied to both `Version` and `PackageVersion` by default. `UsePackageJsonVersion=Strict` fails the build instead of silently falling back to `0.0.1`. | +| Package versions | `Directory.Packages.props` | Central Package Management is on (`ManagePackageVersionsCentrally=true`). Every `PackageVersion` is a minimum, not an exact pin. | + +`NamespacePrefix`, `UsePackageJsonVersion` and `RootPackageJson` must be set **before** the ``. +See [Version Detection](docs/wiki/Version-Detection.md) and the +[Configuration Reference](docs/wiki/Configuration-Reference.md). + +### 6. Setting the target framework(s) + +A project that declares neither `TargetFramework` nor `TargetFrameworks` gets the SDK's default set, +`net10.0;net11.0` (`Current`). Pick a different one in one of three ways, narrowest first: + +1. **Declare it in the project** — `TargetFramework` for a single target, `TargetFrameworks` (plural) + for many. It must be declared **unconditionally**: the SDK reads it from the project XML before the + project body is evaluated, so a `Condition` on it is not seen (and is reported as + `PurviewConditionedProjectDeclaration`). +2. **Select a curated set** — `Supported` in + `Directory.Build.props` or the `.csproj`. Named sets are `Current` (the default), `Latest`, + `Supported`, `Broad` and `All`, so a lifecycle change is one SDK bump rather than an edit in every + project. +3. **Redefine a set** — set `PurviewTargetFrameworks` (for example + `PurviewTargetFrameworksSupported`) **before the import** to pin a repository's meaning of a set. + +An unrecognised set name fails the build with `PurviewInvalidTargetFrameworkSet`. A project declaring +`TargetFrameworks` keeps `TargetFramework` empty. A Roslyn component targets `netstandard2.0` and +ignores the sets. See +[Target framework sets](docs/wiki/Configuration-Reference.md#target-framework-sets). + --- ## Engineering principles @@ -369,15 +405,15 @@ Version detection logging is disabled by default. Set `VersionDetectionLogEnable | `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. | -| `SourceLinkPackageName` | `Microsoft.SourceLink.GitHub` | SourceLink provider. Set to `Microsoft.SourceLink.AzureDevOps.Git` for ADO repos. | -| `DisableSourceLink` | `false` | Set to `true` to stop the SDK from adding the configured SourceLink package automatically. | +| `SourceLinkPackageName` | `Microsoft.SourceLink.GitHub` | SourceLink provider package, added to every non-test project. Match the repository host: `Microsoft.SourceLink.AzureRepos.Git` for Azure DevOps Services, `Microsoft.SourceLink.AzureDevOpsServer.Git` for on-premises Azure DevOps Server / TFS, `Microsoft.SourceLink.GitLab`, `Microsoft.SourceLink.Bitbucket.Git`. Set it **before the SDK import**. See [SourceLink](#sourcelink). | +| `DisableSourceLink` | `false` | Set to `true` **before the SDK import** to stop the SDK from adding the SourceLink package at all. | | `EnableAssemblyNameGeneration` | `true` | When `true` (default), `AssemblyName` and `PackageId` derive from the fully evaluated `RootNamespace`. When explicitly `false`, standard .NET behaviour applies (`$(MSBuildProjectName)`). Explicit ``/`` in a `.csproj` always take precedence. | | `DisableProjectFileNamingConventionCheck` | `false` | Set to `true` to disable the validation that requires `MyProject\MyProject.csproj` naming alignment. | | `DisableGenerateAssemblyInfoClass` | `false` | Set to `true` to disable the generated `AssemblyInfo` helper source. | | `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. | +| `MemorySafetyRules` | `0` | C# 15 memory-safety tier for `net11.0`+ targets: `0` off (default), `1` preview pointer syntax, `2` updated rules (`updated-memory-safety-rules`). The updated rules are an experimental compiler feature, so they are opt-in. `AllowUnsafeBlocks` is never changed. | | `PurviewRuntimeAsync` | `false` | Set to `true` to compile `net11.0`+ targets with `runtime-async=on`. `UseRuntimeAsync=true` also opts in; an explicit `false` under either name keeps it off. | | `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. | @@ -485,7 +521,13 @@ The SDK automatically adds a `.gitignore` file into each second-level folder und ### Compiler-visible SDK properties -The SDK now exports its properties via `CompilerVisibleProperty`, so analyzers and source generators can read them through `build_property.`. +The SDK exports its properties via `CompilerVisibleProperty`, so analyzers and source generators read them through `build_property.`: + +```csharp +context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.IsBffProject", out var isBff); +``` + +Read the exported property rather than re-deriving SDK behaviour — it is the SDK's own answer, and it stays correct when the rules change. A property an analyzer needs must be listed as a `CompilerVisibleProperty` item (with a ``) in `Sdk.props`; nothing else exposes an MSBuild property to the compiler. The SDK's own analyzers rely on this (for example `build_property.IsBffProject` gates `PDS0008`/`PDS0009`). | Property | Description | | -- | -- | @@ -584,12 +626,13 @@ later) target, so the `net10.0` leg of the default `net10.0;net11.0` set is unaf | Feature | Applied | Why | | -- | -- | -- | | Runtime Async V2 (opt-in) | `runtime-async=on` when enabled | Enables Runtime Async for a target where the team has decided it is wanted: cleaner live stack traces, lower await overhead, and continuations that skip `ExecutionContext` when nothing is ambient. It is **off by default** because it changes how every `await` compiles; opt in with `PurviewRuntimeAsync=true` or `UseRuntimeAsync=true`. An explicit `false` under either name keeps it off. | -| 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. | +| C# 15 updated memory-safety rules (opt-in) | `updated-memory-safety-rules` when enabled | `unsafe` marks requires-unsafe APIs and the compiler records the choice in `MemorySafetyRulesAttribute`. **Off by default** because the updated model is an experimental compiler feature; opt in with `MemorySafetyRules=2`. 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`. +`MemorySafetyRules` tiers: `0` (off, the default), `1` (preview pointer syntax, sets `LangVersion=preview` +unless the project named its own version), `2` (updated rules, opt-in). Anything else fails with +`PRCFG0001`. ### Evaluated and deliberately not defaulted @@ -614,6 +657,41 @@ the .NET 11 rule set, the MSBuild server is on by default in the SDK, and the de --- +## SourceLink + +Every non-test project gets a SourceLink provider package with `PrivateAssets="all"`, so a debugger can +step from a consumed package into its source, and the packed `.nuspec` carries the repository URL and the +exact commit. Test and shared-testing projects are excluded, because their symbols are not published. + +| Property | Default | Description | +| -- | -- | -- | +| `SourceLinkPackageName` | `Microsoft.SourceLink.GitHub` | The provider package. Set it **before the SDK import** to match the repository host. | +| `DisableSourceLink` | `false` | Set to `true` **before the import** to stop the SDK adding the package at all. | + +Pick the provider for your host: + +| Repository host | Provider package | +| -- | -- | +| GitHub / GitHub Enterprise | `Microsoft.SourceLink.GitHub` *(default)* | +| Azure DevOps Services / Azure Repos (`dev.azure.com`, `*.visualstudio.com`) | `Microsoft.SourceLink.AzureRepos.Git` | +| Azure DevOps Server / TFS (on-premises) | `Microsoft.SourceLink.AzureDevOpsServer.Git`, plus a `SourceLinkAzureDevOpsServerGitHost` item | +| GitLab | `Microsoft.SourceLink.GitLab` | +| Bitbucket | `Microsoft.SourceLink.Bitbucket.Git` | + +```xml + + + Microsoft.SourceLink.AzureRepos.Git + +``` + +Central Package Management is on, so the chosen provider also needs a `PackageVersion` in +`Directory.Packages.props` — the shipped template declares GitHub, Azure Repos and GitLab. +`ContinuousIntegrationBuild` is set from the CI environment (`CI`, `GITHUB_ACTIONS`, `TF_BUILD`), never +from a local build. Full detail, including on-premises host configuration and self-hosted providers, is +in [SourceLink](docs/wiki/SourceLink.md). + + ## ServerShared `ServerShared` is the server-side counterpart of `Shared`. A project is recognised by the same diff --git a/docs/wiki/Agent-Skills.md b/docs/wiki/Agent-Skills.md index 5d6cff9..b576ee1 100644 --- a/docs/wiki/Agent-Skills.md +++ b/docs/wiki/Agent-Skills.md @@ -394,8 +394,8 @@ The CLI records that portably — `"source": "."`, `"sourceType": "local"` — s `Purview.BuildSdk` itself deliberately does **not** do this. Every skill it authors describes the packages it ships and exists for *other* repositories to install, so an agent working in -`build-sdk` reads them at their authored path and `.agents/skills/` is left for skills installed -from elsewhere. Install from the working tree when a repository authors skills it also consumes; +`build-sdk` reads them at their authored path and `.agents/skills/` is left to the skills CLI (apart +from one repository-local skill). Install from the working tree when a repository authors skills it also consumes; skip it when, as here, the skills are published rather than used locally. What the CLI writes should be gitignored, since all of it is reproducible from `skills/` plus the @@ -406,11 +406,15 @@ lockfile: | `skills//` | authored by hand | yes | | `skills-lock.json` | the CLI | yes | | `.agents/skills//` | the CLI's "universal" location | no | +| `.agents/skills//` | a repository-local skill, authored by hand for this repository only | yes | | `.claude/skills/`, `agent/skills/`, … | per-agent links into it | no | -> **The CLI's universal location is `.agents/skills/`.** Ignore that specifically rather than the -> whole of `.agents/`, so any authored `.agents/agents/` and `.agents/prompts/` content stays -> tracked. +> **The CLI's universal location is `.agents/skills/`.** Ignore its contents specifically rather than +> the whole of `.agents/`, so any authored `.agents/agents/` and `.agents/prompts/` content stays +> tracked — and un-ignore a **repository-local** skill by name. That is guidance about working in this +> repository, which must never ship to a consumer, so it cannot live under `skills/`; `.agents/skills/` +> is the one location an agent reads natively. `RepositorySkillsSelfTests.InstalledSkillsAreNotTracked` +> still fails if any skill the CLI *installed* (anything in `skills-lock.json`) is tracked. ### One installer diff --git a/docs/wiki/Configuration-Reference.md b/docs/wiki/Configuration-Reference.md index 2bac8d1..9e5da0a 100644 --- a/docs/wiki/Configuration-Reference.md +++ b/docs/wiki/Configuration-Reference.md @@ -3,6 +3,18 @@ Set any of these properties **before** the `` in your `Directory.Build.props` (most are consumed during `Sdk.props` evaluation). Explicit values set before the import are always preserved. +## Required settings and versions + +Four values have to be set correctly for the SDK to behave as documented. The first three are read +during `Sdk.props` evaluation, so they must be set **before** the ``. + +| Setting | Where | Notes | +| -- | -- | -- | +| `NamespacePrefix` | `Directory.Build.props` (pre-import) | Required. Without it the build fails (`ValidateRootNamespacePrefixTarget`) unless `DisableNamespacePrefixCheck=true`. | +| SDK version | `global.json` → `msbuild-sdks.Purview.BuildSdk` | Pinned to a released version. Bootstrapped when missing; `PurviewBuildSdkVersionForGlobalJson` decides the value written. | +| Product version | repo-root `package.json` → `version` | Applied to `Version` and `PackageVersion` by default; `UsePackageJsonVersion=Strict` fails the build instead of falling back to `0.0.1`. Set `RootPackageJson` (pre-import) to name the file explicitly. | +| Package versions | `Directory.Packages.props` | Central Package Management is on; a `PackageVersion` is a minimum, not an exact pin. | + ## Version detection | Property | Default | Description | @@ -28,15 +40,15 @@ See [Version Detection](Version-Detection.md) for the full resolution rules. | `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. | -| `SourceLinkPackageName` | `Microsoft.SourceLink.GitHub` | SourceLink provider. Set to `Microsoft.SourceLink.AzureDevOps.Git` for ADO repos. | -| `DisableSourceLink` | `false` | Set to `true` to stop the SDK from adding the configured SourceLink package automatically. | +| `SourceLinkPackageName` | `Microsoft.SourceLink.GitHub` | SourceLink provider package, added (with `PrivateAssets="all"`) to every non-test project. Match the repository host: `Microsoft.SourceLink.AzureRepos.Git` for Azure DevOps Services, `Microsoft.SourceLink.AzureDevOpsServer.Git` for on-premises Azure DevOps Server / TFS, `Microsoft.SourceLink.GitLab`, `Microsoft.SourceLink.Bitbucket.Git`. Set it **before the SDK import** — it is read during `Sdk.props`. See [SourceLink](SourceLink.md). | +| `DisableSourceLink` | `false` | Set to `true` **before the SDK import** to stop the SDK from adding the SourceLink package at all. An empty `SourceLinkPackageName` does not opt out; the SDK fills it with the GitHub default. | | `EnableAssemblyNameGeneration` | `true` | When `true` (default), `AssemblyName` and `PackageId` derive from the fully evaluated `RootNamespace`. When explicitly `false`, standard .NET behaviour applies (`$(MSBuildProjectName)`). Explicit ``/`` in a `.csproj` always take precedence. | | `DisableProjectFileNamingConventionCheck` | `false` | Set to `true` to disable the validation that requires `MyProject\MyProject.csproj` naming alignment. | | `DisableGenerateAssemblyInfoClass` | `false` | Set to `true` to disable the generated `AssemblyInfo` helper source. | | `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. | +| `MemorySafetyRules` | `0` | C# 15 memory-safety tier on `net11.0`+ targets: `0` off (default), `1` preview pointer syntax (sets `LangVersion=preview` when the project has not chosen one), `2` updated rules (`updated-memory-safety-rules`). The updated rules are an experimental compiler feature, so tier `2` is opt-in. An unrecognised value fails with `PRCFG0001`. `AllowUnsafeBlocks` is never changed. | | `PurviewRuntimeAsync` | `false` | Set to `true` to add `runtime-async=on` on `net11.0`+ targets. `UseRuntimeAsync=true` also opts in; an explicit `false` under either name keeps it off. | | `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. | @@ -181,13 +193,13 @@ 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. +Both compiler opt-ins are off by default and apply only to a `net11.0`+ target; the `net10.0` leg of a +multi-targeted project is unchanged. -| Property | Applied | Notes | +| Property | Default | Notes | | -- | -- | -- | | `PurviewRuntimeAsync` | `false` | When set to `true`, adds `runtime-async=on`. `UseRuntimeAsync=true` also opts in; an explicit `false` under either name keeps it off. | -| `MemorySafetyRules` | `2` | `updated-memory-safety-rules`; `1` sets `LangVersion=preview` for the pointer-syntax tier; `0` disables. `PRCFG0001` on an unknown value. | +| `MemorySafetyRules` | `0` | `0` off (default); `1` sets the preview pointer-syntax tier (`LangVersion=preview` unless the project named its own); `2` enables `updated-memory-safety-rules`, which is an experimental compiler feature and therefore opt-in. `PRCFG0001` on an unknown value. | | `AllowUnsafeBlocks` | *(compiler default: false)* | Never set by the SDK. | Server GC, `TieredPGO`, ReadyToRun, Native AOT, `UseArtifactsOutput`, `EnablePackageValidation`, @@ -258,6 +270,16 @@ See [Engineering Principles](Engineering-Principles.md) for the policy-level tes The SDK exports its properties via `CompilerVisibleProperty`, so analyzers and source generators can read them through `build_property.`: +```csharp +// In a DiagnosticAnalyzer or ISourceGenerator. +context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.IsBffProject", out var isBff); +``` + +Read the exported property rather than re-deriving SDK behaviour: the export is the SDK's own answer +and stays correct when the rules change. **To expose a new property**, add a `CompilerVisibleProperty` +item (with a ``) alongside the others in `Sdk.props` — an analyzer cannot see an MSBuild +property that is not in this list. + | Property | Description | | -- | -- | | `UsePackageJsonVersion` | Whether version detection from `package.json` is active. | @@ -270,8 +292,8 @@ read them through `build_property.`: | `TestingFramework` | Selected testing framework (`TUnit`, `Xunit`, or `None`). | | `SubstituteFramework` | Selected mocking provider (`TUnitMocks`, `NSubstitute`, or `None`). | | `TestDataFramework` | Selected test data provider (`Bogus` or `None`). | -| `SourceLinkPackageName` | SourceLink package ID added by the SDK. | -| `DisableSourceLink` | Disables automatic SourceLink integration. | +| `SourceLinkPackageName` | SourceLink provider package added by the SDK (see [SourceLink](SourceLink.md)). | +| `DisableSourceLink` | Disables the automatic SourceLink package reference. | | `ExcludePurviewTelemetry` | Opt-out for `Purview.Telemetry.SourceGenerator`. | | `ExcludeMSTelemetryExtension` | Opt-out for `Microsoft.Extensions.Telemetry.Abstractions`. | | `PurviewAutoSdkPack` | When `true`, automatically packs the `Sdk/` folder contents into the NuGet package. | diff --git a/docs/wiki/Getting-Started.md b/docs/wiki/Getting-Started.md index 734d6b1..bfd0bb4 100644 --- a/docs/wiki/Getting-Started.md +++ b/docs/wiki/Getting-Started.md @@ -6,7 +6,8 @@ automatically. ## 1. Add the SDK to `global.json` -Add `Purview.BuildSdk` to the `msbuild-sdks` section so MSBuild can resolve the SDK: +Add `Purview.BuildSdk` to the `msbuild-sdks` section so MSBuild can resolve the SDK. The value is the +pinned SDK version — bump it deliberately, because a released version can change defaults: ```json { @@ -21,7 +22,8 @@ Add `Purview.BuildSdk` to the `msbuild-sdks` section so MSBuild can resolve the > [!NOTE] > The SDK can also bootstrap a `global.json` for you at the repository root when one is missing; see -> [Repository Bootstrap](Repository-Bootstrap.md). +> [Repository Bootstrap](Repository-Bootstrap.md). The value it writes is the version the package was +> packed as (`PurviewBuildSdkVersionForGlobalJson`). ## 2. Create `Directory.Build.props` at the repo root @@ -60,6 +62,28 @@ build depend on the day it ran. > `Directory.Packages.props` at your repo root for CPM to work, even if it only contains the packages > the SDK adds automatically. +## Required settings and versions + +The four values above are the ones that decide whether the SDK behaves as documented: + +| Setting | Where it goes | Notes | +| -- | -- | -- | +| `NamespacePrefix` | `Directory.Build.props`, above the import | Required; the build fails without it unless `DisableNamespacePrefixCheck=true`. | +| SDK version | `global.json` → `msbuild-sdks.Purview.BuildSdk` | Pinned to a released version. | +| Product version | repo-root `package.json` → `version` | Applied to `Version` and `PackageVersion`. `UsePackageJsonVersion=Strict` fails instead of falling back to `0.0.1`. | +| Package versions | `Directory.Packages.props` | A `PackageVersion` is a minimum, not an exact pin. | + +## Setting the target framework(s) + +A project that declares neither `TargetFramework` nor `TargetFrameworks` gets the SDK's default set, +`net10.0;net11.0`. To change it: + +- declare `TargetFramework` (one) or `TargetFrameworks` (many) **unconditionally** in the project; or +- select a named set with `Supported` in + `Directory.Build.props` or the `.csproj` (`Current`, `Latest`, `Supported`, `Broad`, `All`). + +See [Target framework sets](Configuration-Reference.md#target-framework-sets). + ## Next steps - Read the policy-level guidance in [Engineering Principles](Engineering-Principles.md). diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md index 7f48b2f..e775f1a 100644 --- a/docs/wiki/Home.md +++ b/docs/wiki/Home.md @@ -21,6 +21,7 @@ repo root inherits everything automatically. - [InternalsVisibleTo](InternalsVisibleTo.md) - [Testing Wiring](Testing-Wiring.md) - [Packaging](Packaging.md) +- [SourceLink](SourceLink.md) - [SDK-Shipped Analyzers](Analyzers.md) - [Code Fixes](Code-Fixes.md) - [Repository Bootstrap](Repository-Bootstrap.md) diff --git a/docs/wiki/Packaging.md b/docs/wiki/Packaging.md index c81f7f1..fc0a7d0 100644 --- a/docs/wiki/Packaging.md +++ b/docs/wiki/Packaging.md @@ -19,6 +19,10 @@ project has not supplied a value** — explicit values are always preserved: | `DebugType` | `portable` | Ensures portable PDBs for symbol-package delivery. | | `IncludeSource` | `true` | Includes source files in the package. | +A SourceLink provider package (`SourceLinkPackageName`) is added to every non-test project, and +`PublishRepositoryUrl=true` writes the repository URL and commit into the `.nuspec`. See +[SourceLink](SourceLink.md). + Portable PDBs are delivered through the `.snupkg`; the normal `.nupkg` does **not** receive PDB files unless the project explicitly opts in (for example by adding `.pdb` to `AllowedOutputExtensionsInPackageBuildOutputFolder`). diff --git a/docs/wiki/Project-Type-Detection.md b/docs/wiki/Project-Type-Detection.md index cde3e5f..4c83e65 100644 --- a/docs/wiki/Project-Type-Detection.md +++ b/docs/wiki/Project-Type-Detection.md @@ -167,4 +167,12 @@ failure each behaviour prevents. The detection flags (plus many SDK properties) are exposed to Roslyn analyzers and source generators via `CompilerVisibleProperty`, readable as `build_property.`. The full list is in the -[Configuration Reference](Configuration-Reference.md). +[Configuration Reference](Configuration-Reference.md#compiler-visible-sdk-properties). + +```csharp +context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.IsBffProject", out var isBff); +``` + +Prefer the exported property over re-deriving SDK behaviour in an analyzer. A property an analyzer +needs must be added to the SDK's `CompilerVisibleProperty` list first — MSBuild properties are not +visible to the compiler until they are. diff --git a/docs/wiki/SourceLink.md b/docs/wiki/SourceLink.md new file mode 100644 index 0000000..ea8b805 --- /dev/null +++ b/docs/wiki/SourceLink.md @@ -0,0 +1,117 @@ +# SourceLink + +SourceLink lets a debugger step into a NuGet package's source: the PDB carries a mapping from the +package's own paths to the repository's file-content URLs, and the package manifest carries the +repository URL and the exact commit. The SDK wires it up by default, so the one decision a repository +has to make is **which provider package matches its host** — GitHub, Azure DevOps, GitLab, Bitbucket, or +something self-hosted. + +It also matters beyond debugging. The `` element the provider writes +(via `PublishRepositoryUrl`) is what lets other Purview tooling — the agent skills feature in particular +— pin a package to the exact commit it was built from. + +## What the SDK does automatically + +- Adds `SourceLinkPackageName` (default **`Microsoft.SourceLink.GitHub`**) as a `PackageReference` with + `PrivateAssets="all"` to **every non-test project**, not only packable ones. Test and shared-testing + projects are excluded, because their symbols are never published. +- `PublishRepositoryUrl=true` — the repository URL and commit land in the package's `.nuspec`. +- `EmbedUntrackedSources=true` and `DebugType=portable` on packable projects, so the PDB is portable and + SourceLink has a source for files the compiler generated or that are not tracked. +- `ContinuousIntegrationBuild` is set to `true` only when a real CI environment variable is present — + `CI`, `GITHUB_ACTIONS` or `TF_BUILD`. A local build is deliberately **not** treated as CI, because + SourceLink's CI-mode dirty-repository checks would otherwise fail a local `TreatWarningsAsErrors` + build. Azure Pipelines sets `TF_BUILD`; GitHub Actions sets `GITHUB_ACTIONS`. +- Roslyn components get the same treatment (`EmbedUntrackedSources=true`), so shipped analyzer PDBs are + source-linked too. + +## Choosing a provider for your host + +Set `SourceLinkPackageName` **before the SDK import** — in `Directory.Build.props`, above the +``. It is read while `Sdk.props` evaluates, so a value in the `.csproj` body is too late and has +no effect. + +| Repository host | `SourceLinkPackageName` | Extra configuration | +| -- | -- | -- | +| GitHub (github.com, GitHub Enterprise) | `Microsoft.SourceLink.GitHub` *(default)* | none | +| Azure DevOps Services / Azure Repos (`dev.azure.com`, `*.visualstudio.com`) | `Microsoft.SourceLink.AzureRepos.Git` | none | +| Azure DevOps Server / TFS (on-premises) | `Microsoft.SourceLink.AzureDevOpsServer.Git` | a `SourceLinkAzureDevOpsServerGitHost` item — the virtual directory cannot be inferred | +| GitLab (12.0+) | `Microsoft.SourceLink.GitLab` | none; for older GitLab add `SourceLinkGitLabHost` with `Version` | +| Bitbucket Cloud | `Microsoft.SourceLink.Bitbucket.Git` | none; for Bitbucket Server / Data Center older than 4.7 add `SourceLinkBitbucketGitHost` with `Version` | +| anything else / self-hosted git | *(none built in)* | add your own source-control and provider packages, or opt out | + +```xml + + + Acme + Microsoft.SourceLink.AzureRepos.Git + + + +``` + +### Azure DevOps + +- **Azure DevOps Services** (`https://dev.azure.com/…` or `https://.visualstudio.com/…`): reference + `Microsoft.SourceLink.AzureRepos.Git`, and nothing else. The provider reads the `origin` remote and + derives the content URLs itself. +- **Azure DevOps Server / TFS (on-premises)**: reference `Microsoft.SourceLink.AzureDevOpsServer.Git` + **and list the host**, because the virtual directory cannot be derived from the repository URL: + + ```xml + + + + ``` + + `Include` is the domain, optionally with a port (`tfs.example.com:8080`). A path such as + `tfs.example.com/tfs` is rejected by the provider's own validation ("not a valid domain name"), so the + virtual directory goes in its own attribute. +- Azure Pipelines sets `TF_BUILD`, so `ContinuousIntegrationBuild` turns on by itself there. + +## Central Package Management + +The SDK sets `ManagePackageVersionsCentrally=true`, so a provider package needs a `PackageVersion` entry +in the repository's `Directory.Packages.props`. The shipped +[`templates/Directory.Packages.props`](https://github.com/purview-dev/build-sdk/blob/main/templates/Directory.Packages.props) +already declares the three most common providers: + +```xml + + + +``` + +Add `Microsoft.SourceLink.AzureDevOpsServer.Git` (or `Microsoft.SourceLink.Bitbucket.Git`) yourself when +you need one of those. Selecting a provider you have not declared fails the restore with `NU1010`, +because the SDK adds it as an ordinary `PackageReference`. + +## Opting out + +```xml + + true + +``` + +`DisableSourceLink=true` stops the SDK from adding the package at all. Set it **before the import** — it +is read during `Sdk.props`, and a value in the `.csproj` body is too late. Setting +`SourceLinkPackageName` to an empty value does **not** opt out: the SDK fills an empty value with the +GitHub default. + +To keep the package's targets but skip the source mapping, set `EnableSourceLink=false` — that is the +provider package's own switch. + +## Other and self-hosted hosts + +SourceLink is provider-based: a *source-control* package (`Microsoft.Build.Tasks.Git`) plus a *provider* +package that maps the repository URL to content URLs and sets `EnableSourceLink=true`. When your host has +no official provider, either reference a third-party provider package (and point `SourceLinkPackageName` +at it), or bring both packages in yourself with `DisableSourceLink=true`, so the SDK does not add the +GitHub default alongside them. + +## See also + +- [Packaging](Packaging.md) — symbol packages and portable PDBs +- [Configuration Reference](Configuration-Reference.md) — the full property list +- [dotnet/sourcelink](https://github.com/dotnet/sourcelink) — upstream documentation and the provider list diff --git a/docs/wiki/_Sidebar.md b/docs/wiki/_Sidebar.md index e7e7950..af568b5 100644 --- a/docs/wiki/_Sidebar.md +++ b/docs/wiki/_Sidebar.md @@ -9,6 +9,7 @@ - [InternalsVisibleTo](InternalsVisibleTo.md) - [Testing Wiring](Testing-Wiring.md) - [Packaging](Packaging.md) +- [SourceLink](SourceLink.md) - [SDK-Shipped Analyzers](Analyzers.md) - [Code Fixes](Code-Fixes.md) - [Repository Bootstrap](Repository-Bootstrap.md) diff --git a/skills/purview-buildsdk-adoption/SKILL.md b/skills/purview-buildsdk-adoption/SKILL.md index bb24fa3..9f25048 100644 --- a/skills/purview-buildsdk-adoption/SKILL.md +++ b/skills/purview-buildsdk-adoption/SKILL.md @@ -1,6 +1,6 @@ --- name: purview-buildsdk-adoption -description: Adopt Purview.BuildSdk in a repository, or troubleshoot an adoption that is not behaving. Use when wiring the SDK into Directory.Build.props for the first time, when a property seems to have no effect, when the resolved version or namespace is wrong, or when a consuming repository builds but the SDK's automatic behaviour is not appearing. Covers the import chain, pre-import property ordering, and version detection from package.json. +description: Adopt Purview.BuildSdk in a repository, or troubleshoot an adoption that is not behaving. Use when wiring the SDK into Directory.Build.props for the first time, when a property seems to have no effect, when the resolved version, target framework or namespace is wrong, or when a consuming repository builds but the SDK's automatic behaviour is not appearing. Covers the import chain, pre-import property ordering, required settings and versions, target frameworks, and version detection from package.json. license: MIT metadata: package: Purview.BuildSdk @@ -41,7 +41,25 @@ defaults — naming, identity, framework selection — but none of the targets, packaging behaviour and the generated files never run. That combination usually presents as "the properties look right but nothing happens". -The SDK version comes from `global.json`, which the SDK itself bootstraps. +### Required settings and versions + +Four values decide whether the SDK behaves as documented. Set the first three **before** the import +where noted: + +| What | Where | Notes | +| -- | -- | -- | +| `NamespacePrefix` | `Directory.Build.props`, above the import | Required — the build fails without it unless `DisableNamespacePrefixCheck=true`. | +| SDK version | `global.json` → `msbuild-sdks.Purview.BuildSdk` | The pinned `Purview.BuildSdk` version, e.g. `2.0.1`. The SDK bootstraps `global.json` when it is missing and writes the version it was packed as (`PurviewBuildSdkVersionForGlobalJson`). | +| Product version | repo-root `package.json` → `version` | Drives `Version` and `PackageVersion`. `UsePackageJsonVersion` / `RootPackageJson` are pre-import. | +| Package versions | `Directory.Packages.props` | CPM is on; a `PackageVersion` is a minimum, not an exact pin. | + +```json +// global.json +{ + "test": { "runner": "Microsoft.Testing.Platform" }, + "msbuild-sdks": { "Purview.BuildSdk": "2.0.1" } +} +``` ## 2. Pre-import ordering is the first thing to check @@ -109,6 +127,47 @@ the installing, through `bun x` by default; with `PurviewSkillsAgents` unset it If the repository uses `just`, add recipes for them — see `purview-buildsdk-skills-justfile`. For what each target does and the `PRSKL*` diagnostics, see `purview-buildsdk-skill-installation`. +## 6. Target frameworks + +A project that declares neither `TargetFramework` nor `TargetFrameworks` gets `net10.0;net11.0`. +Confirm what a project resolves before changing it: + +```bash +dotnet msbuild -getProperty:TargetFramework -getProperty:TargetFrameworks +``` + +To set it: + +- **Declare it explicitly** — `TargetFramework` (one) or `TargetFrameworks` (many). It must be declared + **unconditionally**: the SDK reads it from the project XML before the body is evaluated, so a + `Condition` on it is not seen (`PurviewConditionedProjectDeclaration`). +- **Select a curated set** — `Supported` in + `Directory.Build.props` or the `.csproj` (`Current`, `Latest`, `Supported`, `Broad`, `All`), so a + lifecycle change is one SDK bump rather than an edit in every project. +- **Redefine a set** — set `PurviewTargetFrameworks` before the import to pin the repository's + meaning of a set. + +A Roslyn component targets `netstandard2.0` and ignores the sets. Full detail in +`purview-buildsdk-configuration` and [Configuration Reference](https://github.com/purview-dev/build-sdk/blob/main/docs/wiki/Configuration-Reference.md#target-framework-sets). + +## 7. SourceLink provider + +The SDK adds a SourceLink provider package (`SourceLinkPackageName`, default +`Microsoft.SourceLink.GitHub`) to every non-test project, and `PublishRepositoryUrl=true` writes the +repository URL and commit into the packed `.nuspec`. Match the repository host **before the SDK import**: + +| Host | Package | +| -- | -- | +| GitHub / GitHub Enterprise | `Microsoft.SourceLink.GitHub` *(default)* | +| Azure DevOps Services / Azure Repos | `Microsoft.SourceLink.AzureRepos.Git` | +| Azure DevOps Server / TFS (on-premises) | `Microsoft.SourceLink.AzureDevOpsServer.Git`, plus a `SourceLinkAzureDevOpsServerGitHost` item | +| GitLab | `Microsoft.SourceLink.GitLab` | +| Bitbucket | `Microsoft.SourceLink.Bitbucket.Git` | + +CPM is on, so add a `PackageVersion` for the chosen provider to `Directory.Packages.props` — the shipped +template already declares GitHub, Azure Repos and GitLab. `DisableSourceLink=true` opts out entirely. +Detail, including the on-premises host item, is in `purview-buildsdk-configuration`. + ## Constraints - Prefer minimal, targeted property changes over broad `Directory.Build.props` rewrites. diff --git a/skills/purview-buildsdk-configuration/SKILL.md b/skills/purview-buildsdk-configuration/SKILL.md index fca74cc..a83cb4c 100644 --- a/skills/purview-buildsdk-configuration/SKILL.md +++ b/skills/purview-buildsdk-configuration/SKILL.md @@ -1,6 +1,6 @@ --- name: purview-buildsdk-configuration -description: Configure Purview.BuildSdk from Directory.Build.props or a .csproj. Use when setting NamespacePrefix, choosing a testing or mocking framework, turning telemetry or SourceLink off, controlling version detection from package.json, tuning the repository bootstrap, making a library browser-safe for a Blazor WebAssembly client, configuring a BFF host, or working out whether a property must be set before the SDK import. Also use when the SDK "did something automatically" and you need the property that controls it. +description: Configure Purview.BuildSdk from Directory.Build.props or a .csproj. Use when setting NamespacePrefix, setting the target framework(s) or selecting a curated framework set, choosing a SourceLink provider for the repository host (GitHub, Azure DevOps, GitLab, Bitbucket), choosing a testing or mocking framework, turning telemetry or SourceLink off, controlling version detection from package.json, pinning the SDK version in global.json, tuning the repository bootstrap, making a library browser-safe for a Blazor WebAssembly client, configuring a BFF host, or working out whether a property must be set before the SDK import. Also use when the SDK "did something automatically" and you need the property that controls it. license: MIT metadata: package: Purview.BuildSdk @@ -30,9 +30,48 @@ Pre-import properties include: - `DisablePurviewBlazorWebAssemblyDefaults` - repo-wide testing-framework selection you want every project to inherit +A target framework is a special case: the SDK reads `TargetFramework`/`TargetFrameworks` and +`PurviewTargetFrameworkSet` from the **project XML**, so they can live in the project or in +`Directory.Build.props`, but must be declared **unconditionally** either way — see +[Setting the target framework(s)](#setting-the-target-frameworks). + Everything else is read in `Sdk.targets` and can be set in a project file. Prefer repo-wide defaults in `Directory.Build.props` unless the setting is deliberately project-specific. +## Required settings and versions + +Four things must be right, and three of them are read during `Sdk.props` evaluation — set those in +`Directory.Build.props` **above** the ``: + +| Setting | Where | Notes | +| -- | -- | -- | +| `NamespacePrefix` | `Directory.Build.props`, pre-import | Required. The build fails without it (`ValidateRootNamespacePrefixTarget`) unless `DisableNamespacePrefixCheck=true`. | +| SDK version | `global.json` → `msbuild-sdks.Purview.BuildSdk` | The pinned `Purview.BuildSdk` version. The SDK bootstraps `global.json` (with this value) when it is missing; `PurviewBuildSdkVersionForGlobalJson` decides what is written. | +| Product version | repo-root `package.json` → `version` | Drives both `Version` and `PackageVersion`. `UsePackageJsonVersion` (`true`/`false`/`Strict`) and `RootPackageJson` are pre-import; `Strict` fails the build when discovery cannot succeed instead of silently staying on `0.0.1`. | +| Package versions | `Directory.Packages.props` | CPM is on (`ManagePackageVersionsCentrally=true`). Each `PackageVersion` is a minimum, not an exact pin. | + +## Setting the target framework(s) + +A project that declares neither `TargetFramework` nor `TargetFrameworks` gets the SDK's default set, +`net10.0;net11.0` (`Current`). Pick one of three routes, narrowest first: + +1. **Explicit declaration** — `net10.0` for one target, + `net10.0;net11.0` for many. Declare it **unconditionally**: the + SDK reads it from the project XML before the body is evaluated, so a `Condition` on it is not seen + (and is reported as `PurviewConditionedProjectDeclaration`). A project declaring `TargetFrameworks` + keeps `TargetFramework` empty. +2. **A curated set** — `Supported` in + `Directory.Build.props` (repository-wide) or the `.csproj`. Sets: `Current` (default), + `Latest` (`net11.0`), `Supported` (`net10.0;net11.0`), `Broad` (`net8.0;net9.0;net10.0`), `All` + (`net8.0;net9.0;net10.0;net11.0`). An unknown name fails with `PurviewInvalidTargetFrameworkSet` + rather than silently falling back. +3. **Redefine a set** — set `PurviewTargetFrameworks` (for example `PurviewTargetFrameworksSupported`) + **before the import** to pin the repository's meaning of a set across an SDK bump. + +A Roslyn component (`IsRoslynComponent=true`) targets `netstandard2.0` and ignores the sets; selecting +one is reported as `PurviewTargetFrameworkSetIgnored`. The full table is in +[references/properties.md](references/properties.md#target-framework-and-language). + ## The properties you will reach for most | Property | Default | What it does | @@ -50,7 +89,7 @@ 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 | +| `MemorySafetyRules` | `0` | C# 15 memory-safety tier on `net11.0`+: `0` off (default), `1` preview syntax, `2` updated rules (`updated-memory-safety-rules`, opt-in — an experimental compiler feature) | | `PurviewRuntimeAsync` | `false` | Opts a `net11.0`+ target into `runtime-async=on`; `UseRuntimeAsync=true` also opts in | | `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 | @@ -79,10 +118,22 @@ dotnet msbuild -getProperty:TestingType -getProperty:TargetProjectName ## Compiler-visible properties -The SDK exports its properties to analyzers and generators as `build_property.`. When -authoring an analyzer, read the exported property instead of re-deriving SDK behaviour — the export -is the SDK's answer, and it stays correct when the rules change. The exported set is listed in -[references/properties.md](references/properties.md). +The SDK exports a large set of properties to Roslyn analyzers and source generators through +`CompilerVisibleProperty`, readable as `build_property.`: + +```csharp +context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.IsBffProject", out var isBff); +``` + +- **Consuming them** — read the exported property instead of re-deriving SDK behaviour; the export is + the SDK's answer and stays correct when the rules change. The SDK's own analyzers use this + (`build_property.IsBffProject` gates `PDS0008`/`PDS0009`). +- **Exposing a new one** — an analyzer can only see a property the SDK lists as a + `CompilerVisibleProperty` item (with a ``) in `Sdk.props`. Add it there; there is no + other mechanism, and an MSBuild property is invisible to the compiler until it is listed. + +The exported set is listed in +[references/properties.md](references/properties.md#compiler-visible-properties). ## Version detection @@ -126,6 +177,26 @@ on the command line, and a repository-wide choice belongs in `Directory.Build.pr On a command line, `dotnet msbuild -p:` splits values on `,` and `;` — write `-p:PurviewSkillsAgents=claude-code%2Ccodex`. For `just` recipes, see `purview-buildsdk-skills-justfile`. +## SourceLink + +Every non-test project gets a SourceLink provider package — `SourceLinkPackageName`, default +`Microsoft.SourceLink.GitHub`, added with `PrivateAssets="all"` so a debugger can step into a consumed +package's source and the `.nuspec` records the repository URL and commit. A repository on another host +selects the matching package **before the SDK import**: + +| Host | Provider package | +| -- | -- | +| GitHub / GitHub Enterprise | `Microsoft.SourceLink.GitHub` *(default)* | +| Azure DevOps Services / Azure Repos | `Microsoft.SourceLink.AzureRepos.Git` | +| Azure DevOps Server / TFS (on-premises) | `Microsoft.SourceLink.AzureDevOpsServer.Git` (+ `SourceLinkAzureDevOpsServerGitHost`) | +| GitLab | `Microsoft.SourceLink.GitLab` | +| Bitbucket | `Microsoft.SourceLink.Bitbucket.Git` | + +Because CPM is on, the chosen provider also needs a `PackageVersion` in `Directory.Packages.props`. Use +`DisableSourceLink=true` (pre-import) to opt out. The host matrix, the on-premises Azure DevOps Server +host item, the CPM requirement and the self-hosted case are in +[references/sourcelink.md](references/sourcelink.md). + ## Guidance when changing SDK configuration 1. Preserve existing defaults unless the task explicitly changes product behaviour. diff --git a/skills/purview-buildsdk-configuration/references/properties.md b/skills/purview-buildsdk-configuration/references/properties.md index 8a8f139..01b49f8 100644 --- a/skills/purview-buildsdk-configuration/references/properties.md +++ b/skills/purview-buildsdk-configuration/references/properties.md @@ -21,12 +21,23 @@ consuming project supplies nothing. | Property | Default | Behaviour | | -- | -- | -- | -| `TargetFramework` | `net10.0;net11.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. Declare it (or `TargetFrameworks`) **unconditionally** — the SDK reads it from the project XML, so a `Condition` on it is not seen. A project that explicitly declares `IsRoslynComponent=true` defaults to `netstandard2.0` | +| `PurviewTargetFrameworkSet` | `Current` | Named set resolved when the project declares no TFM: `Current`, `Latest`, `Supported`, `Broad` or `All`. An unknown name fails with `PurviewInvalidTargetFrameworkSet`. A single-entry set resolves to `TargetFramework` rather than paying for an outer build. Declare unconditionally | +| `PurviewTargetFrameworksCurrent` | `net10.0;net11.0` | Redefines the `Current` set (the default). Set before the SDK import | +| `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 | +| `PurviewTargetFrameworksAll` | `net8.0;net9.0;net10.0;net11.0` | Redefines the `All` set | | `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` | | `PackProjectReferencedSourceGenerators` | `true` | Packable projects include analyzer `ProjectReference` outputs under `analyzers/dotnet/cs/`. Use `Pack="false"` on one reference to opt that one out | +Overrides are layered, narrowest first: an explicit `TargetFramework`/`TargetFrameworks` in the project +always wins, then a `PurviewTargetFrameworkSet` in the project, then a set the repository selected, then +a `PurviewTargetFrameworks` redefinition, then the SDK default. Roslyn components are not covered — +they target `netstandard2.0`; selecting a set on one is reported as `PurviewTargetFrameworkSetIgnored`. + ## Style policy | Property | Default | Behaviour | @@ -40,7 +51,7 @@ consuming project supplies nothing. | 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` | +| `MemorySafetyRules` | `0` | C# 15 memory-safety tier on `net11.0`+ targets: `0` off (default), `1` preview pointer syntax (sets `LangVersion=preview` when none was chosen), `2` `updated-memory-safety-rules`. Tier `2` is opt-in — the updated rules are an experimental compiler feature. Unknown values fail with `PRCFG0001`. Never touches `AllowUnsafeBlocks` | | `PurviewRuntimeAsync` | `false` | Set to `true` for `runtime-async=on` on `net11.0`+ targets. `UseRuntimeAsync=true` also opts in; an explicit `false` under either name keeps it off | | `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 | @@ -91,8 +102,13 @@ forces SourceLink's CI-mode dirty-repository checks. | -- | -- | -- | | `ExcludePurviewTelemetry` | `false` | Removes `Purview.Telemetry.SourceGenerator` | | `ExcludeMSTelemetryExtension` | `false` | Removes `Microsoft.Extensions.Telemetry.Abstractions`. Only relevant when `ExcludePurviewTelemetry` is also `false` — the whole group is skipped otherwise | -| `SourceLinkPackageName` | `Microsoft.SourceLink.GitHub` | | -| `DisableSourceLink` | `false` | | +| `SourceLinkPackageName` | `Microsoft.SourceLink.GitHub` | The provider package, added with `PrivateAssets=all` to every non-test project. Set before the SDK import to match the host — `Microsoft.SourceLink.AzureRepos.Git` (Azure DevOps Services), `Microsoft.SourceLink.AzureDevOpsServer.Git` (on-premises ADO Server / TFS), `Microsoft.SourceLink.GitLab`, `Microsoft.SourceLink.Bitbucket.Git` | +| `DisableSourceLink` | `false` | `true` (before the import) stops the SDK adding the package at all. An empty `SourceLinkPackageName` does **not** opt out — the GitHub default fills it | +| `EnableSourceLink` | `true` (set by the provider) | The provider package's own switch; `false` keeps the package but skips the source mapping | + +Provider selection is read during `Sdk.props`, so it must be pre-import. See +[sourcelink.md](sourcelink.md) for the host matrix, Azure DevOps specifics, the CPM `PackageVersion` +requirement and self-hosted hosts. ## Blazor WebAssembly, browser-safe libraries and BFF hosts @@ -156,7 +172,18 @@ Covered in full by `purview-buildsdk-skill-installation`: `PurviewSkillsRunner`, ## Compiler-visible properties -Exported to analyzers and generators as `build_property.`: +The SDK lists these as `CompilerVisibleProperty` items, so Roslyn analyzers and source generators read +them as `build_property.`: + +```csharp +context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.IsBffProject", out var isBff); +``` + +Read the exported property rather than re-deriving SDK behaviour — it is the SDK's own answer and stays +correct when the rules change. To make a new property visible, add a `CompilerVisibleProperty` item +(with a ``) in `Sdk.props`; nothing else exposes an MSBuild property to the compiler. + +Exported: - `UsePackageJsonVersion`, `RootPackageJson`, `RepoRoot`, `Version`, `PackageVersion` - `NamespacePrefix`, `DisableNamespacePrefixCheck` diff --git a/skills/purview-buildsdk-configuration/references/sourcelink.md b/skills/purview-buildsdk-configuration/references/sourcelink.md new file mode 100644 index 0000000..062d958 --- /dev/null +++ b/skills/purview-buildsdk-configuration/references/sourcelink.md @@ -0,0 +1,79 @@ +# SourceLink + +Every non-test project gets a SourceLink **provider** package, added with `PrivateAssets="all"`, so a +debugger can step into a consumed package's source and the packed `.nuspec` records the repository URL +and the exact commit (`PublishRepositoryUrl=true`). Test and shared-testing projects are excluded, +because their symbols are never published. + +## Property reference + +| Property | Default | Behaviour | +| -- | -- | -- | +| `SourceLinkPackageName` | `Microsoft.SourceLink.GitHub` | The provider package. Set before the SDK import | +| `DisableSourceLink` | `false` | `true` (before the import) stops the package being added at all | +| `EnableSourceLink` | `true` (set by the provider) | The provider package's own switch; `false` keeps the package but skips the source mapping | + +All three are read during `Sdk.props`, so they must be set **before the SDK import** — in +`Directory.Build.props`, above the ``. A value in the `.csproj` body is too late. An **empty** +`SourceLinkPackageName` does not disable SourceLink: the SDK fills it with the GitHub default. + +## Choosing a provider for the host + +| Host | `SourceLinkPackageName` | Extra configuration | +| -- | -- | -- | +| GitHub / GitHub Enterprise | `Microsoft.SourceLink.GitHub` *(default)* | none | +| Azure DevOps Services / Azure Repos (`dev.azure.com`, `*.visualstudio.com`) | `Microsoft.SourceLink.AzureRepos.Git` | none | +| Azure DevOps Server / TFS (on-premises) | `Microsoft.SourceLink.AzureDevOpsServer.Git` | a `SourceLinkAzureDevOpsServerGitHost` item | +| GitLab (12.0+) | `Microsoft.SourceLink.GitLab` | older GitLab: `SourceLinkGitLabHost` + `Version` | +| Bitbucket Cloud | `Microsoft.SourceLink.Bitbucket.Git` | Bitbucket Server / DC < 4.7: `SourceLinkBitbucketGitHost` + `Version` | +| anything else / self-hosted | *(none built in)* | a third-party or hand-rolled provider, or opt out | + +```xml + + + Microsoft.SourceLink.AzureRepos.Git + +``` + +### Azure DevOps + +- **Azure DevOps Services** — `Microsoft.SourceLink.AzureRepos.Git`, and nothing else. The provider + reads the `origin` remote and derives the content URLs itself. +- **Azure DevOps Server / TFS (on-premises)** — `Microsoft.SourceLink.AzureDevOpsServer.Git` plus the + host, because the virtual directory cannot be inferred from the repository URL: + + ```xml + + + + ``` + + `Include` is the domain, optionally with a port (`tfs.example.com:8080`). A path + (`tfs.example.com/tfs`) fails the provider's own validation ("not a valid domain name"), so the + virtual directory is a separate attribute. +- Azure Pipelines sets `TF_BUILD`, so `ContinuousIntegrationBuild` is on there with no configuration. + +## Central Package Management + +CPM is on, so the chosen provider needs a `PackageVersion` in `Directory.Packages.props`. The shipped +`templates/Directory.Packages.props` declares `Microsoft.SourceLink.GitHub`, +`Microsoft.SourceLink.AzureRepos.Git` and `Microsoft.SourceLink.GitLab` at `$(SourceLinkVersion)`. A +provider that is selected but not declared fails the restore with `NU1010`. + +## Continuous integration + +`ContinuousIntegrationBuild` is set to `true` only when `CI`, `GITHUB_ACTIONS` or `TF_BUILD` is present +(`TF_BUILD` is Azure Pipelines). A local build is deliberately **not** treated as CI, because +SourceLink's CI-mode dirty-repository checks would otherwise fail a local `TreatWarningsAsErrors` build. + +## Opting out, and self-hosted hosts + +`DisableSourceLink=true` (before the import) stops the SDK adding the package. For a host with no +official provider, bring a source-control package (`Microsoft.Build.Tasks.Git`) and a provider package +in yourself and set `DisableSourceLink=true`, so the GitHub default is not added alongside them. + +## Why it matters beyond debugging + +`PublishRepositoryUrl=true` writes `` into the `.nuspec`. Purview's +agent-skill tooling reads that to pin a package to the commit it was built from, and reports a package +with no git identity as `PRSKL0002`. diff --git a/skills/purview-buildsdk-project-detection/SKILL.md b/skills/purview-buildsdk-project-detection/SKILL.md index bd80b3f..9ebc421 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, 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. +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, or why it picked the target framework(s) it did, 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 @@ -93,6 +93,17 @@ dotnet msbuild -t:ResolvePurviewBlazorHostProject -getProperty:IsBlazorHo What each Blazor and BFF classification applies - and the browser-safe library check - is in [references/blazor.md](references/blazor.md). +### Target frameworks + +A project that declares neither `TargetFramework` nor `TargetFrameworks` gets the SDK's default set, +`net10.0;net11.0` (`Current`). A `PurviewTargetFrameworkSet` (`Current`, `Latest`, `Supported`, `Broad`, +`All`) resolves the same way; an unknown name fails with `PurviewInvalidTargetFrameworkSet`. A Roslyn +component keeps `netstandard2.0` and ignores the sets (`PurviewTargetFrameworkSetIgnored`). + +The declaration is read from the **project XML** before the body is evaluated, so a `Condition` on a +`TargetFramework`, `TargetFrameworks` or `PurviewTargetFrameworkSet` is not seen and is reported as +`PurviewConditionedProjectDeclaration`. See `purview-buildsdk-configuration` for how to set them. + ## Namespace and identity 1. `PurviewLogicalProjectName` is `NamespacePrefix` plus the project name, de-duplicated when the @@ -110,7 +121,8 @@ What each Blazor and BFF classification applies - and the browser-safe library c ## Automatic packages and references -- **Non-test C# projects**: SourceLink (unless `DisableSourceLink`), the Purview telemetry packages +- **Non-test C# projects**: a SourceLink provider package (`SourceLinkPackageName`, default + `Microsoft.SourceLink.GitHub`; `DisableSourceLink=true` opts out), the Purview telemetry packages (unless `ExcludePurviewTelemetry`), `GenerateDocumentationFile=true`, and `InternalsVisibleTo` generation (unless `DisableAutoInternalsVisibleTo`). - **Test and shared-testing projects**: test-friendly `NoWarn` defaults, not packable or @@ -127,7 +139,10 @@ Project-reference probing is layout-driven and covered by `purview-buildsdk-proj ## The shipped analyzers The SDK adds `Purview.BuildSdk.Analyzers` as an `` item to every C# project, so its rules -surface in command-line builds and in Visual Studio. The code-fix assembly ships beside it. +surface in command-line builds and in Visual Studio. The code-fix assembly ships beside it. The rules +read the SDK's compiler-visible properties (`build_property.*`) rather than re-deriving project state — +`PDS0008`/`PDS0009`, for example, are gated on `build_property.IsBffProject`. The exported set is in +`purview-buildsdk-configuration`. | Rule | What it enforces | | -- | -- | diff --git a/skills/purview-buildsdk-project-layout/SKILL.md b/skills/purview-buildsdk-project-layout/SKILL.md index 6c19c3d..5550808 100644 --- a/skills/purview-buildsdk-project-layout/SKILL.md +++ b/skills/purview-buildsdk-project-layout/SKILL.md @@ -1,6 +1,6 @@ --- name: purview-buildsdk-project-layout -description: Decide where a project goes and what to call it in a repository that uses Purview.BuildSdk. Use when creating, moving, splitting or renaming a project or test project, choosing a test suffix, aligning namespaces and assembly identity with NamespacePrefix, laying out a Blazor Web App host, WebAssembly client and browser-safe Contracts library, or relying on the SDK's automatic project references. Also use when rationalising a repository's shape so the SDK's conventions work without overrides. +description: Decide where a project goes and what to call it in a repository that uses Purview.BuildSdk. Use when creating, moving, splitting or renaming a project or test project, choosing a test suffix, choosing the target framework(s) for a project, aligning namespaces and assembly identity with NamespacePrefix, laying out a Blazor Web App host, WebAssembly client and browser-safe Contracts library, or relying on the SDK's automatic project references. Also use when rationalising a repository's shape so the SDK's conventions work without overrides. license: MIT metadata: package: Purview.BuildSdk @@ -91,6 +91,15 @@ Shared testing: `SharedTestingFramework`, `SharedTestingInfrastructure`, `Shared `SharedTestingUtilities`, `SharedTestingUtils`, `SharedTestingLibrary`, `SharedTestingLib`, `SharedTestingHelpers`. +## Target frameworks + +A new project should inherit the SDK's default set (`net10.0;net11.0`) rather than pin its own TFMs, so +a lifecycle change is one SDK bump rather than an edit in every project. When a project genuinely needs +a different set, select one with `PurviewTargetFrameworkSet` (`Current`, `Latest`, `Supported`, `Broad`, +`All`) or declare `TargetFramework`/`TargetFrameworks` **unconditionally** — a `Condition` on a TFM is +not seen, because the SDK reads it from the project XML. A Roslyn component targets `netstandard2.0`. +See `purview-buildsdk-configuration`. + ## Automatic project references to preserve Layout is what makes these work. Move a project off the convention and you own the reference. diff --git a/skills/purview-buildsdk-repository-review/SKILL.md b/skills/purview-buildsdk-repository-review/SKILL.md index 2be4c08..6fee41c 100644 --- a/skills/purview-buildsdk-repository-review/SKILL.md +++ b/skills/purview-buildsdk-repository-review/SKILL.md @@ -36,6 +36,7 @@ Inspect, in this order, before forming any opinion: - `Directory.Build.props` and `Directory.Build.targets` — where the SDK is imported, and what is set before the import +- `global.json` (the pinned SDK version) and repo `package.json` (the product version) - the solution entry point, for what is actually in the build - `Directory.Packages.props`, for Central Package Management - the project layout on disk @@ -53,6 +54,11 @@ Inspect, in this order, before forming any opinion: suffix outside it still runs, it just cannot see `internal` members. - Shared and shared-testing projects use the **exact** recognised names. A near miss gets none of the behaviour. +- Target frameworks: a project should inherit the SDK's default set (`net10.0;net11.0`) or select a + curated `PurviewTargetFrameworkSet` (`Current`, `Latest`, `Supported`, `Broad`, `All`), not hard-code + a TFM list. A hard-coded list is an incremental opportunity; a `Condition`-guarded + `TargetFramework`/`TargetFrameworks` is structural debt, because the SDK reads the declaration from + the project XML and cannot evaluate the condition (`PurviewConditionedProjectDeclaration`). ## 3. Verify with MSBuild, do not infer from folder names diff --git a/skills/purview-buildsdk-testing/SKILL.md b/skills/purview-buildsdk-testing/SKILL.md index 5a5a7d5..0ac749b 100644 --- a/skills/purview-buildsdk-testing/SKILL.md +++ b/skills/purview-buildsdk-testing/SKILL.md @@ -1,6 +1,6 @@ --- name: purview-buildsdk-testing -description: Write and run tests in a repository that uses Purview.BuildSdk. Use when authoring or refactoring TUnit tests (AAA structure, Subject_Scenario_Expectation naming, awaited assertions, CancellationToken), when choosing a test project's framework packages, and when running or filtering tests - the SDK wires Microsoft.Testing.Platform, so `dotnet test --filter` reports "Zero tests ran" and `--treenode-filter` is the one that works. +description: Write and run tests in a repository that uses Purview.BuildSdk. Use when authoring or refactoring TUnit tests (AAA structure, Subject_Scenario_Expectation naming, awaited assertions, CancellationToken), when choosing a test project's framework packages or matching its target framework to its subject, and when running or filtering tests - the SDK wires Microsoft.Testing.Platform, so `dotnet test --filter` reports "Zero tests ran" and `--treenode-filter` is the one that works. license: MIT metadata: package: Purview.BuildSdk @@ -27,6 +27,11 @@ With the defaults (`TestingFramework=TUnit`, `SubstituteFramework=TUnitMocks`, contract — `IDE0040`, field naming, formatting, `IDE1006` - `IsPackable=false`, `IsPublishable=false` +A test project inherits the SDK's default target framework set (`net10.0;net11.0`). When it must match +its subject — a project still on one TFM, or a subject that multi-targets — set +`TargetFramework`/`TargetFrameworks` on the **test** project, declared unconditionally, or select a +`PurviewTargetFrameworkSet`. See `purview-buildsdk-configuration`. + Add nothing else unless the project's purpose demands it. Two that stay explicit: - `TUnit.Aspire` — Aspire lifecycle and AppHost-backed integration tests diff --git a/src/src/BuildSdk/Sdk/Props/Defaults.props b/src/src/BuildSdk/Sdk/Props/Defaults.props index 931fe8b..41c37ad 100644 --- a/src/src/BuildSdk/Sdk/Props/Defaults.props +++ b/src/src/BuildSdk/Sdk/Props/Defaults.props @@ -159,20 +159,21 @@ true - 2 + 0 enable diff --git a/src/src/BuildSdk/Sdk/Sdk.props b/src/src/BuildSdk/Sdk/Sdk.props index 9f1e380..5e87453 100644 --- a/src/src/BuildSdk/Sdk/Sdk.props +++ b/src/src/BuildSdk/Sdk/Sdk.props @@ -561,7 +561,7 @@ 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. + The C# 15 memory-safety tier: 0 off (default), 1 preview syntax, 2 updated rules. The updated rules are an experimental compiler feature, so they are opt-in. AllowUnsafeBlocks is left unset. When true, a net11.0+ project compiles with the runtime-async compiler feature. Defaults to false; UseRuntimeAsync=true also opts in. @@ -868,7 +868,10 @@ Microsoft.SourceLink.GitHub - false + false false false diff --git a/src/src/BuildSdk/Sdk/Targets/Modern.targets b/src/src/BuildSdk/Sdk/Targets/Modern.targets index c84ef10..86ac5ec 100644 --- a/src/src/BuildSdk/Sdk/Targets/Modern.targets +++ b/src/src/BuildSdk/Sdk/Targets/Modern.targets @@ -7,19 +7,20 @@ property. That matters: TargetFrameworks defaults to net10.0;net11.0, so a switch that improves net11.0 must not be applied to the net10.0 leg. - One compiler feature is enabled by default for a net11.0+ target, and - a second is available as an opt-in: + Both compiler features are opt-in on a net11.0+ target; neither is + assumed: runtime-async=on Runtime Async V2. Off by default: it changes how every await compiles, so it is opted into with PurviewRuntimeAsync=true (or UseRuntimeAsync=true), never assumed. - updated-memory-safety-rules (default) + updated-memory-safety-rules The C# 15 memory-safety model: 'unsafe' marks requires-unsafe APIs and the caller - must acknowledge the access. Independent of - LangVersion, so 'latest' is enough; the + must acknowledge the access. Off by default, because it is an + experimental feature switch: opt in with MemorySafetyRules=2. + Independent of LangVersion, so 'latest' is enough; the preview *syntax* is a separate tier (1). Neither switch requires AllowUnsafeBlocks. It is deliberately left at @@ -48,7 +49,7 @@ $(UseRuntimeAsync) false - 2 + 0 <_PurviewMemorySafetyRulesNormalized>$([System.String]::Copy('$(MemorySafetyRules)').Trim().ToLowerInvariant()) <_PurviewMemorySafetyRulesNormalized diff --git a/src/tests/BuildSdk.IntegrationTests/Dotnet11FeaturesTests.cs b/src/tests/BuildSdk.IntegrationTests/Dotnet11FeaturesTests.cs index cdf294b..be2c7e9 100644 --- a/src/tests/BuildSdk.IntegrationTests/Dotnet11FeaturesTests.cs +++ b/src/tests/BuildSdk.IntegrationTests/Dotnet11FeaturesTests.cs @@ -3,8 +3,8 @@ 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. +/// Pins the .NET 11 compiler feature switches the SDK makes available to a net11.0 (or later) +/// target: Runtime Async and the C# 15 updated memory-safety rules. Both are opt-in and off by default. /// /// /// The default target set is net10.0;net11.0, so the switches have to be per-TFM: applying @@ -15,7 +15,7 @@ namespace Purview.BuildSdk; sealed class Dotnet11FeaturesTests { [Test] - public async Task Net11Target_EnablesUpdatedMemorySafetyRulesButNotRuntimeAsync(CancellationToken cancellationToken) + public async Task Net11Target_LeavesBothCompilerFeaturesOffByDefault(CancellationToken cancellationToken) { using var h = await ProjectHarness.CreateAsync( "MyLibrary", @@ -34,9 +34,9 @@ await Assert .Because("Runtime Async is opt-in: it changes how every await compiles"); 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"); + .DoesNotContain("updated-memory-safety-rules") + .Because("the updated memory-safety rules are an experimental compiler feature and opt-in"); + await Assert.That(eval.Properties["MemorySafetyRules"]).IsEqualTo("0"); await Assert .That(eval.Properties["AllowUnsafeBlocks"]) .IsEqualTo("false") @@ -68,7 +68,7 @@ public async Task Net11Target_KeepsFeaturesTheProjectAlreadySet(CancellationToke using var h = await ProjectHarness.CreateAsync( "MyLibrary", targetFramework: "net11.0", - extraProps: "$(Features);project-feature=on", + extraProps: "2$(Features);project-feature=on", cancellationToken: cancellationToken ); @@ -78,6 +78,29 @@ public async Task Net11Target_KeepsFeaturesTheProjectAlreadySet(CancellationToke await Assert.That(features).Contains("updated-memory-safety-rules"); } + /// + /// The updated memory-safety rules are an experimental compiler feature, so tier 2 is an + /// explicit opt-in rather than the default. + /// + [Test] + public async Task MemorySafetyTierTwo_EnablesTheUpdatedRules(CancellationToken cancellationToken) + { + using var h = await ProjectHarness.CreateAsync( + "MyLibrary", + targetFramework: "net11.0", + extraProps: "2", + cancellationToken: cancellationToken + ); + + var eval = await h.EvaluateAsync(["Features", "LangVersion"], cancellationToken: cancellationToken); + + await Assert.That(eval.Properties["Features"]).Contains("updated-memory-safety-rules"); + await Assert + .That(eval.Properties["LangVersion"]) + .IsEqualTo("latest") + .Because("tier 2 is an independent feature switch, not the preview language version"); + } + [Test] public async Task RuntimeAsync_IsOptedInByPurviewRuntimeAsync(CancellationToken cancellationToken) { @@ -91,7 +114,6 @@ public async Task RuntimeAsync_IsOptedInByPurviewRuntimeAsync(CancellationToken var features = await h.GetPropertyAsync("Features", cancellationToken); await Assert.That(features).Contains("runtime-async=on"); - await Assert.That(features).Contains("updated-memory-safety-rules"); } /// diff --git a/src/tests/BuildSdk.IntegrationTests/RepositorySkillsSelfTests.cs b/src/tests/BuildSdk.IntegrationTests/RepositorySkillsSelfTests.cs index a3e7ade..2a2ea51 100644 --- a/src/tests/BuildSdk.IntegrationTests/RepositorySkillsSelfTests.cs +++ b/src/tests/BuildSdk.IntegrationTests/RepositorySkillsSelfTests.cs @@ -17,10 +17,12 @@ namespace Purview.BuildSdk; /// /// /// .agents/skills/ is the skills CLI's universal install location, holding skills installed -/// from other sources. It is gitignored, reproducible from skills-lock.json, and it is -/// not where this repository's own skills belong - installing them into it would make this +/// from other sources - reproducible from skills-lock.json and gitignored. It is not +/// where this repository's published skills belong: installing those here would make this /// repository a consumer of advice it wrote for somebody else, and would put a second writer on -/// paths the CLI owns. +/// paths the CLI owns. The one thing tracked there is a repository-local skill - guidance +/// about working in this repository, which must never ship to a consumer - which the agent runtime +/// reads natively. /// /// /// @@ -154,7 +156,7 @@ public async Task InstalledSkillsAreNotTracked() // Everything the CLI writes is reproducible from skills-lock.json, so none of it belongs in // the repository. '.agents/skills/' is the one that most easily ends up committed by // accident, because the rest of '.agents/' is authored content and stays tracked. - foreach (var root in (string[])[".agents", ".claude", "agent"]) + foreach (var root in (string[])[".claude", "agent"]) { var skillsPath = Path.Combine(RepositoryRoot, root, "skills"); if (!Directory.Exists(skillsPath)) @@ -166,6 +168,26 @@ await Assert .IsEmpty() .Because($"'{root}/skills' is written by the skills CLI and must stay gitignored"); } + + // '.agents/skills/' is different in one respect: it is also the one location an agent reads + // natively here, so a repository-local skill - guidance about working in this repository, + // which must never ship to a consumer - is authored in place and tracked. What must never be + // tracked is a skill the CLI *installed*, and skills-lock.json is exactly the record of those. + var installed = ReadLockFile().Select(entry => entry.Name).ToHashSet(StringComparer.Ordinal); + + var trackedCliInstalls = (await TrackedFilesUnderAsync(".agents/skills")) + .Where(path => + { + // '.agents/skills//...' - the third segment is the skill folder. + var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries); + return segments.Length >= 3 && installed.Contains(segments[2]); + }) + .ToArray(); + + await Assert + .That(trackedCliInstalls) + .IsEmpty() + .Because("a skill the skills CLI installed is reproducible from skills-lock.json and must stay gitignored"); } [Test] diff --git a/src/tests/BuildSdk.IntegrationTests/SourceLinkTests.cs b/src/tests/BuildSdk.IntegrationTests/SourceLinkTests.cs new file mode 100644 index 0000000..123639f --- /dev/null +++ b/src/tests/BuildSdk.IntegrationTests/SourceLinkTests.cs @@ -0,0 +1,107 @@ +using Purview.BuildSdk.Harness; + +namespace Purview.BuildSdk; + +/// +/// Pins how the SDK wires SourceLink: the default provider, that a repository can select the provider +/// for its own host (Azure DevOps, GitLab, Bitbucket, ...), and that the documented opt-out really +/// removes the package. +/// +/// +/// SourceLinkPackageName and DisableSourceLink are read during Sdk.props, so a +/// repository must set them before the SDK import — these tests use preImportProps for that +/// reason. They evaluate rather than build: the harness declares only +/// Microsoft.SourceLink.GitHub centrally, so a selected provider other than GitHub would fail a +/// restore with NU1010, which is not what is under test here. +/// +sealed class SourceLinkTests +{ + const string SourceLinkPrefix = "Microsoft.SourceLink."; + + static string[] SourceLinkReferences(IReadOnlyList packageReferences) => + [.. packageReferences.Where(reference => reference.StartsWith(SourceLinkPrefix, StringComparison.Ordinal))]; + + [Test] + public async Task NonTestProject_GetsTheGitHubProviderByDefault(CancellationToken cancellationToken) + { + using var h = await ProjectHarness.CreateAsync("MyLibrary", cancellationToken: cancellationToken); + + var eval = await h.EvaluateAsync( + ["DisableSourceLink", "SourceLinkPackageName"], + ["PackageReference"], + cancellationToken + ); + + await Assert + .That(eval.Properties["SourceLinkPackageName"]) + .IsEqualTo("Microsoft.SourceLink.GitHub") + .Because("GitHub is the sensible default provider"); + await Assert.That(eval.Properties["DisableSourceLink"]).IsEqualTo("false"); + await Assert.That(SourceLinkReferences(eval.Items["PackageReference"])).Contains("Microsoft.SourceLink.GitHub"); + } + + /// + /// A repository on Azure DevOps, GitLab, Bitbucket or an on-premises server sets + /// SourceLinkPackageName before the SDK import; it replaces the GitHub default rather than + /// joining it. + /// + [Test] + public async Task SelectingAProvider_ReplacesTheGitHubDefault(CancellationToken cancellationToken) + { + using var h = await ProjectHarness.CreateAsync( + "MyLibrary", + preImportProps: "Microsoft.SourceLink.AzureRepos.Git", + cancellationToken: cancellationToken + ); + + var references = SourceLinkReferences(await h.GetItemIdentitiesAsync("PackageReference", cancellationToken)); + + await Assert.That(references).Contains("Microsoft.SourceLink.AzureRepos.Git"); + await Assert.That(references).DoesNotContain("Microsoft.SourceLink.GitHub"); + } + + [Test] + public async Task TestProject_DoesNotGetAnAutomaticSourceLinkReference(CancellationToken cancellationToken) + { + using var h = await ProjectHarness.CreateAsync("MyLibrary.UnitTests", cancellationToken: cancellationToken); + + var references = SourceLinkReferences(await h.GetItemIdentitiesAsync("PackageReference", cancellationToken)); + + await Assert + .That(references) + .IsEmpty() + .Because("test projects are excluded from the automatic SourceLink package"); + } + + [Test] + public async Task SharedTestingProject_DoesNotGetAnAutomaticSourceLinkReference(CancellationToken cancellationToken) + { + using var h = await ProjectHarness.CreateAsync("SharedTestingFramework", cancellationToken: cancellationToken); + + var references = SourceLinkReferences(await h.GetItemIdentitiesAsync("PackageReference", cancellationToken)); + + await Assert + .That(references) + .IsEmpty() + .Because("shared-testing projects are excluded from the automatic SourceLink package"); + } + + /// + /// The documented opt-out. It only works when set before the SDK import — a value in the project + /// body is evaluated after the package ItemGroup and has no effect. + /// + [Test] + public async Task DisableSourceLink_RemovesTheAutomaticReference(CancellationToken cancellationToken) + { + using var h = await ProjectHarness.CreateAsync( + "MyLibrary", + preImportProps: "true", + cancellationToken: cancellationToken + ); + + var eval = await h.EvaluateAsync(["DisableSourceLink"], ["PackageReference"], cancellationToken); + + await Assert.That(eval.Properties["DisableSourceLink"]).IsEqualTo("true"); + await Assert.That(SourceLinkReferences(eval.Items["PackageReference"])).IsEmpty(); + } +}