Skip to content

NuGet package licenses #2003

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 14, 2019
Merged
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
20 changes: 20 additions & 0 deletions Documentation/ArcadeSdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,12 @@ It is a common practice to specify properties applicable to all (most) projects

<!-- Public keys used by InternalsVisibleTo project items -->
<MoqPublicKey>00240000048000009400...</MoqPublicKey>

<!--
Specify license used for packages produced by the repository.
Use PackageLicenseExpressionInternal for closed-source licenses.
-->
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
```

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

### /License.txt

The root of the repository shall include a license file named `license.txt`, `license.md` or `license` (any casing is allowed).
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.

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

If `PackageLicenseExpression(Internal)` property is set Arcade SDK validates that the content of the license file in the repository root matches the content of
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.
This validation can be suppressed by setting `SuppressLicenseValidation` to `true` if necessary (not recommended).

See [NuGet documentation](https://docs.microsoft.com/en-us/nuget/reference/msbuild-targets#packing-a-license-expression-or-a-license-file) for details.

### Source Projects

Projects are located under `src` directory under root repo, in any subdirectory structure appropriate for the repo.
Expand Down
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"tools": {
"dotnet": "2.1.401"
"dotnet": "2.1.503"
},
"msbuild-sdks": {
"Microsoft.DotNet.Arcade.Sdk": "1.0.0-beta.19162.7",
Expand Down
37 changes: 37 additions & 0 deletions src/Microsoft.DotNet.Arcade.Sdk.Tests/GetLicenseFilePathTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// 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.

using System;
using System.IO;
using Xunit;

namespace Microsoft.DotNet.Arcade.Sdk.Tests
{
public class GetLicenseFilePathTests
{
[Theory]
[InlineData("licenSe.TXT")]
[InlineData("license.md")]
[InlineData("LICENSE")]
public void GetLicenseFilePath(string licenseFileName)
{
var dir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(dir);
var licensePath = Path.Combine(dir, licenseFileName);

File.WriteAllText(licensePath, "");

var task = new GetLicenseFilePath()
{
Directory = dir
};

bool result = task.Execute();
Assert.Equal(licensePath, task.Path);
Assert.True(result);

Directory.Delete(dir, recursive: true);
}
}
}
24 changes: 24 additions & 0 deletions src/Microsoft.DotNet.Arcade.Sdk.Tests/ValidateLicenseTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// 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.

using Xunit;

namespace Microsoft.DotNet.Arcade.Sdk.Tests
{
public class ValidateLicenseTests
{
[Fact]
public void LinesEqual()
{
Assert.False(ValidateLicense.LinesEqual(new[] { "a" }, new[] { "b" }));
Assert.False(ValidateLicense.LinesEqual(new[] { "a" }, new[] { "A" }));
Assert.False(ValidateLicense.LinesEqual(new[] { "a" }, new[] { "a", "b" }));
Assert.False(ValidateLicense.LinesEqual(new[] { "a" }, new[] { "a", "*ignore-line*" }));
Assert.False(ValidateLicense.LinesEqual(new[] { "*ignore-line*" }, new[] { "a" }));
Assert.True(ValidateLicense.LinesEqual(new[] { "a" }, new[] { "*ignore-line*" }));

Assert.True(ValidateLicense.LinesEqual(new[] { "a", " ", " b", "xxx", "\t \t" }, new[] { "a", "b ", "*ignore-line*" }));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
<!-- 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. -->

<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net472;netcoreapp2.0</TargetFrameworks>
<TargetFrameworks>net472;netcoreapp2.1</TargetFrameworks>

<ExcludeFromSourceBuild>false</ExcludeFromSourceBuild>

Expand All @@ -28,6 +27,10 @@
<PackageReference Include="System.Reflection.Metadata" Version="$(SystemReflectionMetadataVersion)" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="Microsoft.DotNet.Arcade.Sdk.Tests"/>
</ItemGroup>

<ItemGroup>
<None Include="sdk/Sdk.props;sdk/Sdk.targets" Pack="true">
<PackagePath>sdk/%(Filename)%(Extension)</PackagePath>
Expand Down
76 changes: 76 additions & 0 deletions src/Microsoft.DotNet.Arcade.Sdk/src/GetLicenseFilePath.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// 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.

using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

namespace Microsoft.DotNet.Arcade.Sdk
{
/// <summary>
/// Finds a license file in the given directory.
/// File is considered a license file if its name matches 'license(.txt|.md|)', ignoring case.
/// </summary>
public class GetLicenseFilePath : Task
{
/// <summary>
/// Full path to the directory to search for the license file.
/// </summary>
[Required]
public string Directory { get; set; }

/// <summary>
/// Full path to the license file, or empty if it is not found.
/// </summary>
[Output]
public string Path { get; private set; }

public override bool Execute()
{
ExecuteImpl();
return !Log.HasLoggedErrors;
}

private void ExecuteImpl()
{
const string fileName = "license";

#if NET472
IEnumerable<string> enumerateFiles(string extension) =>
System.IO.Directory.EnumerateFiles(Directory, fileName + extension, SearchOption.TopDirectoryOnly);
#else
var options = new EnumerationOptions
{
MatchCasing = MatchCasing.CaseInsensitive,
RecurseSubdirectories = false,
MatchType = MatchType.Simple
};

options.AttributesToSkip |= FileAttributes.Directory;

IEnumerable<string> enumerateFiles(string extension) =>
System.IO.Directory.EnumerateFileSystemEntries(Directory, fileName + extension, options);
#endif
var matches =
(from extension in new[] { ".txt", ".md", "" }
from path in enumerateFiles(extension)
select path).ToArray();

if (matches.Length == 0)
{
Log.LogError($"No license file found in '{Directory}'.");
}
else if (matches.Length > 1)
{
Log.LogError($"Multiple license files found in '{Directory}': '{string.Join("', '", matches)}'.");
}
else
{
Path = matches[0];
}
}
}
}
80 changes: 80 additions & 0 deletions src/Microsoft.DotNet.Arcade.Sdk/src/ValidateLicense.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// 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.

using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

namespace Microsoft.DotNet.Arcade.Sdk
{
/// <summary>
/// Checks that the content of two license files is the same modulo line breaks, leading and trailing whitespace.
/// </summary>
public class ValidateLicense : Task
{
/// <summary>
/// Full path to the file that contains the license text to be validated.
/// </summary>
[Required]
public string LicensePath { get; set; }

/// <summary>
/// Full path to the file that contains expected license text.
/// </summary>
[Required]
public string ExpectedLicensePath { get; set; }

public override bool Execute()
{
ExecuteImpl();
return !Log.HasLoggedErrors;
}

private void ExecuteImpl()
{
var actualLines = File.ReadAllLines(LicensePath, Encoding.UTF8);
var expectedLines = File.ReadAllLines(ExpectedLicensePath, Encoding.UTF8);

if (!LinesEqual(actualLines, expectedLines))
{
Log.LogError($"License file content '{LicensePath}' doesn't match the expected license '{ExpectedLicensePath}'.");
}
}

internal static bool LinesEqual(IEnumerable<string> actual, IEnumerable<string> expected)
{
IEnumerable<string> normalize(IEnumerable<string> lines)
=> from line in lines
where !string.IsNullOrWhiteSpace(line)
select line.Trim();

var normalizedActual = normalize(actual).ToArray();
var normalizedExpected = normalize(expected).ToArray();

if (normalizedActual.Length != normalizedExpected.Length)
{
return false;
}

for (int i = 0; i < normalizedActual.Length; i++)
{
if (normalizedExpected[i] == "*ignore-line*")
{
continue;
}

if (normalizedActual[i] != normalizedExpected[i])
{
return false;
}
}

return true;
}

}
}
2 changes: 1 addition & 1 deletion src/Microsoft.DotNet.Arcade.Sdk/tools/BuildTasks.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
<Project>
<PropertyGroup>
<ArcadeSdkBuildTasksAssembly Condition="'$(MSBuildRuntimeType)' != 'Core'">$(MSBuildThisFileDirectory)net472\Microsoft.DotNet.Arcade.Sdk.dll</ArcadeSdkBuildTasksAssembly>
<ArcadeSdkBuildTasksAssembly Condition="'$(MSBuildRuntimeType)' == 'Core'">$(MSBuildThisFileDirectory)netcoreapp2.0\Microsoft.DotNet.Arcade.Sdk.dll</ArcadeSdkBuildTasksAssembly>
<ArcadeSdkBuildTasksAssembly Condition="'$(MSBuildRuntimeType)' == 'Core'">$(MSBuildThisFileDirectory)netcoreapp2.1\Microsoft.DotNet.Arcade.Sdk.dll</ArcadeSdkBuildTasksAssembly>
</PropertyGroup>
</Project>
41 changes: 2 additions & 39 deletions src/Microsoft.DotNet.Arcade.Sdk/tools/Imports.targets
Original file line number Diff line number Diff line change
Expand Up @@ -5,45 +5,8 @@
Import NuGet targets to WPF temp projects (workaround for https://github.com/dotnet/sourcelink/issues/91)
-->
<Import Project="$(_WpfTempProjectNuGetFilePathNoExt).targets" Condition="'$(_WpfTempProjectNuGetFilePathNoExt)' != '' and Exists('$(_WpfTempProjectNuGetFilePathNoExt).targets')"/>

<PropertyGroup>
<DeployProjectOutput Condition="'$(DeployProjectOutput)' == ''">$(__DeployProjectOutput)</DeployProjectOutput>

<!-- Run Deploy step by default when the solution is build directly via msbuild (from command line or VS). -->
<DeployProjectOutput Condition="'$(DeployProjectOutput)' == ''">true</DeployProjectOutput>
</PropertyGroup>

<!-- Default empty deploy target. -->
<Target Name="Deploy" AfterTargets="Build" Condition="'$(DeployProjectOutput)' == 'true'" />

<PropertyGroup>
<!--
Unless specified otherwise project is assumed to produce artifacts (assembly, package, vsix, etc.) that ship.
Test projects automatically set IsShipping to false.

Some projects may produce packages that contain shipping assemblies but the packages themselves do not ship.
Thes projects shall specify IsShippingPackage=false and leave IsShipping unset (will default to true).

Targets that need to determine whether an artifact is shipping shall use the artifact specific IsShippingXxx property,
if available for the kind of artifact they operate on.
-->
<IsShipping Condition="'$(IsShipping)' == ''">true</IsShipping>

<IsShippingAssembly Condition="'$(IsShippingAssembly)' == ''">$(IsShipping)</IsShippingAssembly>
<IsShippingPackage Condition="'$(IsShippingPackage)' == ''">$(IsShipping)</IsShippingPackage>
<IsShippingVsix Condition="'$(IsShippingVsix)' == ''">$(IsShipping)</IsShippingVsix>

<!--
Set PackageOutputPath based on the IsShippingPackage flag set by projects.
This distinction allows publishing tools to determine which assets to publish to official channels.
-->
<PackageOutputPath Condition="'$(IsShippingPackage)' == 'true'">$(ArtifactsShippingPackagesDir)</PackageOutputPath>
<PackageOutputPath Condition="'$(IsShippingPackage)' != 'true'">$(ArtifactsNonShippingPackagesDir)</PackageOutputPath>

<IsSwixProject>false</IsSwixProject>
<IsSwixProject Condition="'$(VisualStudioInsertionComponent)' != '' and '$(IsVsixProject)' != 'true'">true</IsSwixProject>
</PropertyGroup>


<Import Project="ProjectDefaults.targets"/>
<Import Project="StrongName.targets"/>
<Import Project="GenerateInternalsVisibleTo.targets" />
<Import Project="GenerateResxSource.targets" />
Expand Down
Loading