diff --git a/README.md b/README.md index ec0fee8..6574791 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,7 @@ See [Project Setup & Infrastructure](docs/project-setup.md#local-development-loc | Component | Purpose | |-----------|---------| | [**Scribe SDK**](docs/project-setup.md#scribe-sdk-recommended) | MSBuild SDK — zero-config project setup for analyzer/generator projects | +| [**Solution-Local Analyzers**](docs/solution-local-analyzers.md) | Auto-pack intra-solution analyzers — no NuGet publishing needed | | [**Quill**](docs/quill-reference.md) | Fluent source builder — indentation, usings, namespaces, XML docs, type resolution | | **ScribeHeader** | Assembly-level attribute for branding generated files with a decorative page header | | **Template** | Minimal `{{key}}` marker substitution for structural shells | @@ -194,6 +195,7 @@ See [Project Setup & Infrastructure](docs/project-setup.md#local-development-loc |-------|---------------| | [Writing Generators with Quill](docs/writing-generators.md) | How to structure a generator using the Transform → Register → Render pattern | | [Project Setup & Infrastructure](docs/project-setup.md) | .csproj configuration, packaging, LocalDev multi-repo workflow | +| [Solution-Local Analyzers](docs/solution-local-analyzers.md) | Setup guide for intra-solution analyzers and generators | | [Quill Feature Reference](docs/quill-reference.md) | Complete API reference for every Quill method and scope type | | [How Quill Works](docs/architecture-quill.md) | Internal architecture — type resolution, indentation, XML doc insertion | | [How the Infrastructure Works](docs/architecture-infrastructure.md) | Build system internals — LocalDev props/targets, override file generation | diff --git a/Scribe.Sdk/README.md b/Scribe.Sdk/README.md index ddc482a..507a9a7 100644 --- a/Scribe.Sdk/README.md +++ b/Scribe.Sdk/README.md @@ -80,6 +80,24 @@ All polyfills are guarded by `#if !NET5_0_OR_GREATER` and deactivate on modern t The SDK includes [LocalDev](https://github.com/BulletsForHumanity/Scribe/blob/master/docs/project-setup.md#local-development-localdev) infrastructure for multi-repo development. See the main Scribe documentation for setup instructions. +## Solution-Local Analyzers + +For analyzers that live inside the same solution they serve, set `ScribeSolutionAnalyzer=true` to auto-pack on build with automatic version management: + +```xml + + + true + + + + + + +``` + +Each build produces a unique timestamp version and a version override file, using the same mechanism as [LocalDev](https://github.com/BulletsForHumanity/Scribe/blob/master/docs/project-setup.md#local-development-localdev) but scoped to the solution. See [Solution-Local Analyzers](https://github.com/BulletsForHumanity/Scribe/blob/master/docs/solution-local-analyzers.md) for the complete setup guide. + ## Links - [Scribe Documentation](https://github.com/BulletsForHumanity/Scribe) diff --git a/Scribe.Sdk/Scribe.Sdk.csproj b/Scribe.Sdk/Scribe.Sdk.csproj index 89a7dd6..741a423 100644 --- a/Scribe.Sdk/Scribe.Sdk.csproj +++ b/Scribe.Sdk/Scribe.Sdk.csproj @@ -3,7 +3,8 @@ BulletsForHumanity.Scribe.Sdk — MSBuild SDK for Roslyn analyzer/generator projects. This is a pack-only project. It produces no build output — the NuGet package - contains only MSBuild props/targets, polyfill stubs, and LocalDev infrastructure. + contains only MSBuild props/targets, polyfill stubs, Solution-Local Analyzer + infrastructure, and LocalDev infrastructure. --> BulletsForHumanity.Scribe.Sdk @@ -18,7 +19,7 @@ $(NoWarn);NU5128;NU5129;NETSDK1212 true - MSBuild SDK for Roslyn analyzer and source generator projects. Provides default project configuration, analyzer packaging targets, netstandard2.0 polyfill stubs, and LocalDev multi-repo development infrastructure. Use with: <Project Sdk="BulletsForHumanity.Scribe.Sdk"> + MSBuild SDK for Roslyn analyzer and source generator projects. Provides default project configuration, analyzer packaging targets, netstandard2.0 polyfill stubs, solution-local analyzer auto-pack support, and LocalDev multi-repo development infrastructure. Use with: <Project Sdk="BulletsForHumanity.Scribe.Sdk"> roslyn;analyzer;generator;source-generator;msbuild-sdk;scribe @@ -38,6 +39,12 @@ + + + + + + diff --git a/Scribe.Sdk/Sdk/Sdk.props b/Scribe.Sdk/Sdk/Sdk.props index 6c231cb..8471c46 100644 --- a/Scribe.Sdk/Sdk/Sdk.props +++ b/Scribe.Sdk/Sdk/Sdk.props @@ -56,6 +56,9 @@ true + + + diff --git a/Scribe.Sdk/Sdk/Sdk.targets b/Scribe.Sdk/Sdk/Sdk.targets index 9e0f681..3832a11 100644 --- a/Scribe.Sdk/Sdk/Sdk.targets +++ b/Scribe.Sdk/Sdk/Sdk.targets @@ -53,6 +53,9 @@ + + + diff --git a/Scribe/build/Scribe.SolutionAnalyzer.props b/Scribe/build/Scribe.SolutionAnalyzer.props new file mode 100644 index 0000000..5f7b01e --- /dev/null +++ b/Scribe/build/Scribe.SolutionAnalyzer.props @@ -0,0 +1,99 @@ + + + + + + + <_ScribeSaTimestamp>$([System.DateTime]::UtcNow.ToString('yyyyMMddHHmmss')) + 0.0.0-dev.$(_ScribeSaTimestamp) + 0.0.0-dev.$(_ScribeSaTimestamp) + + + true + true + + + false + + + + + + <_ScribeSaArtifactsDir>$([MSBuild]::EnsureTrailingSlash('$(ArtifactsPath)')) + + + + + + $(_ScribeSaArtifactsDir)packages\ + + + + + + + $(RestoreAdditionalProjectSources);$(_ScribeSaArtifactsDir)packages + + + diff --git a/Scribe/build/Scribe.SolutionAnalyzer.targets b/Scribe/build/Scribe.SolutionAnalyzer.targets new file mode 100644 index 0000000..8e83133 --- /dev/null +++ b/Scribe/build/Scribe.SolutionAnalyzer.targets @@ -0,0 +1,58 @@ + + + + + + + + <_ScribeSaVersion>$(NuGetPackageVersion) + <_ScribeSaVersion Condition="'$(_ScribeSaVersion)' == ''">$(PackageVersion) + <_ScribeSaArtifactsDir>$([MSBuild]::EnsureTrailingSlash('$(ArtifactsPath)')) + <_ScribeSaOverridePath>$(_ScribeSaArtifactsDir)$(PackageId).Directory.Packages.targets + + + + <_ScribeSaLine Include="<Project>" /> + <_ScribeSaLine Include=" <!-- Auto-generated by Scribe SolutionAnalyzer — do not edit. Rebuild the analyzer to refresh. -->" /> + <_ScribeSaLine Include=" <ItemGroup>" /> + <_ScribeSaLine Include=" <PackageVersion Update="$(PackageId)" Version="$(_ScribeSaVersion)" />" /> + <_ScribeSaLine Include=" </ItemGroup>" /> + <_ScribeSaLine Include="</Project>" /> + + + + + + + diff --git a/docs/architecture-infrastructure.md b/docs/architecture-infrastructure.md index 29f75ab..098f690 100644 --- a/docs/architecture-infrastructure.md +++ b/docs/architecture-infrastructure.md @@ -1,6 +1,6 @@ # How the Infrastructure Works -Internal architecture of Scribe's build infrastructure — the Scribe SDK and LocalDev system. Read this if you're contributing to Scribe or want to understand the MSBuild mechanics. +Internal architecture of Scribe's build infrastructure — the Scribe SDK, Solution-Local Analyzer, and LocalDev systems. Read this if you're contributing to Scribe or want to understand the MSBuild mechanics. For setup instructions, see [Project Setup & Infrastructure](project-setup.md). @@ -20,6 +20,8 @@ BulletsForHumanity.Scribe.Sdk.nupkg build/ Scribe.LocalDev.props <- Shared LocalDev infrastructure (early phase) Scribe.LocalDev.targets <- Shared LocalDev infrastructure (late phase) + Scribe.SolutionAnalyzer.props <- Solution-local analyzer support (early phase) + Scribe.SolutionAnalyzer.targets <- Solution-local analyzer support (late phase) content/ Stubs.cs <- netstandard2.0 polyfills, injected as Compile item ``` @@ -43,7 +45,8 @@ Runs before the project file is evaluated. Sets overridable defaults: 1. **Chains to `Microsoft.NET.Sdk`** via `` 2. **Sets analyzer project defaults**: `TargetFramework=netstandard2.0`, `LangVersion=14`, `EnforceExtendedAnalyzerRules=true`, `IncludeBuildOutput=false`, `PackageType=Analyzer`, embedded PDB, nullable enabled 3. **Initialises `ScribeSdkIncludeStubs`** to `true` (opt-out via `false`) -4. **Imports `Scribe.LocalDev.props`** for sentinel file detection and local NuGet source registration +4. **Imports `Scribe.SolutionAnalyzer.props`** for solution-local analyzer auto-pack configuration +5. **Imports `Scribe.LocalDev.props`** for sentinel file detection and local NuGet source registration All properties can be overridden by the consuming `.csproj` because they're set in the early phase. @@ -56,7 +59,67 @@ Runs after the project file. Enforces packaging behaviour: 3. **`_ScribeSdkAddAnalyzerDlls` target**: Places the analyzer DLL into `analyzers/dotnet/cs/` in the NuGet package 4. **`_ScribeSdkAddAnalyzerDependencies` target**: Bundles private NuGet dependencies alongside the analyzer DLL. Excludes `Microsoft.CodeAnalysis.*` DLLs (provided by the compiler host). 5. **Sets `CopyLocalLockFileAssemblies=true`** to enable dependency bundling -6. **Imports `Scribe.LocalDev.targets`** for version override wildcard import +6. **Imports `Scribe.SolutionAnalyzer.targets`** for solution-local override file generation +7. **Imports `Scribe.LocalDev.targets`** for version override wildcard import + +--- + +## Solution-Local Analyzer + +The Solution-Local Analyzer infrastructure enables intra-solution analyzer development — analyzers that live inside the same solution as the projects they serve, without publishing to NuGet. It uses the **same mechanism** as LocalDev (version override files + NuGet source registration) but scoped to a single solution. + +For setup instructions, see [Solution-Local Analyzers](solution-local-analyzers.md). + +### File Layout + +``` +Scribe/build/ + Scribe.SolutionAnalyzer.props <- Configuration (early phase) + Scribe.SolutionAnalyzer.targets <- Override file generation (late phase) +``` + +### Props Phase (Early) + +`Scribe.SolutionAnalyzer.props` runs when `ScribeSolutionAnalyzer=true`: + +1. **Generates a timestamp version** `0.0.0-dev.yyyyMMddHHmmss` — forces Visual Studio to reload analyzers on every build +2. **Disables NBGV** (`NerdbankGitVersioningEnabled=false`) — version is managed by the SDK +3. **Enables auto-pack** via `GeneratePackageOnBuild=true` +4. **Normalises `$(ArtifactsPath)`** to a guaranteed trailing separator (`_ScribeSaArtifactsDir`) for safe path concatenation +5. **Redirects pack output** to `$(ArtifactsPath)packages/` +6. **Registers as NuGet source** via `RestoreAdditionalProjectSources` (for the analyzer project itself) + +### Targets Phase (Late) + +`Scribe.SolutionAnalyzer.targets` defines one target: + +**`_ScribeSolutionAnalyzerOverride`** — Runs after Pack. Reads `$(NuGetPackageVersion)` (falling back to `$(PackageVersion)`) and generates a `.Directory.Packages.targets` file in `$(ArtifactsPath)` containing a `PackageVersion Update` entry: + +```xml + + + + + + +``` + +Consuming projects import this file via wildcard from `$(ArtifactsPath)` in their `Directory.Build.targets`. This is the same pattern as LocalDev's `_ScribeLocalDevOverride` target. + +### Relationship to LocalDev + +Solution-Local Analyzer and LocalDev use the same underlying mechanism but at different scopes: + +| Concern | LocalDev | Solution-Local Analyzer | +|---------|----------|------------------------| +| Scope | Cross-repo | Intra-solution | +| Trigger | `.localscribe` sentinel | `ScribeSolutionAnalyzer=true` | +| Version | NBGV + timestamp suffix | Timestamp-only (`0.0.0-dev.yyyyMMddHHmmss`) | +| Override files | Generated `.Directory.Packages.targets` | Same — generated `.Directory.Packages.targets` | +| Package directory | Shared `$(ScribeRoot)/.artifacts/packages/` | Solution `$(ArtifactsPath)packages/` | +| Override file location | `$(ScribeRoot)/.artifacts/` | `$(ArtifactsPath)` | + +Both features are independent and can coexist. A solution can have solution-local analyzers and also participate in a LocalDev chain. --- diff --git a/docs/project-setup.md b/docs/project-setup.md index 1287950..0b23244 100644 --- a/docs/project-setup.md +++ b/docs/project-setup.md @@ -143,6 +143,34 @@ For test projects that instantiate generators directly via `CSharpGeneratorDrive --- +## Solution-Local Analyzers + +If your analyzer or generator lives inside the same solution it serves (and is not published to NuGet), the Scribe SDK can auto-pack it on build and make it available to other projects in the solution via standard `PackageReference`. + +Set `ScribeSolutionAnalyzer=true` in your analyzer `.csproj`: + +```xml + + + true + + + + + + +``` + +Consuming projects reference it like any NuGet package (the version is managed automatically via an override file): + +```xml + +``` + +See [Solution-Local Analyzers](solution-local-analyzers.md) for the complete setup guide, including consumer-side `Directory.Build.props` and `.targets` configuration. + +--- + ## Packaging ### Placing analyzer DLLs diff --git a/docs/solution-local-analyzers.md b/docs/solution-local-analyzers.md new file mode 100644 index 0000000..84986da --- /dev/null +++ b/docs/solution-local-analyzers.md @@ -0,0 +1,260 @@ +# Solution-Local Analyzers + +How to set up Roslyn analyzers and source generators that live inside the same solution they serve — no NuGet publishing, no cross-repo setup. + +--- + +## Overview + +A **solution-local analyzer** is a Roslyn analyzer or source generator project that exists inside the same solution as the projects it serves. It is not published to NuGet. It is not consumed cross-repo. It automates *this* codebase and nothing else. + +Examples: + +- A generator that reads domain model types and emits mapping code, DTOs, or validation rules +- An analyzer that enforces solution-specific coding conventions +- A generator that emits strongly-typed configuration accessors from appsettings schema + +The Scribe SDK provides first-class support for this workflow. When `ScribeSolutionAnalyzer=true` is set on a Scribe SDK project, the SDK automatically: + +1. **Packs the analyzer on every build** — produces a `.nupkg` in the solution's `artifacts/packages/` directory +2. **Generates a unique timestamp version** (`0.0.0-dev.yyyyMMddHHmmss`) — forces Visual Studio to reload the analyzer on every build +3. **Generates a version override file** — consuming projects automatically resolve the locally-built package via the same mechanism as [LocalDev](project-setup.md#local-development-localdev) +4. **Bundles private dependencies** — the same dependency bundling as any Scribe SDK project + +Consuming projects reference the analyzer via standard ``, getting all the same analyzer wiring that any published package gets. + +--- + +## Quick Start + +### 1. Create the analyzer project + +```xml + + + true + + + + + + +``` + +### 2. Configure the solution for local packages + +The solution must use artifact output. In `Directory.Build.props`: + +```xml + + true + $(MSBuildThisFileDirectory).artifacts/ + + + + + + $(RestoreAdditionalProjectSources);$(ArtifactsPath)packages + + +``` + +In `Directory.Build.targets`, import override files generated by solution-local analyzers: + +```xml + +``` + +### 3. Reference from consuming projects + +Add the analyzer to your `Directory.Packages.props` (any version — the override file pins the actual version): + +```xml + + +``` + +Then reference it from consuming projects: + +```xml + + + +``` + +### 4. Build + +```shell +dotnet build MySolution.sln +``` + +The analyzer auto-packs on build. The version override file is generated automatically. Consuming projects resolve the package from the artifacts directory. Diagnostics and generated code appear automatically. + +--- + +## How It Works + +### Auto-Pack on Build + +When `ScribeSolutionAnalyzer=true`, the SDK sets `GeneratePackageOnBuild=true` and redirects `PackageOutputPath` to `$(ArtifactsPath)packages/`. Every build produces a fresh `.nupkg`. + +### Timestamp Version + +Each build produces a unique version: `0.0.0-dev.yyyyMMddHHmmss` (e.g. `0.0.0-dev.20260406054112`). This serves two purposes: + +1. **Forces Visual Studio to reload** — VS detects a new analyzer version and reloads the analyzer assembly, picking up changes immediately +2. **Eliminates NuGet cache issues** — each version is unique, so NuGet never serves stale cached content + +### Version Override File + +After each pack, the SDK generates a `.Directory.Packages.targets` file in the artifacts directory: + +```xml + + + + + + +``` + +This is the same mechanism as [LocalDev](project-setup.md#local-development-localdev). Consuming projects import the file via wildcard from `$(ArtifactsPath)`, which updates the `PackageVersion` to match the freshly-built package. + +### Package Directory + +Packages are placed in `$(ArtifactsPath)packages/` — the standard artifacts packages directory. This requires the solution to use artifact output (`UseArtifactsOutput=true`), which is the recommended setup for modern .NET projects. + +--- + +## Solution Layout + +A typical solution with a solution-local analyzer: + +``` +MySolution/ + MySolution.sln + Directory.Build.props ← UseArtifactsOutput, RestoreAdditionalProjectSources + Directory.Build.targets ← Import override files from .artifacts/ + Directory.Packages.props ← PackageVersion Include for MyAnalyzer + .gitignore ← includes .artifacts/ + .artifacts/ ← auto-generated, git-ignored + packages/ + MyAnalyzer.0.0.0-dev.20260406054112.nupkg + MyAnalyzer.Directory.Packages.targets ← version override file + MyAnalyzer/ + MyAnalyzer.csproj ← ScribeSolutionAnalyzer=true + MyGenerator.cs + MyApp/ + MyApp.csproj ← + Program.cs +``` + +--- + +## Multiple Analyzers + +A solution can have multiple solution-local analyzers. Each generates its own override file in the artifacts directory: + +```xml + + + + true + + ... + +``` + +```xml + + + + true + + ... + +``` + +The wildcard import in `Directory.Build.targets` picks up all override files: + +``` +.artifacts/ + FirstAnalyzer.Directory.Packages.targets + SecondAnalyzer.Directory.Packages.targets + packages/ + FirstAnalyzer.0.0.0-dev.20260406054112.nupkg + SecondAnalyzer.0.0.0-dev.20260406054112.nupkg +``` + +--- + +## Relationship to LocalDev + +Solution-local analyzers use the **same mechanism** as LocalDev (version override files + NuGet source registration) but scoped to a single solution: + +| Concern | LocalDev | Solution-Local Analyzer | +|---------|----------|------------------------| +| Scope | Cross-repo (e.g. Scribe → Hermetic → MyApp) | Intra-solution | +| Trigger | `.localscribe` sentinel file | `ScribeSolutionAnalyzer=true` property | +| Published? | Yes (eventually to NuGet) | Never | +| Version management | NBGV + timestamp + override files | Timestamp-only + override files | +| Package directory | Shared `$(ScribeRoot)/.artifacts/packages/` | Solution `$(ArtifactsPath)packages/` | +| Override file location | `$(ScribeRoot)/.artifacts/` | `$(ArtifactsPath)` | +| Setup complexity | `$(ScribeRoot)`, trigger project, package names | One property + consumer-side imports | + +Both features are independent and can coexist. A solution can have solution-local analyzers and also participate in a LocalDev chain. + +--- + +## Provided Properties + +| Property | Value | Set by | +|----------|-------|--------| +| `$(ScribeSolutionAnalyzer)` | `true` | Developer (in `.csproj`) | +| `$(Version)` | `0.0.0-dev.yyyyMMddHHmmss` | SDK (timestamp-based) | +| `$(PackageVersion)` | `0.0.0-dev.yyyyMMddHHmmss` | SDK (same as Version) | +| `$(GeneratePackageOnBuild)` | `true` | SDK | +| `$(PackageOutputPath)` | `$(ArtifactsPath)packages\` | SDK | + +--- + +## Design Decisions + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| Detection mechanism | Explicit `ScribeSolutionAnalyzer=true` property | Simple, discoverable, no magic. Convention-based detection would be ambiguous. | +| Version strategy | Timestamp-based `0.0.0-dev.yyyyMMddHHmmss` | Unique version per build forces Visual Studio to reload analyzers. Eliminates NuGet cache issues. | +| Package directory | `$(ArtifactsPath)packages/` | Uses the solution's existing artifact output directory. No separate `.packages/` directory needed. | +| Override files | Same mechanism as LocalDev | Proven pattern — `PackageVersion Update` entries in auto-generated `.Directory.Packages.targets` files, imported via wildcard. | +| Consumer-side setup | Manual one-time setup (`Directory.Build.props` + `.targets`) | Same pattern as LocalDev. All projects in the solution share the same `Directory.Build.*` files, so one-time setup covers everything. | + +--- + +## Troubleshooting + +### Consuming project doesn't see the analyzer + +1. Verify `$(ArtifactsPath)packages/` contains a `.nupkg` for the analyzer +2. Verify `Directory.Build.props` registers `$(ArtifactsPath)packages` in `RestoreAdditionalProjectSources` +3. Verify `Directory.Build.targets` imports `$(ArtifactsPath)*.Directory.Packages.targets` +4. Run `dotnet restore` on the consuming project +5. In Visual Studio, try **Build → Rebuild Solution** + +### Stale generated code after analyzer changes + +Each build produces a unique timestamp version, which forces Visual Studio to reload the analyzer. If you still see stale output: + +1. Close and reopen the solution in Visual Studio +2. Run `dotnet restore && dotnet build` + +### ArtifactsPath is not set + +The solution must use artifact output. Add to `Directory.Build.props`: + +```xml + + true + $(MSBuildThisFileDirectory).artifacts/ + +```