Files
UA-.NETStandard/tools/SourceGeneratorPack.targets
Marc Schier 00664404c0 Ship the OPC Foundation license file from every package and fix the NU5033 pack failure (#4174)
# Description

The `preview-pack` stage of the Azure DevOps pipeline fails with nine `NU5033` errors, which fails the whole build:

```
NuGet.Build.Tasks.Pack.targets(222,5): error NU5033: Invalid metadata.
Cannot specify both a PackageLicenseExpression and a PackageLicenseFile.
[src\Opc.Ua.Robotics\Opc.Ua.Robotics.csproj]
```

`common.props` gives every package the shared `LICENSE.txt` as its `<PackageLicenseFile>`, but **19** packages also declared `<PackageLicenseExpression>MIT</PackageLicenseExpression>`. Nine of them hit `NU5033`; the other ten only avoided it by carrying an empty `<PackageLicenseFile></PackageLicenseFile>` workaround.

That declaration was wrong on two counts.

**It advertised the wrong license.** `LICENSE.txt` is the **OPC Foundation MIT License 1.00**, which carries the OPC Foundation copyright and preamble — it is a different text than the plain SPDX `MIT` license. The affected packages therefore published `<license type="expression">MIT</license>` plus a `licenses.nuget.org/MIT` URL on nuget.org, instead of the OPC Foundation text that every other package in the stack ships.

**It also broke the build,** because NuGet refuses to pack a project carrying both a license expression and a license file, failing `pack` solution-wide rather than just for the offending projects.

## Changes

The expression is dropped from all 19 packages so every one uses the shared OPC Foundation license file uniformly, like the rest of the stack:

* `src/Opc.Ua.OpenUsd`, `.Client`, `.Server`
* `src/Opc.Ua.OpenUsdScene`, `.Server`
* `src/Opc.Ua.Robotics`, `.Client`, `.Server`
* `src/Opc.Ua.Positioning`, `.Client`, `.Server`
* `src/Opc.Ua.Di`, `src/Opc.Ua.ISA95`, `src/Opc.Ua.WotCon`, `src/Opc.Ua.Gds.Common`
* `tools/Opc.Ua.OpenUsd.Connector.Viewer`, `tools/Opc.Ua.SourceGeneration.Core`
* `tools/Opc.Ua.SourceGeneration.Pack`, `tools/Opc.Ua.SourceGeneration.Stack.Pack` — these two set the license not in their own `.csproj` but in the shared `tools/SourceGeneratorPack.targets`, so they were only caught once the new guard ran in CI.

The eight per-project empty `<PackageLicenseFile></PackageLicenseFile>` workarounds go away with it.

To stop the whole class of failure from recurring, `Directory.Build.targets` now *enforces* the rule rather than papering over it. Silently clearing the inherited license file would have fixed the build while still publishing the wrong license metadata, so instead a violation fails fast with an actionable error naming the project:

```
error OPCUA0001: Opc.Ua.Foo sets PackageLicenseExpression=MIT. Every package must
ship the OPC Foundation MIT License 1.00 via the shared LICENSE.txt that
common.props sets as PackageLicenseFile, which the SPDX expression neither
matches nor is allowed to coexist with (NU5033). Remove the
PackageLicenseExpression property.
```

`common.props` cannot catch this itself: it is imported from `Directory.Build.props`, i.e. **before** the project body, so it never observes a project-level license expression. `Directory.Build.targets` is imported **after** the project body and is the earliest place that can. The `common.props` comment that previously hinted the expression was an option now points at this rule.

### Verification

* Every packable project evaluates to `PackageLicenseFile=LICENSE.txt` with an empty `PackageLicenseExpression`; file-licensed packages such as `Opc.Ua.Client` are unchanged.
* `dotnet pack -c Release` succeeds for `Opc.Ua.Robotics` (previously failing) and `Opc.Ua.SourceGeneration.Core` (previously relying on the removed workaround).
* Replicated the CI build commands locally — `dotnet build` of `UA.slnx` (net48/Debug), `tools/SourceGeneration.slnx` (net472/Release) and `tools/MigrationAnalyzer.slnx` (net48/Debug) all succeed with 0 errors.
* Inspected the produced `.nupkg`: now `<license type="file">LICENSE.txt</license>` with the license text at the package root, matching the rest of the stack (it was `<license type="expression">MIT</license>` before).
* Confirmed the guard fires on a simulated violation (`-p:PackageLicenseExpression=MIT`) and is inert otherwise.

## Related Issues

No tracking issue — this fixes the `NU5033` failure in the OPC Foundation Azure DevOps pipeline (build 16247, `preview-pack` stage) and corrects the license metadata published for the affected packages. No product code or public API changes.

## Checklist

_Put an `x` in the boxes that apply. You can complete these step by step after opening the PR._

- [x] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc.
- [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. _(Build-infrastructure change with no code surface. Validated by `dotnet pack` of the affected projects, by inspecting the resulting package metadata, and by confirming the new `OPCUA0001` guard fires on a simulated violation. The pipeline `preview-pack` stage is the regression test.)_
- [x] I have added all necessary documentation. _(The rule is documented in place, on the new `Directory.Build.targets` guard and at the `PackageLicenseFile` definition in `common.props`.)_
- [x] I have verified that my changes do not introduce (new) build or analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
2026-08-04 18:49:50 +00:00

95 lines
5.5 KiB
XML

<Project>
<!--
Packaging shared by the source generator `*.Pack` projects.
A pack project owns nothing but the package identity; it lists the generator
variants it ships in `@(SourceGeneratorVariant)`, each stamped with the
`analyzers/dotnet/<folder>/cs` folder its Roslyn API version maps to.
The payload is collected by globbing each variant's output directory rather
than by enumerating assemblies, so any package or project added to a
generator's dependency tree flows into every Roslyn folder without touching
a project file.
Assemblies the Roslyn host itself supplies are excluded. Shipping a second
copy of Microsoft.CodeAnalysis*, System.Collections.Immutable or
System.Reflection.Metadata makes the analyzer load context bind a different
identity for types the compiler passes across its own API surface (most
visibly ImmutableArray<T>), which fails at generator-initialization time with
MissingMethodException - reported as warning CS8784, i.e. silently no
generated code. See roslyn.props for the full rationale.
The `ProjectReference` entries exist purely for build ordering and NuGet
graph correctness. `ReferenceOutputAssembly` is off, the assemblies are
taken from disk. That also keeps packing with `NoBuild` working, where the
variants were built by a preceding solution build.
-->
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsPackable>true</IsPackable>
<IncludeBuildOutput>false</IncludeBuildOutput>
<!-- The analyzer payload is custom package content, so no build-output symbol package is produced. -->
<IncludeSymbols>false</IncludeSymbols>
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
<DevelopmentDependency>true</DevelopmentDependency>
<NoPackageAnalysis>true</NoPackageAnalysis>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<PackageReadmeFile>NugetREADME.md</PackageReadmeFile>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<!-- The pack project carries no code and its assembly is never shipped, so
there is nothing for the analyzers to inspect. -->
<RunAnalyzers>false</RunAnalyzers>
<EnableNETAnalyzers>false</EnableNETAnalyzers>
<!-- Spelling intentionally matches the (misspelled) condition in common.props,
which is the only thing that reads this property. -->
<NoRoslynatorAnalzers>true</NoRoslynatorAnalzers>
<TargetsForTfmSpecificContentInPackage>
$(TargetsForTfmSpecificContentInPackage);IncludeSourceGeneratorAnalyzerAssets
</TargetsForTfmSpecificContentInPackage>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<PackageId>$(PackageId).Debug</PackageId>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="@(SourceGeneratorVariant)">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<PrivateAssets>all</PrivateAssets>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="NugetREADME.md" Pack="true" PackagePath="\" />
</ItemGroup>
<Target Name="IncludeSourceGeneratorAnalyzerAssets">
<PropertyGroup>
<_SourceGeneratorVariantTargetFramework>netstandard2.0</_SourceGeneratorVariantTargetFramework>
<_SourceGeneratorVariantBin>bin\$(Configuration)\$(_SourceGeneratorVariantTargetFramework)</_SourceGeneratorVariantBin>
</PropertyGroup>
<ItemGroup>
<_SourceGeneratorVariantAssembly
Include="%(SourceGeneratorVariant.RootDir)%(SourceGeneratorVariant.Directory)$(_SourceGeneratorVariantBin)\*.dll"
Exclude="%(SourceGeneratorVariant.RootDir)%(SourceGeneratorVariant.Directory)$(_SourceGeneratorVariantBin)\Microsoft.CodeAnalysis*.dll;
%(SourceGeneratorVariant.RootDir)%(SourceGeneratorVariant.Directory)$(_SourceGeneratorVariantBin)\System.Collections.Immutable.dll;
%(SourceGeneratorVariant.RootDir)%(SourceGeneratorVariant.Directory)$(_SourceGeneratorVariantBin)\System.Reflection.Metadata.dll">
<RoslynAnalyzerFolder>%(SourceGeneratorVariant.RoslynAnalyzerFolder)</RoslynAnalyzerFolder>
</_SourceGeneratorVariantAssembly>
<TfmSpecificPackageFile Include="@(_SourceGeneratorVariantAssembly)">
<PackagePath>analyzers/dotnet/%(_SourceGeneratorVariantAssembly.RoslynAnalyzerFolder)/cs/%(_SourceGeneratorVariantAssembly.Filename)%(_SourceGeneratorVariantAssembly.Extension)</PackagePath>
</TfmSpecificPackageFile>
</ItemGroup>
<!-- Guard per variant, not over the flattened list: with N variants declared,
an aggregate emptiness check still passes when N-1 of them produced
nothing, which would silently ship a package missing a Roslyn band. -->
<PropertyGroup>
<_SourceGeneratorProducedFolders>@(_SourceGeneratorVariantAssembly->'%(RoslynAnalyzerFolder)'->Distinct(), ';')</_SourceGeneratorProducedFolders>
</PropertyGroup>
<ItemGroup>
<_SourceGeneratorVariantWithoutAssets Include="@(SourceGeneratorVariant)"
Condition="!$(_SourceGeneratorProducedFolders.Contains('%(SourceGeneratorVariant.RoslynAnalyzerFolder)'))" />
</ItemGroup>
<Error Condition="'@(_SourceGeneratorVariantWithoutAssets)' != ''"
Text="No analyzer assemblies found under '$(_SourceGeneratorVariantBin)' for @(_SourceGeneratorVariantWithoutAssets->'%(Filename)', ', ') while packing $(PackageId). Build every variant project for configuration '$(Configuration)' first." />
</Target>
</Project>