From da31ef0bb998033047eeed97ffabc6f5ab30f344 Mon Sep 17 00:00:00 2001 From: Frederik Carlier Date: Wed, 29 May 2019 19:05:35 +0200 Subject: [PATCH] Draft dotnet-thirdparty, a tool which can be used to generate third party notice files for .NET software. --- dotnet-packaging.sln | 6 + dotnet-thirdparty/GitHubLicense.cs | 22 +++ dotnet-thirdparty/GitHubProjectLicense.cs | 38 +++++ dotnet-thirdparty/ILicenseAnalysisService.cs | 10 ++ dotnet-thirdparty/ILicenseResolver.cs | 11 ++ dotnet-thirdparty/IProjectAssetsService.cs | 11 ++ dotnet-thirdparty/License.cs | 14 ++ dotnet-thirdparty/LicenseAnalysisService.cs | 118 +++++++++++++++ dotnet-thirdparty/LicenseResolver.cs | 135 ++++++++++++++++++ dotnet-thirdparty/Program.cs | 132 +++++++++++++++++ dotnet-thirdparty/ProjectAssetsService.cs | 35 +++++ dotnet-thirdparty/ProjectDiscoveryService.cs | 49 +++++++ dotnet-thirdparty/SpdxLicenseEntry.cs | 26 ++++ .../THIRD-PARTY-NOTICES.TXT.template | 22 +++ dotnet-thirdparty/ThirdPartyNotice.cs | 15 ++ .../CommandValidationException.cs | 17 +++ .../IProjectDiscoveryService.cs | 7 + dotnet-thirdparty/dotnet-outdated/LICENSE | 21 +++ .../ValidationErrorMessages.Designer.cs | 108 ++++++++++++++ .../ValidationErrorMessages.resx | 135 ++++++++++++++++++ dotnet-thirdparty/dotnet-thirdparty.csproj | 28 ++++ 21 files changed, 960 insertions(+) create mode 100644 dotnet-thirdparty/GitHubLicense.cs create mode 100644 dotnet-thirdparty/GitHubProjectLicense.cs create mode 100644 dotnet-thirdparty/ILicenseAnalysisService.cs create mode 100644 dotnet-thirdparty/ILicenseResolver.cs create mode 100644 dotnet-thirdparty/IProjectAssetsService.cs create mode 100644 dotnet-thirdparty/License.cs create mode 100644 dotnet-thirdparty/LicenseAnalysisService.cs create mode 100644 dotnet-thirdparty/LicenseResolver.cs create mode 100644 dotnet-thirdparty/Program.cs create mode 100644 dotnet-thirdparty/ProjectAssetsService.cs create mode 100644 dotnet-thirdparty/ProjectDiscoveryService.cs create mode 100644 dotnet-thirdparty/SpdxLicenseEntry.cs create mode 100644 dotnet-thirdparty/THIRD-PARTY-NOTICES.TXT.template create mode 100644 dotnet-thirdparty/ThirdPartyNotice.cs create mode 100644 dotnet-thirdparty/dotnet-outdated/CommandValidationException.cs create mode 100644 dotnet-thirdparty/dotnet-outdated/IProjectDiscoveryService.cs create mode 100644 dotnet-thirdparty/dotnet-outdated/LICENSE create mode 100644 dotnet-thirdparty/dotnet-outdated/ValidationErrorMessages.Designer.cs create mode 100644 dotnet-thirdparty/dotnet-outdated/ValidationErrorMessages.resx create mode 100644 dotnet-thirdparty/dotnet-thirdparty.csproj diff --git a/dotnet-packaging.sln b/dotnet-packaging.sln index fc6c115..14aebee 100644 --- a/dotnet-packaging.sln +++ b/dotnet-packaging.sln @@ -15,6 +15,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-rpm", "dotnet-rpm\do EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-deb", "dotnet-deb\dotnet-deb.csproj", "{CB69DD41-E3DA-4418-8A32-419E1B5642DD}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-thirdparty", "dotnet-thirdparty\dotnet-thirdparty.csproj", "{3018EDCF-625E-4A1B-85BB-6C575EB74771}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -45,6 +47,10 @@ Global {CB69DD41-E3DA-4418-8A32-419E1B5642DD}.Debug|Any CPU.Build.0 = Debug|Any CPU {CB69DD41-E3DA-4418-8A32-419E1B5642DD}.Release|Any CPU.ActiveCfg = Release|Any CPU {CB69DD41-E3DA-4418-8A32-419E1B5642DD}.Release|Any CPU.Build.0 = Release|Any CPU + {3018EDCF-625E-4A1B-85BB-6C575EB74771}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3018EDCF-625E-4A1B-85BB-6C575EB74771}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3018EDCF-625E-4A1B-85BB-6C575EB74771}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3018EDCF-625E-4A1B-85BB-6C575EB74771}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/dotnet-thirdparty/GitHubLicense.cs b/dotnet-thirdparty/GitHubLicense.cs new file mode 100644 index 0000000..43fe1a9 --- /dev/null +++ b/dotnet-thirdparty/GitHubLicense.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace DotNet.ThirdParty +{ + class GitHubLicense + { + [JsonProperty("key")] + public string Key { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("spdx_id")] + public string Spdxid { get; set; } + + [JsonProperty("url")] + public string Url { get; set; } + + [JsonProperty("node_id")] + public string NodeId { get; set; } + } +} diff --git a/dotnet-thirdparty/GitHubProjectLicense.cs b/dotnet-thirdparty/GitHubProjectLicense.cs new file mode 100644 index 0000000..ddfc7e1 --- /dev/null +++ b/dotnet-thirdparty/GitHubProjectLicense.cs @@ -0,0 +1,38 @@ +using Newtonsoft.Json; + +namespace DotNet.ThirdParty +{ + class GitHubProjectLicense + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("path")] + public string Path { get; set; } + + [JsonProperty("sha")] + public string Sha { get; set; } + + [JsonProperty("size")] + public int Size { get; set; } + + [JsonProperty("url")] + public string Url { get; set; } + + [JsonProperty("html_url")] + public string HtmlUrl { get; set; } + + [JsonProperty("git_url")] + public string GitUrl { get; set; } + + [JsonProperty("download_url")] + public string DownloadUrl { get; set; } + + [JsonProperty("type")] + public string Type { get; set; } + + // Content, encoding & links are ignored + [JsonProperty("license")] + public GitHubLicense License { get; set; } + } +} diff --git a/dotnet-thirdparty/ILicenseAnalysisService.cs b/dotnet-thirdparty/ILicenseAnalysisService.cs new file mode 100644 index 0000000..b25f9e8 --- /dev/null +++ b/dotnet-thirdparty/ILicenseAnalysisService.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace DotNet.ThirdParty +{ + interface ILicenseAnalysisService + { + Task> AnalyzeProject(string projectAssetsPath, string nugetPackageRoot, string licenseCacheDirectory); + } +} diff --git a/dotnet-thirdparty/ILicenseResolver.cs b/dotnet-thirdparty/ILicenseResolver.cs new file mode 100644 index 0000000..71ec040 --- /dev/null +++ b/dotnet-thirdparty/ILicenseResolver.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; + +namespace DotNet.ThirdParty +{ + interface ILicenseResolver + { + Task GetSpdxLicense(string identifier, string licenseCacheFolder); + Task GetLicenseFromUrl(string url, string licenseCacheFolder); + Task GetLicenseFromGitHubUrl(string url, string licenseCacheFolder); + } +} diff --git a/dotnet-thirdparty/IProjectAssetsService.cs b/dotnet-thirdparty/IProjectAssetsService.cs new file mode 100644 index 0000000..494b8d8 --- /dev/null +++ b/dotnet-thirdparty/IProjectAssetsService.cs @@ -0,0 +1,11 @@ +namespace DotNet.ThirdParty +{ + interface IProjectAssetsService + { + string ProjectAssetsPath { get; } + string NuGetPackageRoot { get; } + string IntermediateOutputPath { get; } + + bool AnalyzeProjectAsset(string projectFilePath, bool restore); + } +} diff --git a/dotnet-thirdparty/License.cs b/dotnet-thirdparty/License.cs new file mode 100644 index 0000000..5de1a31 --- /dev/null +++ b/dotnet-thirdparty/License.cs @@ -0,0 +1,14 @@ +namespace DotNet.ThirdParty +{ + class License + { + public string Name { get; set; } + public string Text { get; set; } + public string LicenseId { get; set; } + + public override string ToString() + { + return this.Name; + } + } +} diff --git a/dotnet-thirdparty/LicenseAnalysisService.cs b/dotnet-thirdparty/LicenseAnalysisService.cs new file mode 100644 index 0000000..1e5fa58 --- /dev/null +++ b/dotnet-thirdparty/LicenseAnalysisService.cs @@ -0,0 +1,118 @@ +using NuGet.Common; +using NuGet.Packaging.Core; +using NuGet.Packaging.Licenses; +using NuGet.ProjectModel; +using NuGet.Protocol; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO.Abstractions; +using System.Linq; +using System.Threading.Tasks; + +namespace DotNet.ThirdParty +{ + internal class LicenseAnalysisService : ILicenseAnalysisService + { + private readonly IFileSystem fileSystem; + private readonly ILicenseResolver licenseResolver; + + public LicenseAnalysisService(IFileSystem fileSystem, ILicenseResolver licenseResolver) + { + this.fileSystem = fileSystem; + this.licenseResolver = licenseResolver; + } + + public async Task> AnalyzeProject(string projectAssetsPath, string nugetPackageRoot, string licenseCacheDirectory) + { + List notices = new List(); + + // Load the lock file + string lockFilePath = this.fileSystem.Path.Combine(projectAssetsPath); + var lockFile = LockFileUtilities.GetLockFile(lockFilePath, NullLogger.Instance); + + // TODO: Make this selection dynamic + var target = lockFile.Targets[0]; + + foreach (var library in target.Libraries) + { + bool isDeployed = + library.RuntimeAssemblies.Any() + || library.NativeLibraries.Any() + || library.ContentFiles.Any(c => c.CopyToOutput); + + if (!isDeployed) + { + Debug.WriteLine($"Ignoring library {library.Name} because it has no runtime assemblies, native libraries or content files"); + continue; + } + + var packageIdentity = new PackageIdentity(library.Name, library.Version); + var packageInfo = LocalFolderUtility.GetPackageV3(nugetPackageRoot, packageIdentity, NullLogger.Instance); + + if (packageInfo == null) + { + Debug.WriteLine($"Ignoring library {library.Name} because its NuGet package could not be found"); + continue; + } + + var copyright = !string.IsNullOrWhiteSpace(packageInfo.Nuspec.GetCopyright()) ? packageInfo.Nuspec.GetCopyright() : $"Copyright (c) {packageInfo.Nuspec.GetAuthors()}"; + var licenseMetadata = packageInfo.Nuspec.GetLicenseMetadata(); + var licenseUrl = packageInfo.Nuspec.GetLicenseUrl(); + var projectUrl = packageInfo.Nuspec.GetProjectUrl(); + var packageName = !string.IsNullOrWhiteSpace(packageInfo.Nuspec.GetTitle()) ? packageInfo.Nuspec.GetTitle() : packageInfo.Nuspec.GetId(); + var packageUrl = packageInfo.Nuspec.GetProjectUrl(); + + if (copyright == null && licenseMetadata == null && licenseUrl == null) + { + Debug.WriteLine($"Ignoring library {library.Name} because no license information could be found"); + continue; + } + + License license = null; + + if (license == null + && licenseMetadata != null + && licenseMetadata.LicenseExpression.Type == LicenseExpressionType.License) + { + var nugetLicense = (NuGetLicense)licenseMetadata.LicenseExpression; + license = await this.licenseResolver.GetSpdxLicense(nugetLicense.Identifier, licenseCacheDirectory).ConfigureAwait(false); + } + else if (license == null + && licenseUrl != null + && licenseUrl.Contains("github.com/")) + { + license = await this.licenseResolver.GetLicenseFromGitHubUrl(licenseUrl, licenseCacheDirectory).ConfigureAwait(false); + } + else if (license == null + && projectUrl.Contains("github.com/")) + { + license = await this.licenseResolver.GetLicenseFromGitHubUrl(projectUrl, licenseCacheDirectory).ConfigureAwait(false); + } + else if (license == null + && licenseUrl != null) + { + license = await this.licenseResolver.GetLicenseFromUrl(licenseUrl, licenseCacheDirectory).ConfigureAwait(false); + } + + if (license != null) + { + notices.Add(new ThirdPartyNotice() + { + Copyright = copyright, + PackageName = packageName, + PackageUrl = packageUrl, + License = license + }); + } + else + { + Debug.WriteLine($"Ignoring library {library.Name} because no license information could be found"); + } + + } + + return notices; + + } + } +} diff --git a/dotnet-thirdparty/LicenseResolver.cs b/dotnet-thirdparty/LicenseResolver.cs new file mode 100644 index 0000000..41f26e4 --- /dev/null +++ b/dotnet-thirdparty/LicenseResolver.cs @@ -0,0 +1,135 @@ +using Newtonsoft.Json; +using System; +using System.IO; +using System.IO.Abstractions; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace DotNet.ThirdParty +{ + class LicenseResolver : ILicenseResolver + { + private readonly IFileSystem fileSystem; + private readonly JsonSerializer serializer = new JsonSerializer(); + private readonly SHA256 sha = SHA256.Create(); + + public LicenseResolver(IFileSystem fileSystem) + { + this.fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + } + + public async Task GetSpdxLicense(string identifier, string licenseCacheFolder) + { + var licensePath = await this.DownloadFile($"http://spdx.org/licenses/{identifier}.json", $"{identifier}.json", licenseCacheFolder).ConfigureAwait(false); + + using (var stream = this.fileSystem.File.OpenRead(licensePath)) + using (var streamReader = new StreamReader(stream)) + using (var jsonTextReader = new JsonTextReader(streamReader)) + { + var licenseEntry = serializer.Deserialize(jsonTextReader); + + return new License() + { + Name = licenseEntry.Name, + LicenseId = licenseEntry.LicenseId, + Text = licenseEntry.LicenseText + }; + } + } + + public async Task GetLicenseFromUrl(string url, string licenseCacheFolder) + { + var licensePath = await this.DownloadFile(url, licenseCacheFolder).ConfigureAwait(false); + + // Something went wrong downloading the license file. Bail out. + if (!this.fileSystem.File.Exists(licensePath)) + { + return null; + } + + return new License() + { + Text = this.fileSystem.File.ReadAllText(licensePath) + }; + } + + public async Task GetLicenseFromGitHubUrl(string url, string licenseCacheFolder) + { + // Get the owner and repo + string[] parts = url.Substring(8).Split(new char[] { '/' }); + var owner = parts[1]; + var repo = parts[2]; + + var licenseUrl = $"https://api.github.com/repos/{owner}/{repo}/license"; + var localName = $"github_{owner}_{repo}.json"; + + var licensePath = await this.DownloadFile(licenseUrl, localName, licenseCacheFolder).ConfigureAwait(false); + + // Something went wrong downloading the license file. Bail out. + if (!this.fileSystem.File.Exists(licensePath)) + { + return null; + } + + using (var stream = this.fileSystem.File.OpenRead(licensePath)) + using (var streamReader = new StreamReader(stream)) + using (var jsonTextReader = new JsonTextReader(streamReader)) + { + var licenseEntry = serializer.Deserialize(jsonTextReader); + + if (licenseEntry.License.Spdxid != "NOASSERTION") + { + return await this.GetSpdxLicense(licenseEntry.License.Spdxid, licenseCacheFolder); + } + else + { + var licenseText = await this.DownloadFile(licenseEntry.DownloadUrl, licenseCacheFolder); + + return new License() + { + Text = this.fileSystem.File.ReadAllText(licenseText) + }; + }; + } + } + + public Task DownloadFile(string url, string licenseCacheFolder) + { + var hash = BitConverter.ToString(this.sha.ComputeHash(Encoding.UTF8.GetBytes(url))).Replace("-", string.Empty); + return DownloadFile(url, hash, licenseCacheFolder); + } + + public async Task DownloadFile(string url, string localName, string licenseCacheFolder) + { + var localPath = this.fileSystem.Path.Combine(licenseCacheFolder, localName); + + if (!this.fileSystem.File.Exists(localPath)) + { + try + { + HttpClient client = new HttpClient(); + client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("dotnet-thirdparty", "1.0.0")); + + using (var response = await client.GetAsync(url).ConfigureAwait(false)) + using (var remoteStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) + using (var localStream = File.Open(localPath, FileMode.Create, FileAccess.Write)) + { + await remoteStream.CopyToAsync(localStream).ConfigureAwait(false); + + response.EnsureSuccessStatusCode(); + } + } + catch (HttpRequestException) + { + this.fileSystem.File.Delete(localPath); + return null; + } + } + + return localPath; + } + } +} diff --git a/dotnet-thirdparty/Program.cs b/dotnet-thirdparty/Program.cs new file mode 100644 index 0000000..5b5d6f6 --- /dev/null +++ b/dotnet-thirdparty/Program.cs @@ -0,0 +1,132 @@ +using DotNetOutdated.Exceptions; +using DotNetOutdated.Services; +using McMaster.Extensions.CommandLineUtils; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using Nustache.Core; +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; +using System.Threading.Tasks; + +namespace DotNet.ThirdParty +{ + class Program + { + static int Main(string[] args) + { + using (var services = new ServiceCollection() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .BuildServiceProvider()) + { + var app = new CommandLineApplication + { + ThrowOnUnexpectedArgument = true, + FullName = $"dotnet thirdparty", + LongVersionGetter = () => ThisAssembly.AssemblyInformationalVersion, + ExtendedHelpText = $"{Environment.NewLine}See https://github.com/qmfrederik/dotnet-packaging for more information" + }; + app.Conventions + .UseDefaultConventions() + .UseConstructorInjection(services); + + return app.Execute(args); + } + } + + private readonly IFileSystem fileSystem; + private readonly IProjectDiscoveryService projectDiscoveryService; + + [Argument(0, Description = "The path to a .csproj or .fsproj file, or to a directory containing a .NET Core project. " + + "If none is specified, the current directory will be used.")] + public string Path { get; set; } + + [Argument(1, "--no-restore", Description = "Do not restore the project before analyzing the third party license notices.")] + public bool SkipRestore { get; set; } = true; + + private readonly ILicenseAnalysisService licenseAnalysisService; + private readonly IProjectAssetsService projectAssetsService; + + public Program(IFileSystem fileSystem, IProjectDiscoveryService projectDiscoveryService, IProjectAssetsService projectAssetsService, ILicenseAnalysisService licenseAnalysisService) + { + this.fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + this.projectDiscoveryService = projectDiscoveryService ?? throw new ArgumentNullException(nameof(projectDiscoveryService)); + this.projectAssetsService = projectAssetsService ?? throw new ArgumentNullException(nameof(projectAssetsService)); + this.licenseAnalysisService = licenseAnalysisService ?? throw new ArgumentNullException(nameof(licenseAnalysisService)); + } + + public async Task OnExecute(CommandLineApplication app, IConsole console) + { + try + { + Console.WriteLine($"dotnet thirdparty ({ThisAssembly.AssemblyInformationalVersion})"); + + // If no path is set, use the current directory + if (string.IsNullOrEmpty(Path)) + Path = this.fileSystem.Directory.GetCurrentDirectory(); + + // Get all the projects + console.Write("Discovering project..."); + string projectFilePath = this.projectDiscoveryService.DiscoverProject(Path); + console.WriteLine(); + + console.Write($"Analyzing '{projectFilePath}' dependencies"); + if (!this.projectAssetsService.AnalyzeProjectAsset(projectFilePath, restore: !this.SkipRestore)) + { + return 1; + } + + var licenseCachePath = this.fileSystem.Path.Combine(this.projectAssetsService.IntermediateOutputPath, "ThirdPartyNotices"); + if (!this.fileSystem.Directory.Exists(licenseCachePath)) + { + this.fileSystem.Directory.CreateDirectory(licenseCachePath); + } + + var licenses = await this.licenseAnalysisService.AnalyzeProject(this.projectAssetsService.ProjectAssetsPath, this.projectAssetsService.NuGetPackageRoot, licenseCachePath).ConfigureAwait(false); + + // Cache the license information in a JSON file + using (var stream = this.fileSystem.File.Open(this.fileSystem.Path.Combine(this.projectAssetsService.IntermediateOutputPath, "ThirdPartyNotices", "licenses.json"), FileMode.Create, FileAccess.Write)) + using (var streamWriter = new StreamWriter(stream)) + using (var jsonTextWriter = new JsonTextWriter(streamWriter)) + { + JsonSerializer serializer = new JsonSerializer(); + serializer.Serialize(jsonTextWriter, licenses); + } + + Helpers.Register(nameof(TitleLine), TitleLine); + + // Inspiration: https://www.nexb.com/blog/oss_attribution_obligations.html + this.fileSystem.File.WriteAllText( + this.fileSystem.Path.Combine(this.projectAssetsService.IntermediateOutputPath, "ThirdPartyNotices", "THIRD-PARTY-NOTICES.TXT"), + Render.FileToString("THIRD-PARTY-NOTICES.TXT.template", licenses)); + + console.WriteLine(); + return 0; + } + catch (CommandValidationException e) + { + console.Error.WriteLine(e.Message); + + return 1; + } + } + + private static void TitleLine(RenderContext context, IList arguments, IDictionary options, RenderBlock fn, RenderBlock inverse) + { + if (arguments != null && arguments.Count > 0 && arguments[0] != null && arguments[0] is string) + { + context.Write(TitleLine(arguments[0] as string)); + } + } + + private static string TitleLine(string title) + { + return new string('-', title.Length); + } + } +} diff --git a/dotnet-thirdparty/ProjectAssetsService.cs b/dotnet-thirdparty/ProjectAssetsService.cs new file mode 100644 index 0000000..0c698a6 --- /dev/null +++ b/dotnet-thirdparty/ProjectAssetsService.cs @@ -0,0 +1,35 @@ +using Microsoft.Build.Locator; +using MSBuildProject = Microsoft.Build.Evaluation.Project; + +namespace DotNet.ThirdParty +{ + class ProjectAssetsService : IProjectAssetsService + { + static ProjectAssetsService() + { + MSBuildLocator.RegisterDefaults(); + } + + public string ProjectAssetsPath { get; private set; } + public string NuGetPackageRoot { get; private set; } + public string IntermediateOutputPath { get; private set; } + + public bool AnalyzeProjectAsset(string projectFilePath, bool restore) + { + var project = new MSBuildProject(projectFilePath); + + if (restore) + { + if (!project.Build("Restore")) + { + return false; + } + } + + this.ProjectAssetsPath = project.GetPropertyValue("ProjectAssetsFile"); + this.NuGetPackageRoot = project.GetPropertyValue("NugetPackageRoot"); + this.IntermediateOutputPath = project.GetPropertyValue("IntermediateOutputPath"); + return true; + } + } +} diff --git a/dotnet-thirdparty/ProjectDiscoveryService.cs b/dotnet-thirdparty/ProjectDiscoveryService.cs new file mode 100644 index 0000000..d327585 --- /dev/null +++ b/dotnet-thirdparty/ProjectDiscoveryService.cs @@ -0,0 +1,49 @@ +using DotNetOutdated.Exceptions; +using System; +using System.IO; +using System.IO.Abstractions; +using System.Linq; + +namespace DotNetOutdated.Services +{ + internal class ProjectDiscoveryService : IProjectDiscoveryService + { + private readonly IFileSystem _fileSystem; + + public ProjectDiscoveryService(IFileSystem fileSystem) + { + _fileSystem = fileSystem; + } + + public string DiscoverProject(string path) + { + if (!(_fileSystem.File.Exists(path) || _fileSystem.Directory.Exists(path))) + throw new CommandValidationException(string.Format(Resources.ValidationErrorMessages.DirectoryOrFileDoesNotExist, path)); + + var fileAttributes = _fileSystem.File.GetAttributes(path); + + // If a directory was passed in, search for a .csproj or .fsproj file + if (fileAttributes.HasFlag(FileAttributes.Directory)) + { + // Try and find individual projects + var projectFiles = _fileSystem.Directory.GetFiles(path, "*.csproj").Concat(_fileSystem.Directory.GetFiles(path, "*.fsproj")).ToArray(); + if (projectFiles.Length == 1) + return _fileSystem.Path.GetFullPath(projectFiles[0]); + + if (projectFiles.Length > 1) + throw new CommandValidationException(string.Format(Resources.ValidationErrorMessages.DirectoryContainsMultipleProjects, path)); + + // At this point the path contains no projects, so throw an exception + throw new CommandValidationException(string.Format(Resources.ValidationErrorMessages.DirectoryDoesNotContainSolutionsOrProjects, path)); + } + + // If a .csproj or .fsproj file was passed, just return that + if ((string.Compare(_fileSystem.Path.GetExtension(path), ".csproj", StringComparison.OrdinalIgnoreCase) == 0) || + (string.Compare(_fileSystem.Path.GetExtension(path), ".fsproj", StringComparison.OrdinalIgnoreCase) == 0)) + return _fileSystem.Path.GetFullPath(path); + + // At this point, we know the file passed in is not a valid project or solution + throw new CommandValidationException(string.Format(Resources.ValidationErrorMessages.FileNotAValidSolutionOrProject, path)); + } + } +} \ No newline at end of file diff --git a/dotnet-thirdparty/SpdxLicenseEntry.cs b/dotnet-thirdparty/SpdxLicenseEntry.cs new file mode 100644 index 0000000..a38a536 --- /dev/null +++ b/dotnet-thirdparty/SpdxLicenseEntry.cs @@ -0,0 +1,26 @@ +using Newtonsoft.Json; +using System.Collections.ObjectModel; + +namespace DotNet.ThirdParty +{ + class SpdxLicenseEntry + { + [JsonProperty("isDeprecatedLicenseId")] + public bool IsDeprecatedLicenseId { get; set; } + + [JsonProperty("licenseText")] + public string LicenseText { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("licenseId")] + public string LicenseId { get; set; } + + [JsonProperty("seeAlso")] + public Collection SeeAlso { get; set; } + + [JsonProperty("isOsiApproved")] + public bool IsOsiApproved { get; set; } + } +} diff --git a/dotnet-thirdparty/THIRD-PARTY-NOTICES.TXT.template b/dotnet-thirdparty/THIRD-PARTY-NOTICES.TXT.template new file mode 100644 index 0000000..6ba0645 --- /dev/null +++ b/dotnet-thirdparty/THIRD-PARTY-NOTICES.TXT.template @@ -0,0 +1,22 @@ +This software product uses third-party libraries or other resources that +may be distributed under different licenses. + +In the event that we accidentally failed to list a required notice, please +bring it to our attention. + +The attached notices are provided for information only. + +{{#.}} +{{PackageName}} +{{TitleLine PackageName}} + +{{Copyright}} + +{{#License.Name}} +Licensed under the terms of {{License.Name}}. +{{/License.Name}} +{{^License.Name}} +{{License.Text}} +{{/License.Name}} + +{{/.}} \ No newline at end of file diff --git a/dotnet-thirdparty/ThirdPartyNotice.cs b/dotnet-thirdparty/ThirdPartyNotice.cs new file mode 100644 index 0000000..f144c04 --- /dev/null +++ b/dotnet-thirdparty/ThirdPartyNotice.cs @@ -0,0 +1,15 @@ +namespace DotNet.ThirdParty +{ + class ThirdPartyNotice + { + public string PackageName { get; set; } + public string PackageUrl { get; set; } + public string Copyright { get; set; } + public License License { get; set; } + + public override string ToString() + { + return $"{this.PackageName} - {this.License.Name}"; + } + } +} diff --git a/dotnet-thirdparty/dotnet-outdated/CommandValidationException.cs b/dotnet-thirdparty/dotnet-outdated/CommandValidationException.cs new file mode 100644 index 0000000..c737a73 --- /dev/null +++ b/dotnet-thirdparty/dotnet-outdated/CommandValidationException.cs @@ -0,0 +1,17 @@ +using System; + +namespace DotNetOutdated.Exceptions +{ + public class CommandValidationException : Exception + { + public CommandValidationException(string message) + : base(message) + { + } + + public CommandValidationException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} \ No newline at end of file diff --git a/dotnet-thirdparty/dotnet-outdated/IProjectDiscoveryService.cs b/dotnet-thirdparty/dotnet-outdated/IProjectDiscoveryService.cs new file mode 100644 index 0000000..1242192 --- /dev/null +++ b/dotnet-thirdparty/dotnet-outdated/IProjectDiscoveryService.cs @@ -0,0 +1,7 @@ +namespace DotNetOutdated.Services +{ + internal interface IProjectDiscoveryService + { + string DiscoverProject(string path); + } +} \ No newline at end of file diff --git a/dotnet-thirdparty/dotnet-outdated/LICENSE b/dotnet-thirdparty/dotnet-outdated/LICENSE new file mode 100644 index 0000000..049832f --- /dev/null +++ b/dotnet-thirdparty/dotnet-outdated/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Jerrie Pelser + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/dotnet-thirdparty/dotnet-outdated/ValidationErrorMessages.Designer.cs b/dotnet-thirdparty/dotnet-outdated/ValidationErrorMessages.Designer.cs new file mode 100644 index 0000000..e271b20 --- /dev/null +++ b/dotnet-thirdparty/dotnet-outdated/ValidationErrorMessages.Designer.cs @@ -0,0 +1,108 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace DotNetOutdated.Resources { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class ValidationErrorMessages { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal ValidationErrorMessages() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DotNet.ThirdParty.dotnet_outdated.ValidationErrorMessages", typeof(ValidationErrorMessages).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Specify which project file to use because the directory '{0}' contains more than one.. + /// + internal static string DirectoryContainsMultipleProjects { + get { + return ResourceManager.GetString("DirectoryContainsMultipleProjects", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Specify which solution file to use because the directory '{0}' contains more than one.. + /// + internal static string DirectoryContainsMultipleSolutions { + get { + return ResourceManager.GetString("DirectoryContainsMultipleSolutions", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The directory '{0}' does not contain any solutions or projects.. + /// + internal static string DirectoryDoesNotContainSolutionsOrProjects { + get { + return ResourceManager.GetString("DirectoryDoesNotContainSolutionsOrProjects", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The directory or file '{0}' does not exist.. + /// + internal static string DirectoryOrFileDoesNotExist { + get { + return ResourceManager.GetString("DirectoryOrFileDoesNotExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The file '{0}' is not a valid solution or project.. + /// + internal static string FileNotAValidSolutionOrProject { + get { + return ResourceManager.GetString("FileNotAValidSolutionOrProject", resourceCulture); + } + } + } +} diff --git a/dotnet-thirdparty/dotnet-outdated/ValidationErrorMessages.resx b/dotnet-thirdparty/dotnet-outdated/ValidationErrorMessages.resx new file mode 100644 index 0000000..f727d82 --- /dev/null +++ b/dotnet-thirdparty/dotnet-outdated/ValidationErrorMessages.resx @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Specify which project file to use because the directory '{0}' contains more than one. + + + Specify which solution file to use because the directory '{0}' contains more than one. + + + The directory '{0}' does not contain any solutions or projects. + + + The directory or file '{0}' does not exist. + + + The file '{0}' is not a valid solution or project. + + \ No newline at end of file diff --git a/dotnet-thirdparty/dotnet-thirdparty.csproj b/dotnet-thirdparty/dotnet-thirdparty.csproj new file mode 100644 index 0000000..c322cce --- /dev/null +++ b/dotnet-thirdparty/dotnet-thirdparty.csproj @@ -0,0 +1,28 @@ + + + + Exe + netcoreapp2.2 + DotNet.ThirdParty + + + + + + + + + + + + + + + + + + + PreserveNewest + + +