Skip to content

Commit 5df0055

Browse files
authored
Ensure nuget packages expose license via SPDX expression (#3580)
1 parent b4b46a8 commit 5df0055

2 files changed

Lines changed: 157 additions & 1 deletion

File tree

lang/csharp/common.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
<!-- Reference: https://docs.microsoft.com/en-us/nuget/reference/msbuild-targets#pack-target -->
4949
<Copyright>Copyright © 2019 The Apache Software Foundation.</Copyright>
5050
<PackageIcon>logo.png</PackageIcon>
51-
<PackageLicenseFile>LICENSE</PackageLicenseFile>
51+
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
5252
<PackageReadmeFile>README.md</PackageReadmeFile>
5353
<PackageProjectUrl>https://avro.apache.org/</PackageProjectUrl>
5454
<PackageTags>Avro;Apache;Serialization;Binary;Json;Schema</PackageTags>
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* https://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
using System;
20+
using System.IO;
21+
using System.IO.Compression;
22+
using System.Linq;
23+
using System.Xml.Linq;
24+
using NUnit.Framework;
25+
26+
namespace Avro.Test.Utils
27+
{
28+
[TestFixture]
29+
public class NuGetPackageTests
30+
{
31+
private static readonly string[] PackageIds = new[]
32+
{
33+
"Apache.Avro",
34+
"Apache.Avro.Tools",
35+
"Apache.Avro.File.Snappy",
36+
"Apache.Avro.File.BZip2",
37+
"Apache.Avro.File.XZ",
38+
"Apache.Avro.File.Zstandard"
39+
};
40+
41+
[TestCaseSource(nameof(PackageIds))]
42+
public void PackageContainsSpdxLicenseExpression(string packageId)
43+
{
44+
var nupkgPath = FindPackageInBuildOutput(packageId);
45+
if (nupkgPath == null)
46+
{
47+
Assert.Inconclusive($"Package {packageId} not found. Run 'dotnet pack --configuration Release' first.");
48+
return;
49+
}
50+
51+
var nuspecXml = ExtractNuspecFromPackage(nupkgPath);
52+
53+
// Get the namespace from the root element
54+
var ns = nuspecXml.Root?.Name.Namespace ?? XNamespace.None;
55+
var licenseElement = nuspecXml.Root?.Element(ns + "metadata")?.Element(ns + "license");
56+
57+
Assert.That(licenseElement, Is.Not.Null,
58+
$"Package {packageId} does not contain a license element");
59+
60+
var licenseType = licenseElement?.Attribute("type")?.Value;
61+
Assert.That(licenseType, Is.EqualTo("expression"),
62+
$"Package {packageId} license type should be 'expression', but was '{licenseType}'");
63+
64+
var licenseValue = licenseElement?.Value;
65+
Assert.That(licenseValue, Is.EqualTo("Apache-2.0"),
66+
$"Package {packageId} should have SPDX license expression 'Apache-2.0', but was '{licenseValue}'");
67+
}
68+
69+
[TestCaseSource(nameof(PackageIds))]
70+
public void PackageContainsLicenseFile(string packageId)
71+
{
72+
var nupkgPath = FindPackageInBuildOutput(packageId);
73+
if (nupkgPath == null)
74+
{
75+
Assert.Inconclusive($"Package {packageId} not found. Run 'dotnet pack --configuration Release' first.");
76+
return;
77+
}
78+
79+
using (var archive = ZipFile.OpenRead(nupkgPath))
80+
{
81+
var licenseEntry = archive.Entries.FirstOrDefault(e =>
82+
e.FullName.Equals("LICENSE", StringComparison.OrdinalIgnoreCase));
83+
84+
Assert.That(licenseEntry, Is.Not.Null,
85+
$"Package {packageId} does not contain LICENSE file");
86+
87+
Assert.That(licenseEntry.Length, Is.GreaterThan(0),
88+
$"Package {packageId} LICENSE file is empty");
89+
}
90+
}
91+
92+
[TestCaseSource(nameof(PackageIds))]
93+
public void PackageLicenseFileContainsApacheLicense(string packageId)
94+
{
95+
var nupkgPath = FindPackageInBuildOutput(packageId);
96+
if (nupkgPath == null)
97+
{
98+
Assert.Inconclusive($"Package {packageId} not found. Run 'dotnet pack --configuration Release' first.");
99+
return;
100+
}
101+
102+
using (var archive = ZipFile.OpenRead(nupkgPath))
103+
{
104+
var licenseEntry = archive.Entries.FirstOrDefault(e =>
105+
e.FullName.Equals("LICENSE", StringComparison.OrdinalIgnoreCase));
106+
107+
Assert.That(licenseEntry, Is.Not.Null);
108+
109+
using (var stream = licenseEntry.Open())
110+
using (var reader = new StreamReader(stream))
111+
{
112+
var content = reader.ReadToEnd();
113+
Assert.That(content, Does.Contain("Apache License"),
114+
$"Package {packageId} LICENSE file does not contain Apache License text");
115+
Assert.That(content, Does.Contain("Version 2.0"),
116+
$"Package {packageId} LICENSE file does not specify Version 2.0");
117+
}
118+
}
119+
}
120+
121+
private string FindPackageInBuildOutput(string packageId)
122+
{
123+
// Find the lang/csharp root (4 levels up from test binary directory: bin/Release/net8.0 -> test -> apache -> src -> csharp)
124+
var testDir = TestContext.CurrentContext.TestDirectory;
125+
var csharpRoot = Path.GetFullPath(Path.Combine(testDir, "..", "..", "..", "..", ".."));
126+
127+
// Search for the package in Release output directories
128+
var pattern = $"{packageId}.*.nupkg";
129+
var files = Directory.GetFiles(csharpRoot, pattern, SearchOption.AllDirectories)
130+
.Where(f => f.Contains($"{Path.DirectorySeparatorChar}Release{Path.DirectorySeparatorChar}"))
131+
.OrderByDescending(f => System.IO.File.GetLastWriteTime(f))
132+
.ToArray();
133+
134+
return files.Length > 0 ? files[0] : null;
135+
}
136+
137+
private XDocument ExtractNuspecFromPackage(string nupkgPath)
138+
{
139+
using (var archive = ZipFile.OpenRead(nupkgPath))
140+
{
141+
var nuspecEntry = archive.Entries.FirstOrDefault(e =>
142+
e.FullName.EndsWith(".nuspec", StringComparison.OrdinalIgnoreCase));
143+
144+
if (nuspecEntry == null)
145+
{
146+
Assert.Fail($"No .nuspec file found in package: {nupkgPath}");
147+
}
148+
149+
using (var stream = nuspecEntry.Open())
150+
{
151+
return XDocument.Load(stream);
152+
}
153+
}
154+
}
155+
}
156+
}

0 commit comments

Comments
 (0)