diff --git a/README.md b/README.md
index 27d8730..89aaf3f 100644
--- a/README.md
+++ b/README.md
@@ -227,7 +227,7 @@ Version detection logging is disabled by default. Set `VersionDetectionLogEnable
| `NamespacePrefix` | *(required)* | Root namespace prefix, e.g. `Acme`. Results in `Acme.MyProject`. |
| `DisableNamespacePrefixCheck` | `false` | Set to `true` to suppress the build error for missing `NamespacePrefix`. |
| `TargetFramework` | `net10.0` | Override the default TFM per-project or globally. Defaults to `netstandard2.0` for projects declaring `IsRoslynComponent=true`. |
-| `IsRoslynComponent` | `false` | When explicitly `true`, applies source-generator defaults: a single `netstandard2.0` target, `LangVersion=latest`, `Nullable=enable`, `TreatWarningsAsErrors=true`, extended analyzer rules, SourceLink, generated-file output, dependency output, 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/`. Roslyn development dependencies (`Microsoft.CodeAnalysis.*`, `Microsoft.CodeAnalysis.Analyzers`) default to `PrivateAssets="all"`. |
+| `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`, generated-file output, dependency output, 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. |
@@ -243,7 +243,7 @@ For projects where `IsPackable=true`, the SDK provides these defaults **only whe
| Property | Default | Description |
| -- | -- | -- |
| `GenerateDocumentationFile` | `true` | Emits XML documentation. |
-| `IncludeSymbols` | `true` | Produces a symbol package (`false` for Roslyn components — their PDB ships in `analyzers/dotnet/cs/` instead). |
+| `IncludeSymbols` | `true` | Produces a symbol package (`false` for Roslyn components — their PDB ships inside the `.nupkg` under `analyzers/dotnet/cs/` via `PurviewPackAnalyzerPdb=true`). |
| `SymbolPackageFormat` | `snupkg` | Symbol package format. Always the modern `.snupkg`; the legacy `.symbols.nupkg` is never produced by default. |
| `PublishRepositoryUrl` | `true` | Publishes the repository URL. |
| `EmbedUntrackedSources` | `true` | Embeds untracked sources for SourceLink. |
diff --git a/package.json b/package.json
index fa5d9d8..dcb9d9d 100644
--- a/package.json
+++ b/package.json
@@ -1,4 +1,4 @@
{
- "name": "changeops",
- "version": "1.0.0-prerelease.45"
+ "name": "pruview-dotnet-project-sdk",
+ "version": "1.0.0-prerelease.46"
}
diff --git a/src/src/DotNetProjectSdk/Sdk/.agents/skills/sdk-configuration-reference/SKILL.md b/src/src/DotNetProjectSdk/Sdk/.agents/skills/sdk-configuration-reference/SKILL.md
index 7e60ecb..a99099b 100644
--- a/src/src/DotNetProjectSdk/Sdk/.agents/skills/sdk-configuration-reference/SKILL.md
+++ b/src/src/DotNetProjectSdk/Sdk/.agents/skills/sdk-configuration-reference/SKILL.md
@@ -44,7 +44,7 @@ These are the most important configurable properties exposed by the SDK:
- `NamespacePrefix` — required unless `DisableNamespacePrefixCheck=true`
- `DisableNamespacePrefixCheck` — default `false`
- `TargetFramework` — defaults to `net10.0` when neither `TargetFramework` nor `TargetFrameworks` is set; projects explicitly declaring `IsRoslynComponent=true` default to `netstandard2.0`
-- `IsRoslynComponent` — when explicitly `true`, applies source-generator defaults: a single `netstandard2.0` target, `LangVersion=latest`, `Nullable=enable`, `TreatWarningsAsErrors=true`, extended analyzer rules, disabled SourceLink and untracked-source embedding, no dependency file, compiler-generated output under the framework-specific intermediate directory, telemetry exclusion, excluded normal build output, `IncludeSymbols=false` (no `.symbols.nupkg`/`.snupkg` by default — the PDB ships in `analyzers/dotnet/cs/`), and `PrivateAssets=all` applied to `Microsoft.CodeAnalysis.*` / `Microsoft.CodeAnalysis.Analyzers` references. Packable Roslyn components automatically pack the built analyzer assembly and PDB into `analyzers/dotnet/cs/`
+- `IsRoslynComponent` — 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`, no dependency file, compiler-generated output under the framework-specific intermediate directory, telemetry exclusion, excluded normal build output, `IncludeSymbols=false` (no `.symbols.nupkg`/`.snupkg` by default — the PDB ships inside the `.nupkg` under `analyzers/dotnet/cs/` via `PurviewPackAnalyzerPdb=true`), and `PrivateAssets=all` applied to `Microsoft.CodeAnalysis.*` / `Microsoft.CodeAnalysis.Analyzers` references. Packable Roslyn components automatically pack the built analyzer assembly and PDB into `analyzers/dotnet/cs/`; a pack-time validation (`ValidateRoslynComponentCompilerSettings`) fails the pack if the compiler defaults are missing unless `DisableRoslynCompilerDefaultsValidation=true`
- `PackProjectReferencedSourceGenerators` — default `true`; packable projects automatically include analyzer `ProjectReference` outputs and runtime dependencies under `analyzers/dotnet/cs/`. Set it to `false` globally or use `Pack="false"` on one analyzer reference to opt out.
- `EnableAssemblyNameGeneration` — default `true`; when `true`, `AssemblyName` and default `PackageId` follow the fully evaluated `RootNamespace`. Set `false` before the SDK import to use the standard project-name behaviour
- `DisableProjectFileNamingConventionCheck` — default `false`; disables the directory-name/file-name match validation
diff --git a/src/src/DotNetProjectSdk/Sdk/.agents/skills/sdk-project-behavior-and-detection/SKILL.md b/src/src/DotNetProjectSdk/Sdk/.agents/skills/sdk-project-behavior-and-detection/SKILL.md
index bb27583..7c651b7 100644
--- a/src/src/DotNetProjectSdk/Sdk/.agents/skills/sdk-project-behavior-and-detection/SKILL.md
+++ b/src/src/DotNetProjectSdk/Sdk/.agents/skills/sdk-project-behavior-and-detection/SKILL.md
@@ -147,10 +147,12 @@ This is why consistent naming and placement matter so much in repos that use the
### For Roslyn component (analyzer/source-generator) projects
-- Defaults a single `netstandard2.0` target, `LangVersion=latest`, `Nullable=enable`, `TreatWarningsAsErrors=true`, extended analyzer rules, disabled SourceLink, and excluded normal build output (`IncludeBuildOutput=false`)
-- Defaults `IncludeSymbols=false` — no `.symbols.nupkg` or `.snupkg` is produced by default; the analyzer PDB ships inside the main `.nupkg` under `analyzers/dotnet/cs/` beside the analyzer assembly
+- Defaults a single `netstandard2.0` target, `LangVersion=latest`, `Nullable=enable`, `TreatWarningsAsErrors=true`, `Deterministic=true`, extended analyzer rules, SourceLink with `EmbedUntrackedSources=true`, and excluded normal build output (`IncludeBuildOutput=false`)
+- Defaults `IncludeSymbols=false` — no `.symbols.nupkg` or `.snupkg` is produced by default; the analyzer PDB ships inside the main `.nupkg` under `analyzers/dotnet/cs/` beside the analyzer assembly (`PurviewPackAnalyzerPdb=true`; set `false` only when symbols are delivered another way, since NuGet's `.snupkg` cannot host `analyzers/dotnet/cs` symbols)
- Packable Roslyn components automatically pack the built analyzer assembly (and PDB) into `analyzers/dotnet/cs/`; `SymbolPackageFormat` defaults to the modern `snupkg` if symbols are explicitly opted into
- `Microsoft.CodeAnalysis.*` and `Microsoft.CodeAnalysis.Analyzers` references are defaulted to `PrivateAssets=all` (development-only dependencies) so they never leak into the packed nuspec
+- A pack-time validation (`ValidateRoslynComponentCompilerSettings`) fails the pack of a packable Roslyn component if `LangVersion`, `Nullable`, `TreatWarningsAsErrors`, or `EnforceExtendedAnalyzerRules` is missing; opt out with `DisableRoslynCompilerDefaultsValidation=true`
+- `ContinuousIntegrationBuild` is set only by real CI environment variables — packability alone never forces SourceLink's CI-mode dirty-repository checks
### For test and shared-testing projects
diff --git a/src/src/DotNetProjectSdk/Sdk/Sdk.props b/src/src/DotNetProjectSdk/Sdk/Sdk.props
index 9eb9405..d575a25 100644
--- a/src/src/DotNetProjectSdk/Sdk/Sdk.props
+++ b/src/src/DotNetProjectSdk/Sdk/Sdk.props
@@ -307,6 +307,40 @@
+
+
+
+ C# language version used to build the Roslyn component. Defaults to latest.
+
+
+ Nullable reference-types context used to build the Roslyn component. Defaults to enable.
+
+
+ Treats all compiler/analyzer warnings as errors. Defaults to true for Roslyn components.
+
+
+ Enforces Roslyn analyzer best-practice rules. Defaults to true for Roslyn components.
+
+
+ Produce deterministic builds. Defaults to true.
+
+
+ True when building in a recognized CI environment, producing deterministic SourceLink output.
+
+
+ Embeds untracked source files for SourceLink. Defaults to true for Roslyn components.
+
+
+
@@ -330,8 +364,13 @@
modern snupkg format rather than the legacy symbols.nupkg. -->
false
$(NoWarn);NU5128;
- true
- false
+
+ true
+
+ true
false
true
false
@@ -340,9 +379,12 @@
net10.0
-
+ true
+
true
<_PurviewPackagedEditorConfigFilePath>$(MSBuildThisFileDirectory).editorconfig
diff --git a/src/src/DotNetProjectSdk/Sdk/Sdk.targets b/src/src/DotNetProjectSdk/Sdk/Sdk.targets
index dd60ab1..a73d334 100644
--- a/src/src/DotNetProjectSdk/Sdk/Sdk.targets
+++ b/src/src/DotNetProjectSdk/Sdk/Sdk.targets
@@ -29,12 +29,16 @@
Condition="'$(IsCSharpProject)' == 'true' AND '$(IsRoslynComponent)' == 'true' AND '$(IsPackable)' == 'true' AND '$(TargetFrameworks)' == ''"
>
+
@@ -64,6 +68,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ (StringComparer.OrdinalIgnoreCase);
+ var results = new List();
+ foreach (var file in Files ?? Array.Empty())
+ {
+ // Two analyzer projects can share a runtime dependency (for example a shared
+ // support assembly). NuGet fails the pack on duplicate target paths, so keep
+ // only the first file for each resolved analyzers/dotnet/cs package path.
+ // The calling target re-applies PackagePath metadata afterwards.
+ var packagePath = "analyzers/dotnet/cs/" + Path.GetFileName(file.ItemSpec);
+ if (!seen.Add(packagePath))
+ continue;
+
+ results.Add(new TaskItem(file.ItemSpec));
+ }
+ DeduplicatedFiles = results.ToArray();
+ ]]>
+
+
+
+
+
+
+
+
-
+
analyzers/dotnet/cs/
@@ -563,6 +647,7 @@
===================================================================== -->
@@ -571,6 +656,10 @@
Include="@(None)"
Condition="'%(None.Pack)' == 'true' AND $([System.String]::Copy('%(None.Filename)').ToLowerInvariant()) == 'readme'"
/>
+ <_PurviewPackedReadme
+ Include="@(None)"
+ Condition="($([System.String]::Copy('%(None.Link)').ToLowerInvariant()) == 'sdk/readme.md' OR $([System.String]::Copy('%(None.Link)').ToLowerInvariant()) == 'sdk\readme.md')"
+ />
<_PurviewPackedReadme
Include="@(TfmSpecificPackageFile)"
Condition="$([System.String]::Copy('%(TfmSpecificPackageFile.Filename)').ToLowerInvariant()) == 'readme'"
diff --git a/src/tests/DotNetProjectSdk.IntegrationTests/CoreDefaultsTests.cs b/src/tests/DotNetProjectSdk.IntegrationTests/CoreDefaultsTests.cs
index 61675c8..cae7415 100644
--- a/src/tests/DotNetProjectSdk.IntegrationTests/CoreDefaultsTests.cs
+++ b/src/tests/DotNetProjectSdk.IntegrationTests/CoreDefaultsTests.cs
@@ -152,6 +152,24 @@ public async Task Ci_PropertyNotSet_WhenEnvironmentVariableAbsent(CancellationTo
await Assert.That(value).IsNotEqualTo("true");
}
+ [Test]
+ public async Task Ci_PropertyNotSet_WhenPackableButNotCi(CancellationToken cancellationToken)
+ {
+ // Packability alone must not force ContinuousIntegrationBuild: local packs must not enter
+ // SourceLink's CI-mode dirty-repository checks (which fail under TreatWarningsAsErrors).
+ using var h = await ProjectHarness.CreateAsync(
+ "MyLibrary",
+ extraEnv: new Dictionary { ["CI"] = "" },
+ extraProps: "true",
+ cancellationToken: cancellationToken
+ );
+ var value = await h.GetPropertyAsync("ContinuousIntegrationBuild", cancellationToken);
+ await Assert
+ .That(value)
+ .IsNotEqualTo("true")
+ .Because("Only real CI environment variables may set ContinuousIntegrationBuild.");
+ }
+
[Test]
[Arguments("net9.0", "net9.0")]
[Arguments("net10.0", "net10.0")]
diff --git a/src/tests/DotNetProjectSdk.IntegrationTests/Harness/ProjectHarness.cs b/src/tests/DotNetProjectSdk.IntegrationTests/Harness/ProjectHarness.cs
index 77b13ec..eb3a86f 100644
--- a/src/tests/DotNetProjectSdk.IntegrationTests/Harness/ProjectHarness.cs
+++ b/src/tests/DotNetProjectSdk.IntegrationTests/Harness/ProjectHarness.cs
@@ -154,6 +154,9 @@ await File.WriteAllTextAsync(
true
+
+
+
""",
cancellationToken
diff --git a/src/tests/DotNetProjectSdk.IntegrationTests/PackableProjectDefaultsTests.cs b/src/tests/DotNetProjectSdk.IntegrationTests/PackableProjectDefaultsTests.cs
index 36b113b..feab96d 100644
--- a/src/tests/DotNetProjectSdk.IntegrationTests/PackableProjectDefaultsTests.cs
+++ b/src/tests/DotNetProjectSdk.IntegrationTests/PackableProjectDefaultsTests.cs
@@ -476,10 +476,8 @@ static string ExtractPackVersion(string stdOut, string packageId)
stdOut,
$"Successfully created package '[^']*{System.Text.RegularExpressions.Regex.Escape(packageId)}\\.([^']+)\\.nupkg'"
);
- if (!match.Success)
- return string.Empty;
- return match.Groups[1].Value;
+ return match.Success ? match.Groups[1].Value : string.Empty;
}
static async Task<(int Code, string StdOut, string StdErr)> RunProcessAsync(
diff --git a/src/tests/DotNetProjectSdk.IntegrationTests/RoslynComponentDefaultsTests.cs b/src/tests/DotNetProjectSdk.IntegrationTests/RoslynComponentDefaultsTests.cs
index ad1dec6..64299f1 100644
--- a/src/tests/DotNetProjectSdk.IntegrationTests/RoslynComponentDefaultsTests.cs
+++ b/src/tests/DotNetProjectSdk.IntegrationTests/RoslynComponentDefaultsTests.cs
@@ -143,6 +143,7 @@ public async Task IsRoslynComponent_True_AppliesSourceGeneratorDefaults(Cancella
"EnforceExtendedAnalyzerRules",
"DisableSourceLink",
"EmbedUntrackedSources",
+ "Deterministic",
"GenerateDependencyFile",
"CompilerGeneratedFilesOutputPath",
"SymbolPackageFormat",
@@ -152,8 +153,18 @@ public async Task IsRoslynComponent_True_AppliesSourceGeneratorDefaults(Cancella
await Assert.That(properties["TargetFramework"]).IsEqualTo("netstandard2.0");
await Assert.That(properties["EnforceExtendedAnalyzerRules"]).IsEqualTo("true");
- await Assert.That(properties["DisableSourceLink"]).IsEqualTo("true");
- await Assert.That(properties["EmbedUntrackedSources"]).IsEqualTo("false");
+ await Assert
+ .That(properties["DisableSourceLink"])
+ .IsNotEqualTo("true")
+ .Because("Roslyn components must ship source-linked PDBs like regular packages.");
+ await Assert
+ .That(properties["EmbedUntrackedSources"])
+ .IsEqualTo("true")
+ .Because("Roslyn components must embed untracked sources for full SourceLink parity.");
+ await Assert
+ .That(properties["Deterministic"])
+ .IsEqualTo("true")
+ .Because("All builds, including analyzer packages, must be deterministic.");
await Assert.That(properties["GenerateDependencyFile"]).IsEqualTo("false");
await Assert
.That(TestHelpers.NormalizePath(properties["CompilerGeneratedFilesOutputPath"]))
@@ -349,4 +360,376 @@ await Assert
.IsTrue()
.Because("Microsoft.CodeAnalysis.Analyzers should flow as an analyzer-only development asset.");
}
+
+ [Test]
+ public async Task PackableRoslynComponent_IncludeSymbolsTrue_PdbStillInNupkg(CancellationToken cancellationToken)
+ {
+ using var harness = await ProjectHarness
+ .For("SourceGeneration")
+ .WithProjectFileContent(
+ """
+
+
+ true
+ true
+ true
+ true
+ snupkg
+
+
+ """
+ )
+ .BuildAsync(cancellationToken);
+
+ var packageDirectory = Path.Combine(harness.SolutionDirectory, "packages");
+ var packageVersion = $"0.0.0-integration-test-{Guid.NewGuid():N}";
+ var (exitCode, stdOut, stdErr) = await harness.RunMSBuildAsync(
+ $"-restore -t:Pack -p:PackageOutputPath=\"{packageDirectory}\" -p:PackageVersion={packageVersion} -p:Version={packageVersion}",
+ cancellationToken
+ );
+
+ await Assert.That(exitCode).IsEqualTo(0).Because(TestHelpers.GenerateError(stdOut, stdErr));
+
+ var nupkgFiles = Directory.GetFiles(packageDirectory, "Test.SourceGeneration.*.nupkg");
+ await Assert.That(nupkgFiles).HasSingleItem().Because("Only the main .nupkg is produced.");
+
+ using var package = await ZipFile.OpenReadAsync(nupkgFiles[0], cancellationToken);
+ var entries = package.Entries.Select(entry => entry.FullName).ToList();
+ await Assert.That(entries).Contains("analyzers/dotnet/cs/Test.SourceGeneration.dll");
+ await Assert
+ .That(entries)
+ .Contains("analyzers/dotnet/cs/Test.SourceGeneration.pdb")
+ .Because(
+ "The analyzer PDB must always ship in the .nupkg by default (PurviewPackAnalyzerPdb=true); the .snupkg cannot host analyzers/dotnet/cs symbols."
+ );
+ await Assert
+ .That(entries)
+ .Contains("lib/netstandard2.0/Test.SourceGeneration.dll")
+ .Because("IncludeBuildOutput=true keeps the library asset for the dual-role layout.");
+
+ var snupkgFiles = Directory.GetFiles(packageDirectory, "Test.SourceGeneration.*.snupkg");
+ await Assert.That(snupkgFiles).HasSingleItem().Because("The symbol package must be produced.");
+
+ using var symbolPackage = await ZipFile.OpenReadAsync(snupkgFiles[0], cancellationToken);
+ var symbolEntries = symbolPackage.Entries.Select(entry => entry.FullName).ToList();
+ await Assert
+ .That(symbolEntries)
+ .Contains("lib/netstandard2.0/Test.SourceGeneration.pdb")
+ .Because("The library PDB flows to the .snupkg.");
+ }
+
+ [Test]
+ public async Task PackableRoslynComponent_PurviewPackAnalyzerPdbFalse_OmitsPdbFromNupkg(
+ CancellationToken cancellationToken
+ )
+ {
+ using var harness = await ProjectHarness
+ .For("SourceGeneration")
+ .WithProjectFileContent(
+ """
+
+
+ true
+ true
+ true
+ true
+ snupkg
+ false
+
+
+ """
+ )
+ .BuildAsync(cancellationToken);
+
+ var packageDirectory = Path.Combine(harness.SolutionDirectory, "packages");
+ var packageVersion = $"0.0.0-integration-test-{Guid.NewGuid():N}";
+ var (exitCode, stdOut, stdErr) = await harness.RunMSBuildAsync(
+ $"-restore -t:Pack -p:PackageOutputPath=\"{packageDirectory}\" -p:PackageVersion={packageVersion} -p:Version={packageVersion}",
+ cancellationToken
+ );
+
+ await Assert.That(exitCode).IsEqualTo(0).Because(TestHelpers.GenerateError(stdOut, stdErr));
+
+ var nupkgFiles = Directory.GetFiles(packageDirectory, "Test.SourceGeneration.*.nupkg");
+ using var package = await ZipFile.OpenReadAsync(nupkgFiles[0], cancellationToken);
+ var entries = package.Entries.Select(entry => entry.FullName).ToList();
+ await Assert.That(entries).Contains("analyzers/dotnet/cs/Test.SourceGeneration.dll");
+ await Assert
+ .That(
+ entries.Any(entry =>
+ entry.StartsWith("analyzers/dotnet/cs/", StringComparison.OrdinalIgnoreCase)
+ && entry.EndsWith(".pdb", StringComparison.OrdinalIgnoreCase)
+ )
+ )
+ .IsFalse()
+ .Because("PurviewPackAnalyzerPdb=false opts the analyzer PDB out of the .nupkg.");
+ await Assert
+ .That(entries)
+ .Contains("lib/netstandard2.0/Test.SourceGeneration.dll")
+ .Because("IncludeBuildOutput=true keeps the library asset for the dual-role layout.");
+
+ var snupkgFiles = Directory.GetFiles(packageDirectory, "Test.SourceGeneration.*.snupkg");
+ await Assert.That(snupkgFiles).HasSingleItem().Because("The symbol package must be produced.");
+ }
+
+ [Test]
+ public async Task PackableRoslynComponent_MultipleAnalyzerRefs_SharedRuntimeDependencyPackedOnce(
+ CancellationToken cancellationToken
+ )
+ {
+ using var generatorOne = await ProjectHarness
+ .For("GeneratorOne")
+ .WithProjectFileContent(
+ """
+
+
+ true
+
+
+
+
+
+
+
+
+ """
+ )
+ .BuildAsync(cancellationToken);
+
+ using var generatorTwo = await ProjectHarness
+ .For("GeneratorTwo")
+ .WithSolutionDirectory(generatorOne.SolutionDirectory)
+ .WithProjectFileContent(
+ """
+
+
+ true
+
+
+
+
+
+
+
+
+ """
+ )
+ .BuildAsync(cancellationToken);
+
+ using var consumer = await ProjectHarness
+ .For("Consumer")
+ .WithSolutionDirectory(generatorOne.SolutionDirectory)
+ .WithProjectFileContent(
+ """
+
+
+ net10.0
+ true
+ true
+ true
+
+
+
+
+
+
+ """
+ )
+ .BuildAsync(cancellationToken);
+
+ var packageDirectory = Path.Combine(generatorOne.SolutionDirectory, "packages");
+ var packageVersion = $"0.0.0-integration-test-{Guid.NewGuid():N}";
+ var (exitCode, stdOut, stdErr) = await consumer.RunMSBuildAsync(
+ $"-restore -t:Pack -p:PackageOutputPath=\"{packageDirectory}\" -p:PackageVersion={packageVersion} -p:Version={packageVersion}",
+ cancellationToken
+ );
+
+ await Assert.That(exitCode).IsEqualTo(0).Because(TestHelpers.GenerateError(stdOut, stdErr));
+ var packagePath = Directory.GetFiles(packageDirectory, "Test.Consumer.*.nupkg").Single();
+ using var package = await ZipFile.OpenReadAsync(packagePath, cancellationToken);
+ var entries = package.Entries.Select(entry => entry.FullName).ToList();
+ await Assert.That(entries).Contains("analyzers/dotnet/cs/Test.GeneratorOne.dll");
+ await Assert.That(entries).Contains("analyzers/dotnet/cs/Test.GeneratorTwo.dll");
+ await Assert
+ .That(entries.Count(entry => entry == "analyzers/dotnet/cs/SharedRuntime.dll"))
+ .IsEqualTo(1)
+ .Because("A runtime dependency shared by two analyzer project references must be packed exactly once.");
+ }
+
+ [Test]
+ public async Task PackableRoslynComponent_MissingCompilerDefaults_FailsPack(CancellationToken cancellationToken)
+ {
+ using var harness = await ProjectHarness
+ .For("SourceGeneration")
+ .WithProjectFileContent(
+ """
+
+
+ true
+ true
+ false
+
+
+ """
+ )
+ .BuildAsync(cancellationToken);
+
+ var packageDirectory = Path.Combine(harness.SolutionDirectory, "packages");
+ var packageVersion = $"0.0.0-integration-test-{Guid.NewGuid():N}";
+ var (exitCode, stdOut, stdErr) = await harness.RunMSBuildAsync(
+ $"-restore -t:Pack -p:PackageOutputPath=\"{packageDirectory}\" -p:PackageVersion={packageVersion} -p:Version={packageVersion}",
+ cancellationToken
+ );
+
+ await Assert
+ .That(exitCode)
+ .IsEqualTo(1)
+ .Because("A packable Roslyn component that disables TreatWarningsAsErrors must fail the pack.")
+ .Because(TestHelpers.GenerateError(stdOut, stdErr));
+ await Assert
+ .That(stdOut)
+ .Contains("PRSGD0003")
+ .Because("The missing compiler-default diagnostic must be emitted.");
+ }
+
+ [Test]
+ public async Task PackableRoslynComponent_MissingCompilerDefaults_CanBeDisabled(CancellationToken cancellationToken)
+ {
+ using var harness = await ProjectHarness
+ .For("SourceGeneration")
+ .WithProjectFileContent(
+ """
+
+
+ true
+ true
+ false
+ true
+
+
+ """
+ )
+ .BuildAsync(cancellationToken);
+
+ var packageDirectory = Path.Combine(harness.SolutionDirectory, "packages");
+ var packageVersion = $"0.0.0-integration-test-{Guid.NewGuid():N}";
+ var (exitCode, stdOut, stdErr) = await harness.RunMSBuildAsync(
+ $"-restore -t:Pack -p:PackageOutputPath=\"{packageDirectory}\" -p:PackageVersion={packageVersion} -p:Version={packageVersion}",
+ cancellationToken
+ );
+
+ await Assert
+ .That(exitCode)
+ .IsEqualTo(0)
+ .Because("DisableRoslynCompilerDefaultsValidation=true must silence the pack-time validation.")
+ .Because(TestHelpers.GenerateError(stdOut, stdErr));
+ }
+
+ [Test]
+ public async Task RoslynComponent_ExposesCompilerSettingsToCompiler(CancellationToken cancellationToken)
+ {
+ using var harness = await ProjectHarness
+ .For("SourceGeneration")
+ .WithProjectFileContent(
+ """
+
+
+ true
+
+
+ """
+ )
+ .BuildAsync(cancellationToken);
+
+ var compilerVisible = await harness.GetItemIdentitiesAsync("CompilerVisibleProperty", cancellationToken);
+ await Assert.That(compilerVisible).Contains("LangVersion");
+ await Assert.That(compilerVisible).Contains("Nullable");
+ await Assert.That(compilerVisible).Contains("TreatWarningsAsErrors");
+ await Assert.That(compilerVisible).Contains("EnforceExtendedAnalyzerRules");
+ await Assert.That(compilerVisible).Contains("Deterministic");
+ await Assert.That(compilerVisible).Contains("ContinuousIntegrationBuild");
+ await Assert.That(compilerVisible).Contains("EmbedUntrackedSources");
+ }
+
+ [Test]
+ public async Task RoslynComponent_SourceLinkPackageReference_Added(CancellationToken cancellationToken)
+ {
+ using var harness = await ProjectHarness
+ .For("SourceGeneration")
+ .WithProjectFileContent(
+ """
+
+
+ true
+
+
+ """
+ )
+ .BuildAsync(cancellationToken);
+
+ var packageReferences = await harness.GetItemIdentitiesAsync("PackageReference", cancellationToken);
+ await Assert
+ .That(packageReferences)
+ .Contains("Microsoft.SourceLink.GitHub")
+ .Because("Roslyn components must receive the SourceLink package like regular packages.");
+ }
+
+ [Test]
+ public async Task PackableRoslynComponent_LinkedSdkReadme_DoesNotDuplicate(CancellationToken cancellationToken)
+ {
+ var repoRoot = Path.Combine(Path.GetTempPath(), "PurviewSdkTests", Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(repoRoot);
+ await File.WriteAllTextAsync(Path.Combine(repoRoot, "README.md"), "# Test Repo Readme", cancellationToken);
+ await File.WriteAllTextAsync(
+ Path.Combine(repoRoot, "package.json"),
+ /*lang=json,strict*/"""{"name": "test-repo", "version": "0.0.0-test"}""",
+ cancellationToken
+ );
+
+ using var harness = await ProjectHarness
+ .For("SourceGeneration")
+ .WithSolutionDirectory(Path.Combine(repoRoot, "src"))
+ .WithProjectFileContent(
+ """
+
+
+ true
+ true
+ README.md
+
+
+
+
+
+ """
+ )
+ .BuildAsync(cancellationToken);
+
+ var packageDirectory = Path.Combine(harness.SolutionDirectory, "packages");
+ var packageVersion = $"0.0.0-integration-test-{Guid.NewGuid():N}";
+ var (exitCode, stdOut, stdErr) = await harness.RunMSBuildAsync(
+ $"-restore -t:Pack -p:PackageOutputPath=\"{packageDirectory}\" -p:PackageVersion={packageVersion} -p:Version={packageVersion} -p:RepoRoot=\"{repoRoot}\"",
+ cancellationToken
+ );
+
+ await Assert.That(exitCode).IsEqualTo(0).Because(TestHelpers.GenerateError(stdOut, stdErr));
+
+ var nupkgFiles = Directory.GetFiles(packageDirectory, "Test.SourceGeneration.*.nupkg");
+ using var package = await ZipFile.OpenReadAsync(nupkgFiles[0], cancellationToken);
+ var entries = package.Entries.Select(entry => entry.FullName).ToList();
+ await Assert
+ .That(entries.Count(entry => entry == "README.md"))
+ .IsEqualTo(1)
+ .Because("A linked Sdk/README.md must not be packed twice (no NU5118).");
+ }
}