Skip to content
Closed
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
13 changes: 9 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ jobs:
run: dotnet tool restore

- name: Build
run: dotnet build --configuration ${{ env.CONFIGURATION }}
run: dotnet pwsh ./build.ps1 -Build -Configuration ${{ env.CONFIGURATION }}

- name: Pack (Windows only)
if: runner.os == 'Windows'
Expand Down Expand Up @@ -76,11 +76,16 @@ jobs:
}

Add-Content $releaseNotesFile -Encoding UTF8 "Commit @ ${{ github.sha }}"
$packArgs = @()

$packParams = @{
Configuration = '${{ env.CONFIGURATION }}'
PackageReleaseNotesFile = $releaseNotesFile
}
if ($versionSuffix) {
$packArgs += @('--version-suffix', $versionSuffix)
$packParams.VersionSuffix = $versionSuffix
}
dotnet pack --no-build --configuration ${{ env.CONFIGURATION }} @packArgs "-p:PackageReleaseNotesFile=$releaseNotesFile"

& ./build.ps1 -Pack @packParams
if ($LASTEXITCODE) { throw "Pack failed" }

Get-ChildItem -File -Filter docopt.net.*.nupkg dist |
Expand Down
7 changes: 7 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
<Project>

<!-- Roslyn 4.4 variant output paths must be set here (before Microsoft.Common.props)
to avoid MSB3539 warning. See: https://go.microsoft.com/fwlink/?linkid=869650 -->
<PropertyGroup Condition="'$(RoslynVersion)' == '4.4'">
<BaseOutputPath>$(MSBuildProjectDirectory)/bin/roslyn4.4/</BaseOutputPath>
<BaseIntermediateOutputPath>$(MSBuildProjectDirectory)/obj/roslyn4.4/</BaseIntermediateOutputPath>
</PropertyGroup>

<PropertyGroup>
<LangVersion>14</LangVersion>
<Nullable>enable</Nullable>
Expand Down
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,58 @@ Install [the package][nupkg] in a .NET project using:

dotnet add package docopt.net

## Building from Source

The repository uses a PowerShell script to orchestrate multi-Roslyn variant builds. Ensure you have the required tools installed:

```bash
dotnet tool restore
```

### Build

To build the project with both Roslyn 3.10 (baseline) and Roslyn 4.4 variants:

```bash
dotnet pwsh ./build.ps1
```

or simply:

```bash
dotnet pwsh ./build.ps1 -Build -Configuration Release
```

### Test

To run tests:

```bash
dotnet pwsh ./build.ps1 -Test
```

To run tests without rebuilding:

```bash
dotnet pwsh ./build.ps1 -Test -NoBuild
```

### Pack

To create a NuGet package containing both analyzer variants:

```bash
dotnet pwsh ./build.ps1 -Pack
```

**Note:** Running `dotnet pack` directly without first building all Roslyn variants will fail with an error message. Always use `./build.ps1 -Pack` to ensure both analyzer variants are included in the package.

To pack with a version suffix:

```bash
dotnet pwsh ./build.ps1 -Pack -VersionSuffix "beta1"
```

## Copyright and License

- &copy; 2012-2014 Vladimir Keleshev <vladimir@keleshev.com>
Expand Down
148 changes: 148 additions & 0 deletions build.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
#!/usr/bin/env pwsh

<#
.SYNOPSIS
Build, test, and pack DocoptNet with multi-Roslyn variant support.

.DESCRIPTION
This script orchestrates builds across Roslyn 3.10 (baseline) and Roslyn 4.4 variants,
runs tests for both variants, and produces a NuGet package containing both analyzer DLLs.

.PARAMETER Build
Build the solution (default parameter set).

.PARAMETER Test
Run tests. Note: May fail on non-Windows platforms if .NET Framework tests cannot run.

.PARAMETER Pack
Create a NuGet package containing both analyzer variants.

.PARAMETER Configuration
The build configuration (default: Release).

.PARAMETER NoBuild
Skip the build step when running tests (only applies to baseline tests).

.PARAMETER VersionSuffix
Optional version suffix for the NuGet package (e.g., "beta1").

.PARAMETER PackageReleaseNotesFile
Optional path to a file containing release notes for the NuGet package.

.EXAMPLE
./build.ps1
Build both Roslyn variants (baseline + 4.4).

.EXAMPLE
./build.ps1 -Test
Build and test both Roslyn variants.

.EXAMPLE
./build.ps1 -Test -NoBuild
Run tests without rebuilding baseline (Roslyn 4.4 tests always build as needed).

.EXAMPLE
./build.ps1 -Pack -VersionSuffix "beta1"
Build and pack with version suffix.

.EXAMPLE
./build.ps1 -Pack -PackageReleaseNotesFile "/path/to/notes.txt"
Build and pack with release notes from a file.
#>

[CmdletBinding(DefaultParameterSetName = 'Build')]
param(
[Parameter(ParameterSetName = 'Build')]
[switch] $Build,

[Parameter(ParameterSetName = 'Test', Mandatory)]
[switch] $Test,

[Parameter(ParameterSetName = 'Pack', Mandatory)]
[switch] $Pack,

[Parameter(ParameterSetName = 'Build')]
[Parameter(ParameterSetName = 'Test')]
[Parameter(ParameterSetName = 'Pack')]
[string] $Configuration = 'Release',

[Parameter(ParameterSetName = 'Test')]
[switch] $NoBuild,

[Parameter(ParameterSetName = 'Pack')]
[string] $VersionSuffix,

[Parameter(ParameterSetName = 'Pack')]
[string] $PackageReleaseNotesFile
)

$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest

# Make the script directory-independent
Push-Location $PSScriptRoot
try {
function Invoke-DotNet {
param(
[string[]] $Arguments
)

Write-Host "dotnet $($Arguments -join ' ')" -ForegroundColor Cyan
& dotnet @Arguments
if ($LASTEXITCODE -ne 0) {
throw "dotnet command failed with exit code $LASTEXITCODE"
}
}

function Invoke-BuildFlow {
Write-Host "`n=== Building Baseline (Roslyn 3.10) ===" -ForegroundColor Green
Invoke-DotNet 'build', '--configuration', $Configuration

Write-Host "`n=== Building Roslyn 4.4 Variant ===" -ForegroundColor Green
Invoke-DotNet 'build', 'src/DocoptNet/DocoptNet.csproj', '-f', 'netstandard2.0', '-p:RoslynVersion=4.4', '--configuration', $Configuration
}

function Invoke-TestFlow {
if (-not $NoBuild) {
Invoke-BuildFlow
}

Write-Host "`n=== Running Tests ===" -ForegroundColor Green
Invoke-DotNet 'test', '--no-build', '--configuration', $Configuration

# Note: Roslyn 4.4 analyzer is validated through integration tests
# that use the packed NuGet package containing both analyzer variants
}

function Invoke-PackFlow {
Invoke-BuildFlow

Write-Host "`n=== Packing NuGet Package ===" -ForegroundColor Green
$packArgs = @('pack', 'src/DocoptNet/DocoptNet.csproj', '--no-build', '--configuration', $Configuration)
if ($VersionSuffix) {
$packArgs += @('--version-suffix', $VersionSuffix)
}
if ($PackageReleaseNotesFile) {
$packArgs += "-p:PackageReleaseNotesFile=$PackageReleaseNotesFile"
}
Invoke-DotNet $packArgs
}

# Execute the appropriate flow based on parameter set
switch ($PSCmdlet.ParameterSetName) {
'Build' {
Invoke-BuildFlow
}
'Test' {
Invoke-TestFlow
}
'Pack' {
Invoke-PackFlow
}
}

Write-Host "`n=== Success ===" -ForegroundColor Green
}
finally {
Pop-Location
}
128 changes: 128 additions & 0 deletions plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
## Plan: Multi-Roslyn Configuration Support via Build Script

**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.

### Assumptions

- **Roslyn 4.4.0** is the correct target for C# 11 raw/interpolated string literal support (ships with .NET 7 SDK / VS 17.4).
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.

---

**Steps**

### 1. Modify [src/DocoptNet/DocoptNet.csproj](src/DocoptNet/DocoptNet.csproj)

**a) Redirect output paths when `RoslynVersion=4.4`**

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.

**b) Split the Roslyn `PackageReference` into two conditions**

Replace the current unconditional `Microsoft.CodeAnalysis.CSharp` 3.10.0 reference with:
- 3.10.0 when `$(RoslynVersion)` is empty or unset (baseline)
- 4.4.0 when `$(RoslynVersion)` is `4.4`

Both retain the existing `Condition="'$(TargetFramework)' != 'net47'"` guard.

**c) Add the Roslyn 4.4 analyzer to pack items**

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.

**d) Add a guard target `_ValidateRoslyn44AnalyzerOutput`**

Runs `BeforeTargets="GenerateNuspec"`. Emits an `<Error>` if the Roslyn 4.4 DLL doesn't exist, with a message like:

> *Roslyn 4.4 analyzer output not found at '...'. Build all Roslyn variants first by running: ./build.ps1 -Pack*

This prevents `dotnet pack` from producing an incomplete NuGet package.

### 2. Create `build.ps1` at the repository root

A single PowerShell script with three parameter sets:

**`Build` (default)**
- Parameters: `-Configuration` (default `Release`)
- Actions:
1. `dotnet build` the solution (all projects, all TFMs, baseline Roslyn 3.10)
2. `dotnet build src/DocoptNet/DocoptNet.csproj -f netstandard2.0 -p:RoslynVersion=4.4` (Roslyn 4.4 variant, single TFM only)

**`Test`**
- Parameters: `-Configuration` (default `Release`), `-NoBuild` switch
- Actions:
1. If not `-NoBuild`: invoke `Build` logic first
2. `dotnet test --no-build` the solution (tests against baseline Roslyn)
3. `dotnet test` the test project with `-p:RoslynVersion=4.4` (tests against Roslyn 4.4 — always built in this step because outputs differ)

`-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.

**`Pack`**
- Parameters: `-Configuration` (default `Release`), `-VersionSuffix` (optional)
- Actions:
1. Invoke `Build` logic first
2. `dotnet pack src/DocoptNet/DocoptNet.csproj --no-build` with optional `--version-suffix`

All parameter sets pass `-c $Configuration` through. The script uses `$ErrorActionPreference = 'Stop'` and checks `$LASTEXITCODE` after each `dotnet` invocation.

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.

### 3. Update [.github/workflows/ci.yml](.github/workflows/ci.yml)

**a) Replace the `Build` step**

Change from `dotnet build --configuration Release` to `dotnet pwsh ./build.ps1 -Configuration Release`.

**b) Replace the `Test` step**

Change from `dotnet test --no-build --configuration Release` to `dotnet pwsh ./build.ps1 -Test -NoBuild -Configuration Release`.

**c) Update the `Pack` step**

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.

**d) Consider Linux builds**

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.

### 4. Add `.gitignore` entry (if needed)

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.

### 5. Documentation update (independent final step)

After all code/CI/verification work is complete, perform documentation updates as a separate, standalone step:

- Update [README.md](README.md) (or CONTRIBUTING if preferred) with `build.ps1` usage for `Build`, `Test`, and `Pack`.
- Explicitly state that plain `dotnet pack src/DocoptNet/DocoptNet.csproj` is expected to fail the guard unless Roslyn 4.4 artifacts have been built.
- Include cross-platform examples that use `dotnet pwsh ./build.ps1` and note the prerequisite `dotnet tool restore`.

---

**Verification**

1. `dotnet build` at the root still works and produces the baseline build (no regression)
2. `dotnet pack src/DocoptNet/DocoptNet.csproj` **fails** with the guard target error message pointing to `build.ps1`
3. `dotnet pwsh ./build.ps1` builds both Roslyn variants successfully
4. `dotnet pwsh ./build.ps1 -Test` runs tests for both variants
5. `dotnet pwsh ./build.ps1 -Pack` produces a `.nupkg` in `dist/` that contains:
- `lib/netstandard2.0/DocoptNet.dll`
- `lib/netstandard2.1/DocoptNet.dll`
- `lib/net47/DocoptNet.dll`
- `analyzers/dotnet/cs/DocoptNet.dll` (Roslyn 3.10 baseline)
- `analyzers/dotnet/roslyn4.4/cs/DocoptNet.dll` (Roslyn 4.4)
- `build/netstandard2.0/docopt.net.targets`
6. Inspect the package with `dotnet nuget verify` or extract and check folder structure
7. CI workflow passes on both Windows and Linux
8. Documentation is updated only after steps 1–7 are complete (as an independent final step)

**Decisions**

- **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
- **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
- **Script at repo root**: `build.ps1` matches convention alongside existing `mark-shipped.ps1`; cross-platform via PowerShell Core (already used in CI)
- **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
Loading