|
| 1 | +## Plan: Multi-Roslyn Configuration Support via Build Script |
| 2 | + |
| 3 | +**TL;DR**: Add a `RoslynVersion` MSBuild property to [src/DocoptNet/DocoptNet.csproj](src/DocoptNet/DocoptNet.csproj) that conditionally switches the `Microsoft.CodeAnalysis.CSharp` reference (3.10.0 baseline → 4.4.0) and defines `ROSLYN4_4`. The default build remains unchanged. A new PowerShell script (`build.ps1`) with `Build`, `Test`, and `Pack` parameter sets orchestrates multi-Roslyn builds. The NuGet package ships both analyzer DLLs: unversioned (baseline) and `roslyn4.4/` (new). A guard target in the `.csproj` prevents packing without the Roslyn 4.4 output. |
| 4 | + |
| 5 | +### Assumptions |
| 6 | + |
| 7 | +- **Roslyn 4.4.0** is the correct target for C# 11 raw/interpolated string literal support (ships with .NET 7 SDK / VS 17.4). |
| 8 | +- The `roslyn{version}` analyzer path convention (`analyzers/dotnet/roslyn4.4/cs/`) is supported by .NET SDK 6.0.4+. Users on older SDKs automatically fall back to the unversioned `analyzers/dotnet/cs/` path. |
| 9 | +- Only the `netstandard2.0` TFM needs to be built with Roslyn 4.4 (the `analyzers/` DLL). The `lib/` TFMs (`netstandard2.0`, `netstandard2.1`, `net47`) are unaffected. |
| 10 | +- The `RoslynVersion` property is **not** passed by default — a bare `dotnet build` produces the baseline. Only the script (or explicit `-p:RoslynVersion=4.4`) triggers the variant build. |
| 11 | +- The test project [tests/DocoptNet.Tests/DocoptNet.Tests.csproj](tests/DocoptNet.Tests/DocoptNet.Tests.csproj) will also accept `RoslynVersion` passthrough (since it project-references `DocoptNet.csproj`), enabling Roslyn-4.4-specific test coverage. |
| 12 | +- The existing [tests/Integration/run.ps1](tests/Integration/run.ps1) and its wrappers remain unchanged — they already invoke `dotnet pack` and `dotnet test`, and will work with the packed NuGet that contains both analyzer variants. |
| 13 | +- PowerShell script execution in CI/local commands can rely on the pinned local tool (`dotnet pwsh`) and therefore assumes `dotnet tool restore` has been run first. |
| 14 | + |
| 15 | +--- |
| 16 | + |
| 17 | +**Steps** |
| 18 | + |
| 19 | +### 1. Modify [src/DocoptNet/DocoptNet.csproj](src/DocoptNet/DocoptNet.csproj) |
| 20 | + |
| 21 | +**a) Redirect output paths when `RoslynVersion=4.4`** |
| 22 | + |
| 23 | +Add a `PropertyGroup` conditioned on `$(RoslynVersion)` that overrides `BaseOutputPath` and `BaseIntermediateOutputPath` to isolate the Roslyn 4.4 build artifacts from the default build. Also define `ROSLYN4_4` for conditional compilation. |
| 24 | + |
| 25 | +**b) Split the Roslyn `PackageReference` into two conditions** |
| 26 | + |
| 27 | +Replace the current unconditional `Microsoft.CodeAnalysis.CSharp` 3.10.0 reference with: |
| 28 | +- 3.10.0 when `$(RoslynVersion)` is empty or unset (baseline) |
| 29 | +- 4.4.0 when `$(RoslynVersion)` is `4.4` |
| 30 | + |
| 31 | +Both retain the existing `Condition="'$(TargetFramework)' != 'net47'"` guard. |
| 32 | + |
| 33 | +**c) Add the Roslyn 4.4 analyzer to pack items** |
| 34 | + |
| 35 | +Add a second `<None Pack="true" PackagePath="analyzers/dotnet/roslyn4.4/cs" />` item pointing to the Roslyn 4.4 build output. Use project-relative MSBuild properties (`$(MSBuildProjectDirectory)`, `$(BaseOutputPath)`, etc.) for robust path construction instead of hard-coded Windows separators. |
| 36 | + |
| 37 | +**d) Add a guard target `_ValidateRoslyn44AnalyzerOutput`** |
| 38 | + |
| 39 | +Runs `BeforeTargets="GenerateNuspec"`. Emits an `<Error>` if the Roslyn 4.4 DLL doesn't exist, with a message like: |
| 40 | + |
| 41 | +> *Roslyn 4.4 analyzer output not found at '...'. Build all Roslyn variants first by running: ./build.ps1 -Pack* |
| 42 | +
|
| 43 | +This prevents `dotnet pack` from producing an incomplete NuGet package. |
| 44 | + |
| 45 | +### 2. Create `build.ps1` at the repository root |
| 46 | + |
| 47 | +A single PowerShell script with three parameter sets: |
| 48 | + |
| 49 | +**`Build` (default)** |
| 50 | +- Parameters: `-Configuration` (default `Release`) |
| 51 | +- Actions: |
| 52 | + 1. `dotnet build` the solution (all projects, all TFMs, baseline Roslyn 3.10) |
| 53 | + 2. `dotnet build src/DocoptNet/DocoptNet.csproj -f netstandard2.0 -p:RoslynVersion=4.4` (Roslyn 4.4 variant, single TFM only) |
| 54 | + |
| 55 | +**`Test`** |
| 56 | +- Parameters: `-Configuration` (default `Release`), `-NoBuild` switch |
| 57 | +- Actions: |
| 58 | + 1. If not `-NoBuild`: invoke `Build` logic first |
| 59 | + 2. `dotnet test --no-build` the solution (tests against baseline Roslyn) |
| 60 | + 3. `dotnet test` the test project with `-p:RoslynVersion=4.4` (tests against Roslyn 4.4 — always built in this step because outputs differ) |
| 61 | + |
| 62 | +`-NoBuild` behavior clarification: this switch applies to baseline solution tests only. The Roslyn 4.4 test pass still performs build work as needed for the alternate output path. |
| 63 | + |
| 64 | +**`Pack`** |
| 65 | +- Parameters: `-Configuration` (default `Release`), `-VersionSuffix` (optional) |
| 66 | +- Actions: |
| 67 | + 1. Invoke `Build` logic first |
| 68 | + 2. `dotnet pack src/DocoptNet/DocoptNet.csproj --no-build` with optional `--version-suffix` |
| 69 | + |
| 70 | +All parameter sets pass `-c $Configuration` through. The script uses `$ErrorActionPreference = 'Stop'` and checks `$LASTEXITCODE` after each `dotnet` invocation. |
| 71 | + |
| 72 | +To make invocation directory-independent, the script should `Push-Location` to `$PSScriptRoot` at startup and `Pop-Location` in a `finally` block so cleanup always runs on success or failure. |
| 73 | + |
| 74 | +### 3. Update [.github/workflows/ci.yml](.github/workflows/ci.yml) |
| 75 | + |
| 76 | +**a) Replace the `Build` step** |
| 77 | + |
| 78 | +Change from `dotnet build --configuration Release` to `dotnet pwsh ./build.ps1 -Configuration Release`. |
| 79 | + |
| 80 | +**b) Replace the `Test` step** |
| 81 | + |
| 82 | +Change from `dotnet test --no-build --configuration Release` to `dotnet pwsh ./build.ps1 -Test -NoBuild -Configuration Release`. |
| 83 | + |
| 84 | +**c) Update the `Pack` step** |
| 85 | + |
| 86 | +The existing pack step has version-suffix logic. Keep that logic but replace the `dotnet pack` invocation with `dotnet pwsh ./build.ps1 -Pack -Configuration Release -VersionSuffix $versionSuffix`. The guard target ensures the Roslyn 4.4 output is present. |
| 87 | + |
| 88 | +**d) Consider Linux builds** |
| 89 | + |
| 90 | +The CI matrix includes `ubuntu-22.04`. The build step there also needs to run the PowerShell script. Since this plan uses `dotnet pwsh`, ensure `dotnet tool restore` runs before Build/Test/Pack steps so the pinned PowerShell tool is available on both platforms. |
| 91 | + |
| 92 | +### 4. Add `.gitignore` entry (if needed) |
| 93 | + |
| 94 | +The redirected output path `src/DocoptNet/bin/roslyn4.4/` and `src/DocoptNet/obj/roslyn4.4/` should already be covered by the existing `bin/` and `obj/` ignore patterns. Verify this. |
| 95 | + |
| 96 | +### 5. Documentation update (independent final step) |
| 97 | + |
| 98 | +After all code/CI/verification work is complete, perform documentation updates as a separate, standalone step: |
| 99 | + |
| 100 | +- Update [README.md](README.md) (or CONTRIBUTING if preferred) with `build.ps1` usage for `Build`, `Test`, and `Pack`. |
| 101 | +- Explicitly state that plain `dotnet pack src/DocoptNet/DocoptNet.csproj` is expected to fail the guard unless Roslyn 4.4 artifacts have been built. |
| 102 | +- Include cross-platform examples that use `dotnet pwsh ./build.ps1` and note the prerequisite `dotnet tool restore`. |
| 103 | + |
| 104 | +--- |
| 105 | + |
| 106 | +**Verification** |
| 107 | + |
| 108 | +1. `dotnet build` at the root still works and produces the baseline build (no regression) |
| 109 | +2. `dotnet pack src/DocoptNet/DocoptNet.csproj` **fails** with the guard target error message pointing to `build.ps1` |
| 110 | +3. `dotnet pwsh ./build.ps1` builds both Roslyn variants successfully |
| 111 | +4. `dotnet pwsh ./build.ps1 -Test` runs tests for both variants |
| 112 | +5. `dotnet pwsh ./build.ps1 -Pack` produces a `.nupkg` in `dist/` that contains: |
| 113 | + - `lib/netstandard2.0/DocoptNet.dll` |
| 114 | + - `lib/netstandard2.1/DocoptNet.dll` |
| 115 | + - `lib/net47/DocoptNet.dll` |
| 116 | + - `analyzers/dotnet/cs/DocoptNet.dll` (Roslyn 3.10 baseline) |
| 117 | + - `analyzers/dotnet/roslyn4.4/cs/DocoptNet.dll` (Roslyn 4.4) |
| 118 | + - `build/netstandard2.0/docopt.net.targets` |
| 119 | +6. Inspect the package with `dotnet nuget verify` or extract and check folder structure |
| 120 | +7. CI workflow passes on both Windows and Linux |
| 121 | +8. Documentation is updated only after steps 1–7 are complete (as an independent final step) |
| 122 | + |
| 123 | +**Decisions** |
| 124 | + |
| 125 | +- **Configuration-driven, not multi-project**: avoids code duplication and shared project complexity; the tradeoff is that `dotnet build`/`dotnet pack` alone only handle the baseline |
| 126 | +- **Guard target over silent omission**: using `Condition="Exists(...)"` on the pack item would silently skip the Roslyn 4.4 DLL; an explicit `<Error>` is safer and more informative |
| 127 | +- **Script at repo root**: `build.ps1` matches convention alongside existing `mark-shipped.ps1`; cross-platform via PowerShell Core (already used in CI) |
| 128 | +- **Only `netstandard2.0` for Roslyn 4.4 variant**: the other TFMs are for the runtime library only and don't include generator code, so building them again with Roslyn 4.4 is unnecessary |
0 commit comments