Skip to content

Commit dc526e1

Browse files
committed
License validation
Add predefined licenses and copyrights Replace PackageLicenseUrl with PackageLicenseExpression(Internal)
1 parent 22b4f97 commit dc526e1

15 files changed

+828
-64
lines changed

Documentation/ArcadeSdk.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,12 @@ It is a common practice to specify properties applicable to all (most) projects
359359

360360
<!-- Public keys used by InternalsVisibleTo project items -->
361361
<MoqPublicKey>00240000048000009400...</MoqPublicKey>
362+
363+
<!--
364+
Specify license used for packages produced by the repository.
365+
Use PackageLicenseExpressionInternal for closed-source licenses.
366+
-->
367+
<PackageLicenseExpression>MIT</PackageLicenseExpression>
362368
</PropertyGroup>
363369
```
364370

@@ -372,6 +378,20 @@ It is a common practice to specify properties applicable to all (most) projects
372378
</Project>
373379
```
374380

381+
### /License.txt
382+
383+
The root of the repository shall include a license file named `license.txt`, `license.md` or `license` (any casing is allowed).
384+
It is expected that all packages built from the repository have the same license, which is the license declared in the repository root license file.
385+
386+
If the repository uses open source license it shall specify the license name globally using `PackageLicenseExpression` property, e.g. in [Directory.Build.props](https://github.com/dotnet/arcade/blob/master/Documentation/ArcadeSdk.md#directorybuildprops).
387+
If the repository uses a closed source license it shall specify the license name using `PackageLicenseExpressionInternal` property. In this case the closed source license file is automatically added to any package build by the repository.
388+
389+
If `PackageLicenseExpression(Internal)` property is set Arcade SDK validates that the content of the license file in the repository root matches the content of
390+
the [well-known license file](https://github.com/dotnet/arcade/tree/master/src/Microsoft.DotNet.Arcade.Sdk/tools/Licenses) that corresponds to the value of the license expression.
391+
This validation can be suppressed by setting `SuppressLicenseValidation` to `true` if necessary (not recommended).
392+
393+
See [NuGet documentation](https://docs.microsoft.com/en-us/nuget/reference/msbuild-targets#packing-a-license-expression-or-a-license-file) for details.
394+
375395
### Source Projects
376396

377397
Projects are located under `src` directory under root repo, in any subdirectory structure appropriate for the repo.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.IO;
7+
using Xunit;
8+
9+
namespace Microsoft.DotNet.Arcade.Sdk.Tests
10+
{
11+
public class GetLicenseFilePathTests
12+
{
13+
[Theory]
14+
[InlineData("licenSe.TXT")]
15+
[InlineData("license.md")]
16+
[InlineData("LICENSE")]
17+
public void GetLicenseFilePath(string licenseFileName)
18+
{
19+
var dir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
20+
Directory.CreateDirectory(dir);
21+
var licensePath = Path.Combine(dir, licenseFileName);
22+
23+
File.WriteAllText(licensePath, "");
24+
25+
var task = new GetLicenseFilePath()
26+
{
27+
Directory = dir
28+
};
29+
30+
bool result = task.Execute();
31+
Assert.Equal(licensePath, task.Path);
32+
Assert.True(result);
33+
34+
Directory.Delete(dir, recursive: true);
35+
}
36+
}
37+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using Xunit;
6+
7+
namespace Microsoft.DotNet.Arcade.Sdk.Tests
8+
{
9+
public class ValidateLicenseTests
10+
{
11+
[Fact]
12+
public void LinesEqual()
13+
{
14+
Assert.False(ValidateLicense.LinesEqual(new[] { "a" }, new[] { "b" }));
15+
Assert.False(ValidateLicense.LinesEqual(new[] { "a" }, new[] { "A" }));
16+
Assert.False(ValidateLicense.LinesEqual(new[] { "a" }, new[] { "a", "b" }));
17+
Assert.False(ValidateLicense.LinesEqual(new[] { "a" }, new[] { "a", "*ignore-line*" }));
18+
Assert.False(ValidateLicense.LinesEqual(new[] { "*ignore-line*" }, new[] { "a" }));
19+
Assert.True(ValidateLicense.LinesEqual(new[] { "a" }, new[] { "*ignore-line*" }));
20+
21+
Assert.True(ValidateLicense.LinesEqual(new[] { "a", " ", " b", "xxx", "\t \t" }, new[] { "a", "b ", "*ignore-line*" }));
22+
}
23+
}
24+
}

src/Microsoft.DotNet.Arcade.Sdk/Microsoft.DotNet.Arcade.Sdk.csproj

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
<!-- Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE file in the project root for more information. -->
2-
1+
<!-- Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE file in the project root for more information. -->
32
<Project Sdk="Microsoft.NET.Sdk">
43
<PropertyGroup>
54
<TargetFrameworks>net472;netcoreapp2.1</TargetFrameworks>
@@ -28,6 +27,10 @@
2827
<PackageReference Include="System.Reflection.Metadata" Version="$(SystemReflectionMetadataVersion)" />
2928
</ItemGroup>
3029

30+
<ItemGroup>
31+
<InternalsVisibleTo Include="Microsoft.DotNet.Arcade.Sdk.Tests"/>
32+
</ItemGroup>
33+
3134
<ItemGroup>
3235
<None Include="sdk/Sdk.props;sdk/Sdk.targets" Pack="true">
3336
<PackagePath>sdk/%(Filename)%(Extension)</PackagePath>
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.Linq;
8+
using Microsoft.Build.Framework;
9+
using Microsoft.Build.Utilities;
10+
11+
namespace Microsoft.DotNet.Arcade.Sdk
12+
{
13+
/// <summary>
14+
/// Finds a license file in the given directory.
15+
/// File is considered a license file if its name matches 'license(.txt|.md|)', ignoring case.
16+
/// </summary>
17+
public class GetLicenseFilePath : Task
18+
{
19+
/// <summary>
20+
/// Full path to the directory to search for the license file.
21+
/// </summary>
22+
[Required]
23+
public string Directory { get; set; }
24+
25+
/// <summary>
26+
/// Full path to the license file, or empty if it is not found.
27+
/// </summary>
28+
[Output]
29+
public string Path { get; private set; }
30+
31+
public override bool Execute()
32+
{
33+
ExecuteImpl();
34+
return !Log.HasLoggedErrors;
35+
}
36+
37+
private void ExecuteImpl()
38+
{
39+
const string fileName = "license";
40+
41+
#if NET472
42+
IEnumerable<string> enumerateFiles(string extension) =>
43+
System.IO.Directory.EnumerateFiles(Directory, fileName + extension, SearchOption.TopDirectoryOnly);
44+
#else
45+
var options = new EnumerationOptions
46+
{
47+
MatchCasing = MatchCasing.CaseInsensitive,
48+
RecurseSubdirectories = false,
49+
MatchType = MatchType.Simple
50+
};
51+
52+
options.AttributesToSkip |= FileAttributes.Directory;
53+
54+
IEnumerable<string> enumerateFiles(string extension) =>
55+
System.IO.Directory.EnumerateFileSystemEntries(Directory, fileName + extension, options);
56+
#endif
57+
var matches =
58+
(from extension in new[] { ".txt", ".md", "" }
59+
from path in enumerateFiles(extension)
60+
select path).ToArray();
61+
62+
if (matches.Length == 0)
63+
{
64+
Log.LogError($"No license file found in '{Directory}'.");
65+
}
66+
else if (matches.Length > 1)
67+
{
68+
Log.LogError($"Multiple license files found in '{Directory}': '{string.Join("', '", matches)}'.");
69+
}
70+
else
71+
{
72+
Path = matches[0];
73+
}
74+
}
75+
}
76+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.Linq;
8+
using System.Text;
9+
using Microsoft.Build.Framework;
10+
using Microsoft.Build.Utilities;
11+
12+
namespace Microsoft.DotNet.Arcade.Sdk
13+
{
14+
/// <summary>
15+
/// Checks that the content of two license files is the same modulo line breaks, leading and trailing whitespace.
16+
/// </summary>
17+
public class ValidateLicense : Task
18+
{
19+
/// <summary>
20+
/// Full path to the file that contains the license text to be validated.
21+
/// </summary>
22+
[Required]
23+
public string LicensePath { get; set; }
24+
25+
/// <summary>
26+
/// Full path to the file that contains expected license text.
27+
/// </summary>
28+
[Required]
29+
public string ExpectedLicensePath { get; set; }
30+
31+
public override bool Execute()
32+
{
33+
ExecuteImpl();
34+
return !Log.HasLoggedErrors;
35+
}
36+
37+
private void ExecuteImpl()
38+
{
39+
var actualLines = File.ReadAllLines(LicensePath, Encoding.UTF8);
40+
var expectedLines = File.ReadAllLines(ExpectedLicensePath, Encoding.UTF8);
41+
42+
if (!LinesEqual(actualLines, expectedLines))
43+
{
44+
Log.LogError($"License file content '{LicensePath}' doesn't match the expected license '{ExpectedLicensePath}'.");
45+
}
46+
}
47+
48+
internal static bool LinesEqual(IEnumerable<string> actual, IEnumerable<string> expected)
49+
{
50+
IEnumerable<string> normalize(IEnumerable<string> lines)
51+
=> from line in lines
52+
where !string.IsNullOrWhiteSpace(line)
53+
select line.Trim();
54+
55+
var normalizedActual = normalize(actual).ToArray();
56+
var normalizedExpected = normalize(expected).ToArray();
57+
58+
if (normalizedActual.Length != normalizedExpected.Length)
59+
{
60+
return false;
61+
}
62+
63+
for (int i = 0; i < normalizedActual.Length; i++)
64+
{
65+
if (normalizedExpected[i] == "*ignore-line*")
66+
{
67+
continue;
68+
}
69+
70+
if (normalizedActual[i] != normalizedExpected[i])
71+
{
72+
return false;
73+
}
74+
}
75+
76+
return true;
77+
}
78+
79+
}
80+
}

src/Microsoft.DotNet.Arcade.Sdk/tools/Imports.targets

Lines changed: 2 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,45 +5,8 @@
55
Import NuGet targets to WPF temp projects (workaround for https://github.com/dotnet/sourcelink/issues/91)
66
-->
77
<Import Project="$(_WpfTempProjectNuGetFilePathNoExt).targets" Condition="'$(_WpfTempProjectNuGetFilePathNoExt)' != '' and Exists('$(_WpfTempProjectNuGetFilePathNoExt).targets')"/>
8-
9-
<PropertyGroup>
10-
<DeployProjectOutput Condition="'$(DeployProjectOutput)' == ''">$(__DeployProjectOutput)</DeployProjectOutput>
11-
12-
<!-- Run Deploy step by default when the solution is build directly via msbuild (from command line or VS). -->
13-
<DeployProjectOutput Condition="'$(DeployProjectOutput)' == ''">true</DeployProjectOutput>
14-
</PropertyGroup>
15-
16-
<!-- Default empty deploy target. -->
17-
<Target Name="Deploy" AfterTargets="Build" Condition="'$(DeployProjectOutput)' == 'true'" />
18-
19-
<PropertyGroup>
20-
<!--
21-
Unless specified otherwise project is assumed to produce artifacts (assembly, package, vsix, etc.) that ship.
22-
Test projects automatically set IsShipping to false.
23-
24-
Some projects may produce packages that contain shipping assemblies but the packages themselves do not ship.
25-
Thes projects shall specify IsShippingPackage=false and leave IsShipping unset (will default to true).
26-
27-
Targets that need to determine whether an artifact is shipping shall use the artifact specific IsShippingXxx property,
28-
if available for the kind of artifact they operate on.
29-
-->
30-
<IsShipping Condition="'$(IsShipping)' == ''">true</IsShipping>
31-
32-
<IsShippingAssembly Condition="'$(IsShippingAssembly)' == ''">$(IsShipping)</IsShippingAssembly>
33-
<IsShippingPackage Condition="'$(IsShippingPackage)' == ''">$(IsShipping)</IsShippingPackage>
34-
<IsShippingVsix Condition="'$(IsShippingVsix)' == ''">$(IsShipping)</IsShippingVsix>
35-
36-
<!--
37-
Set PackageOutputPath based on the IsShippingPackage flag set by projects.
38-
This distinction allows publishing tools to determine which assets to publish to official channels.
39-
-->
40-
<PackageOutputPath Condition="'$(IsShippingPackage)' == 'true'">$(ArtifactsShippingPackagesDir)</PackageOutputPath>
41-
<PackageOutputPath Condition="'$(IsShippingPackage)' != 'true'">$(ArtifactsNonShippingPackagesDir)</PackageOutputPath>
42-
43-
<IsSwixProject>false</IsSwixProject>
44-
<IsSwixProject Condition="'$(VisualStudioInsertionComponent)' != '' and '$(IsVsixProject)' != 'true'">true</IsSwixProject>
45-
</PropertyGroup>
46-
8+
9+
<Import Project="ProjectDefaults.targets"/>
4710
<Import Project="StrongName.targets"/>
4811
<Import Project="GenerateInternalsVisibleTo.targets" />
4912
<Import Project="GenerateResxSource.targets" />

0 commit comments

Comments
 (0)