Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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 |
Expand Down
18 changes: 18 additions & 0 deletions Scribe.Sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<Project Sdk="BulletsForHumanity.Scribe.Sdk">
<PropertyGroup>
<ScribeSolutionAnalyzer>true</ScribeSolutionAnalyzer>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
<PackageReference Include="BulletsForHumanity.Scribe" PrivateAssets="all" />
</ItemGroup>
</Project>
```

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)
Expand Down
11 changes: 9 additions & 2 deletions Scribe.Sdk/Scribe.Sdk.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -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.
-->
<PropertyGroup>
<PackageId>BulletsForHumanity.Scribe.Sdk</PackageId>
Expand All @@ -18,7 +19,7 @@
<NoWarn>$(NoWarn);NU5128;NU5129;NETSDK1212</NoWarn>
<!-- Ensure .template.config and other dotfiles are included. -->
<NoDefaultExcludes>true</NoDefaultExcludes>
<Description>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: &lt;Project Sdk="BulletsForHumanity.Scribe.Sdk"&gt;</Description>
<Description>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: &lt;Project Sdk="BulletsForHumanity.Scribe.Sdk"&gt;</Description>
<PackageTags>roslyn;analyzer;generator;source-generator;msbuild-sdk;scribe</PackageTags>
</PropertyGroup>

Expand All @@ -38,6 +39,12 @@
<None Include="..\Scribe\build\Scribe.LocalDev.targets" Pack="true" PackagePath="build" />
</ItemGroup>

<!-- Pack the SolutionAnalyzer build infrastructure from the main Scribe project. -->
<ItemGroup>
<None Include="..\Scribe\build\Scribe.SolutionAnalyzer.props" Pack="true" PackagePath="build" />
<None Include="..\Scribe\build\Scribe.SolutionAnalyzer.targets" Pack="true" PackagePath="build" />
</ItemGroup>

<!-- Pack LICENSE and README. -->
<ItemGroup>
<None Include="..\LICENSE" Pack="true" PackagePath="" />
Expand Down
3 changes: 3 additions & 0 deletions Scribe.Sdk/Sdk/Sdk.props
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@
<ScribeSdkIncludeStubs>true</ScribeSdkIncludeStubs>
</PropertyGroup>

<!-- ── Solution-Local Analyzer Infrastructure (Early Phase) ──────────── -->
<Import Project="$(MSBuildThisFileDirectory)..\build\Scribe.SolutionAnalyzer.props" />

<!-- ── LocalDev Infrastructure (Early Phase) ─────────────────────────── -->
<Import Project="$(MSBuildThisFileDirectory)..\build\Scribe.LocalDev.props" />
</Project>
3 changes: 3 additions & 0 deletions Scribe.Sdk/Sdk/Sdk.targets
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@
</ItemGroup>
</Target>

<!-- ── Solution-Local Analyzer Infrastructure (Late Phase) ───────────── -->
<Import Project="$(MSBuildThisFileDirectory)..\build\Scribe.SolutionAnalyzer.targets" />

<!-- ── LocalDev Infrastructure (Late Phase) ──────────────────────────── -->
<Import Project="$(MSBuildThisFileDirectory)..\build\Scribe.LocalDev.targets" />
</Project>
99 changes: 99 additions & 0 deletions Scribe/build/Scribe.SolutionAnalyzer.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<Project>
<!--
Scribe Solution-Local Analyzer — Property Definitions (Early Phase)

Automatically activates when a Scribe SDK project sets
ScribeSolutionAnalyzer=true. Configures auto-pack on build with a
timestamp-based version, outputs to the solution's artifacts/packages/
directory, and registers that directory as a NuGet source.

Uses the same mechanism as LocalDev — the companion targets file
generates a version override .Directory.Packages.targets file so
consuming projects resolve the locally-built package automatically.

── Usage ────────────────────────────────────────────────────────────────

In the analyzer .csproj (uses the Scribe SDK):

<PropertyGroup>
<ScribeSolutionAnalyzer>true</ScribeSolutionAnalyzer>
</PropertyGroup>

Consuming projects reference the analyzer via standard PackageReference.
The version is managed automatically via an override file — consumers
only need to declare the PackageVersion Include in Directory.Packages.props
(any version; the override file will pin the actual version).

── Prerequisites ──────────────────────────────────────────────────────

The solution must use artifact output (UseArtifactsOutput=true) with
$(ArtifactsPath) set in Directory.Build.props.

The solution's Directory.Build.props must register the packages source:

<RestoreAdditionalProjectSources>
$(RestoreAdditionalProjectSources);$(ArtifactsPath)packages
</RestoreAdditionalProjectSources>

The solution's Directory.Build.targets must import override files:

<Import Project="$(ArtifactsPath)*.Directory.Packages.targets"
Condition="'$(ArtifactsPath)' != '' and Exists('$(ArtifactsPath)')" />

See docs/solution-local-analyzers.md for the complete setup guide.

── Provides ─────────────────────────────────────────────────────────────

$(ScribeSolutionAnalyzer) 'true' when active.
$(Version) Timestamp-based: 0.0.0-dev.yyyyMMddHHmmss
$(PackageVersion) Same as $(Version).
-->

<!-- ── Solution-Local Analyzer Configuration ─────────────────────────── -->
<PropertyGroup Condition="'$(ScribeSolutionAnalyzer)' == 'true'">
<!-- Timestamp-based version — each build produces a unique version,
which forces Visual Studio to reload the analyzer assembly.
No NuGet cache invalidation is needed because the version is always new. -->
<_ScribeSaTimestamp>$([System.DateTime]::UtcNow.ToString('yyyyMMddHHmmss'))</_ScribeSaTimestamp>
<Version>0.0.0-dev.$(_ScribeSaTimestamp)</Version>
<PackageVersion>0.0.0-dev.$(_ScribeSaTimestamp)</PackageVersion>

<!-- Auto-pack on every build so consuming projects always get fresh output. -->
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<IsPackable>true</IsPackable>

<!-- Disable Nerdbank.GitVersioning — version is managed by the SDK. -->
<NerdbankGitVersioningEnabled>false</NerdbankGitVersioningEnabled>
</PropertyGroup>

<!-- ── Normalised Artifacts Directory ──────────────────────────────── -->
<!--
$(ArtifactsPath) may or may not end with a directory separator.
Normalise to a guaranteed trailing backslash so path concatenation is safe.
-->
<PropertyGroup Condition="'$(ScribeSolutionAnalyzer)' == 'true' and '$(ArtifactsPath)' != ''">
<_ScribeSaArtifactsDir>$([MSBuild]::EnsureTrailingSlash('$(ArtifactsPath)'))</_ScribeSaArtifactsDir>
</PropertyGroup>

<!-- ── Package Output Directory ──────────────────────────────────────── -->
<!--
Outputs to $(ArtifactsPath)packages/ — the standard artifacts packages
directory. Projects using artifact output (UseArtifactsOutput=true) have
$(ArtifactsPath) set via Directory.Build.props.
-->
<PropertyGroup Condition="'$(ScribeSolutionAnalyzer)' == 'true' and '$(_ScribeSaArtifactsDir)' != ''">
<PackageOutputPath>$(_ScribeSaArtifactsDir)packages\</PackageOutputPath>
</PropertyGroup>

<!-- ── NuGet Source Registration ─────────────────────────────────────── -->
<!--
Registers artifacts/packages/ as an additional NuGet source for the
analyzer project itself (so it can resolve sibling solution-local packages).
Consuming projects must register this source in Directory.Build.props.
-->
<PropertyGroup Condition="'$(ScribeSolutionAnalyzer)' == 'true' and '$(_ScribeSaArtifactsDir)' != ''">
<RestoreAdditionalProjectSources>
$(RestoreAdditionalProjectSources);$(_ScribeSaArtifactsDir)packages
</RestoreAdditionalProjectSources>
</PropertyGroup>
</Project>
58 changes: 58 additions & 0 deletions Scribe/build/Scribe.SolutionAnalyzer.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<Project>
<!--
Scribe Solution-Local Analyzer — Build Targets (Late Phase)

When $(ScribeSolutionAnalyzer) is 'true', generates a version override
.Directory.Packages.targets file after pack. This is the same mechanism
as LocalDev — consuming projects import the override file via wildcard
to resolve the locally-built package.
-->

<!-- ── Override File Generation ──────────────────────────────────────── -->
<!--
Runs after Pack on the analyzer project. Generates a
.Directory.Packages.targets file containing a PackageVersion Update entry
that pins the package to the exact version that was just built.

Consuming projects import these files via wildcard from $(ArtifactsPath):

<Import Project="$(ArtifactsPath)*.Directory.Packages.targets"
Condition="..." />

This is the same pattern as LocalDev's _ScribeLocalDevOverride target.
-->
<Target
Name="_ScribeSolutionAnalyzerOverride"
AfterTargets="Pack"
Condition="'$(ScribeSolutionAnalyzer)' == 'true'
and '$(ArtifactsPath)' != ''"
>
<PropertyGroup>
<_ScribeSaVersion>$(NuGetPackageVersion)</_ScribeSaVersion>
<_ScribeSaVersion Condition="'$(_ScribeSaVersion)' == ''">$(PackageVersion)</_ScribeSaVersion>
<_ScribeSaArtifactsDir>$([MSBuild]::EnsureTrailingSlash('$(ArtifactsPath)'))</_ScribeSaArtifactsDir>
<_ScribeSaOverridePath>$(_ScribeSaArtifactsDir)$(PackageId).Directory.Packages.targets</_ScribeSaOverridePath>
</PropertyGroup>

<ItemGroup>
<_ScribeSaLine Include="&lt;Project&gt;" />
<_ScribeSaLine Include=" &lt;!-- Auto-generated by Scribe SolutionAnalyzer — do not edit. Rebuild the analyzer to refresh. --&gt;" />
<_ScribeSaLine Include=" &lt;ItemGroup&gt;" />
<_ScribeSaLine Include=" &lt;PackageVersion Update=&quot;$(PackageId)&quot; Version=&quot;$(_ScribeSaVersion)&quot; /&gt;" />
<_ScribeSaLine Include=" &lt;/ItemGroup&gt;" />
<_ScribeSaLine Include="&lt;/Project&gt;" />
</ItemGroup>

<MakeDir Directories="$(_ScribeSaArtifactsDir)" />
<WriteLinesToFile
File="$(_ScribeSaOverridePath)"
Lines="@(_ScribeSaLine)"
Overwrite="true"
Encoding="UTF-8"
/>
<Message
Text="[Scribe] Solution analyzer override → $(_ScribeSaOverridePath) ($(_ScribeSaVersion))"
Importance="high"
/>
</Target>
</Project>
69 changes: 66 additions & 3 deletions docs/architecture-infrastructure.md
Original file line number Diff line number Diff line change
@@ -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).

Expand All @@ -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
```
Expand All @@ -43,7 +45,8 @@ Runs before the project file is evaluated. Sets overridable defaults:
1. **Chains to `Microsoft.NET.Sdk`** via `<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />`
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 `<ScribeSdkIncludeStubs>false</ScribeSdkIncludeStubs>`)
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.

Expand All @@ -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
<Project>
<!-- Auto-generated by Scribe SolutionAnalyzer — do not edit. -->
<ItemGroup>
<PackageVersion Update="MyAnalyzer" Version="0.0.0-dev.20260406054112" />
</ItemGroup>
</Project>
```

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.

---

Expand Down
Loading
Loading