diff --git a/DotnetThirdPartyNotices.sln b/DotnetThirdPartyNotices.sln index e67527a..d79699d 100644 --- a/DotnetThirdPartyNotices.sln +++ b/DotnetThirdPartyNotices.sln @@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.28010.2036 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotnetThirdPartyNotices", "src\DotnetThirdPartyNotices\DotnetThirdPartyNotices.csproj", "{052DFD6A-FD20-44C2-9448-0ACFDBFFEFCB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotnetThirdPartyNotices", "DotnetThirdPartyNotices\DotnetThirdPartyNotices.csproj", "{052DFD6A-FD20-44C2-9448-0ACFDBFFEFCB}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/DotnetThirdPartyNotices/Commands/ScanCommand.cs b/DotnetThirdPartyNotices/Commands/ScanCommand.cs similarity index 83% rename from src/DotnetThirdPartyNotices/Commands/ScanCommand.cs rename to DotnetThirdPartyNotices/Commands/ScanCommand.cs index bc27bc1..369da83 100644 --- a/src/DotnetThirdPartyNotices/Commands/ScanCommand.cs +++ b/DotnetThirdPartyNotices/Commands/ScanCommand.cs @@ -22,8 +22,10 @@ internal class ScanCommand : Command { AddArgument(new Argument("scan-dir", "Path of the directory to look for projects (optional)") { Arity = ArgumentArity.ZeroOrOne }); AddOption(new Option("--output-filename", () => "third-party-notices.txt", "Output filename")); - AddOption(new Option("--copy-to-outdir", () => false, "Copy output file to output directory in Release configuration")); + AddOption(new Option("--copy-to-outdir", () => false, "Copy output file to output directory in selected configuration")); AddOption(new Option("--filter", () => string.Empty, "Filter project files")); + AddOption(new Option("--github-token", () => string.Empty, "GitHub's token")); + AddOption(new Option("--configuration", () => "Release", "Project configuration to use")); } internal new class Handler(ILogger logger, IProjectService projectService, ILicenseService licenseService) : ICommandHandler @@ -32,7 +34,9 @@ internal class ScanCommand : Command public string? OutputFilename { get; set; } public bool CopyToOutDir { get; set; } public string? Filter { get; set; } - public bool Merge { get; set; } + public string? GithubToken { get; set; } + public string? Configuration { get; set; } + private readonly Dictionary> _licenseContents = []; private readonly List _unresolvedFiles = []; @@ -43,6 +47,7 @@ public int Invoke(InvocationContext context) public async Task InvokeAsync(InvocationContext context) { + var cancellationToken = context.GetCancellationToken(); MSBuildLocator.RegisterDefaults(); ScanDir ??= Directory.GetCurrentDirectory(); var projectFilePaths = projectService.GetProjectFilePaths(ScanDir); @@ -53,9 +58,9 @@ public async Task InvokeAsync(InvocationContext context) return 0; } foreach (var projectFilePath in projectFilePaths) - await ScanProjectAsync(projectFilePath); + await ScanProjectAsync(projectFilePath, cancellationToken); if (!CopyToOutDir) - await GenerateOutputFileAsync(OutputFilename); + await GenerateOutputFileAsync(OutputFilename, cancellationToken); return 0; } @@ -67,18 +72,20 @@ private string[] GetFilteredProjectPathes(string[] projectPathes) return projectPathes.Where(x => filterRegex.IsMatch(x)).ToArray(); } - private async Task ScanProjectAsync(string projectFilePath) + private async Task ScanProjectAsync(string projectFilePath, CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); var stopWatch = new Stopwatch(); stopWatch.Start(); - logger.LogInformation("Resolving files for {ProjectName}...", Path.GetFileName(projectFilePath)); + logger.LogInformation("Resolving files for {ProjectName} using {configuration} configuration...", Path.GetFileName(projectFilePath), Configuration ?? "Release"); var project = new Project(projectFilePath); - project.SetProperty("Configuration", "Release"); + project.SetProperty("Configuration", Configuration ?? "Release"); project.SetProperty("DesignTimeBuild", "true"); var resolvedFiles = projectService.ResolveFiles(project).ToList(); logger.LogInformation("Resolved files count: {ResolvedFilesCount}", resolvedFiles.Count); foreach (var resolvedFileInfo in resolvedFiles) { + cancellationToken.ThrowIfCancellationRequested(); logger.LogInformation("Resolving license for {RelativeOutputPath}", resolvedFileInfo.RelativeOutputPath); if (resolvedFileInfo.NuSpec != null) { @@ -88,7 +95,11 @@ private async Task ScanProjectAsync(string projectFilePath) { logger.LogWarning("Package not found"); } - var licenseContent = await licenseService.ResolveFromResolvedFileInfo(resolvedFileInfo); + var resolverOptions = new ResolverOptions + { + GitHubToken = GithubToken + }; + var licenseContent = await licenseService.ResolveFromResolvedFileInfoAsync(resolvedFileInfo, resolverOptions, cancellationToken); if (licenseContent == null) { _unresolvedFiles.Add(resolvedFileInfo); @@ -102,11 +113,12 @@ private async Task ScanProjectAsync(string projectFilePath) stopWatch.Stop(); logger.LogInformation("Project {ProjectName} resolved in {StopwatchElapsedMilliseconds}ms", Path.GetFileName(projectFilePath), stopWatch.ElapsedMilliseconds); if (CopyToOutDir && !string.IsNullOrEmpty(ScanDir) && !string.IsNullOrEmpty(OutputFilename)) - await GenerateOutputFileAsync(Path.Combine(ScanDir, project.GetPropertyValue("OutDir"), Path.GetFileName(OutputFilename))); + await GenerateOutputFileAsync(Path.Combine(ScanDir, project.GetPropertyValue("OutDir"), Path.GetFileName(OutputFilename)), cancellationToken); } - private async Task GenerateOutputFileAsync(string? outputFilePath) + private async Task GenerateOutputFileAsync(string? outputFilePath, CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); if (outputFilePath == null) return; var uniqueResolvedFilesCount = _licenseContents.Values.Sum(v => v.GroupBy(x => x.SourcePath).Count()); @@ -134,6 +146,7 @@ private async Task GenerateOutputFileAsync(string? outputFilePath) logger.LogInformation("Generate licenses in {StopwatchElapsedMilliseconds}ms", stopWatch.ElapsedMilliseconds); if (stringBuilder.Length == 0) return; + cancellationToken.ThrowIfCancellationRequested(); logger.LogInformation("Writing to {OutputFilename}...", outputFilePath); await System.IO.File.WriteAllTextAsync(outputFilePath, stringBuilder.ToString()); _licenseContents.Clear(); diff --git a/src/DotnetThirdPartyNotices/Directory.Packages.props b/DotnetThirdPartyNotices/Directory.Packages.props similarity index 99% rename from src/DotnetThirdPartyNotices/Directory.Packages.props rename to DotnetThirdPartyNotices/Directory.Packages.props index b7a153e..e50dc7c 100644 --- a/src/DotnetThirdPartyNotices/Directory.Packages.props +++ b/DotnetThirdPartyNotices/Directory.Packages.props @@ -6,7 +6,7 @@ - + diff --git a/src/DotnetThirdPartyNotices/DotnetThirdPartyNotices.csproj b/DotnetThirdPartyNotices/DotnetThirdPartyNotices.csproj similarity index 97% rename from src/DotnetThirdPartyNotices/DotnetThirdPartyNotices.csproj rename to DotnetThirdPartyNotices/DotnetThirdPartyNotices.csproj index 8a4b9a5..c2f12dc 100644 --- a/src/DotnetThirdPartyNotices/DotnetThirdPartyNotices.csproj +++ b/DotnetThirdPartyNotices/DotnetThirdPartyNotices.csproj @@ -10,7 +10,7 @@ dotnet-thirdpartynotices DotnetThirdPartyNotices A .NET tool to generate file with third party legal notices - 0.3.2 + 0.3.3 MIT git https://github.com/bugproof/DotnetThirdPartyNotices diff --git a/src/DotnetThirdPartyNotices/Models/NuSpec.cs b/DotnetThirdPartyNotices/Models/NuSpec.cs similarity index 100% rename from src/DotnetThirdPartyNotices/Models/NuSpec.cs rename to DotnetThirdPartyNotices/Models/NuSpec.cs diff --git a/src/DotnetThirdPartyNotices/Models/ResolvedFileInfo.cs b/DotnetThirdPartyNotices/Models/ResolvedFileInfo.cs similarity index 100% rename from src/DotnetThirdPartyNotices/Models/ResolvedFileInfo.cs rename to DotnetThirdPartyNotices/Models/ResolvedFileInfo.cs diff --git a/DotnetThirdPartyNotices/Models/ResolverOptions.cs b/DotnetThirdPartyNotices/Models/ResolverOptions.cs new file mode 100644 index 0000000..6238810 --- /dev/null +++ b/DotnetThirdPartyNotices/Models/ResolverOptions.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DotnetThirdPartyNotices.Models; + +internal class ResolverOptions +{ + public string? GitHubToken { get; set; } +} diff --git a/src/DotnetThirdPartyNotices/Models/TargetFrameworkIdentifiers.cs b/DotnetThirdPartyNotices/Models/TargetFrameworkIdentifiers.cs similarity index 100% rename from src/DotnetThirdPartyNotices/Models/TargetFrameworkIdentifiers.cs rename to DotnetThirdPartyNotices/Models/TargetFrameworkIdentifiers.cs diff --git a/src/DotnetThirdPartyNotices/Program.cs b/DotnetThirdPartyNotices/Program.cs similarity index 98% rename from src/DotnetThirdPartyNotices/Program.cs rename to DotnetThirdPartyNotices/Program.cs index 3918199..1caf525 100644 --- a/src/DotnetThirdPartyNotices/Program.cs +++ b/DotnetThirdPartyNotices/Program.cs @@ -1,4 +1,5 @@ using DotnetThirdPartyNotices.Commands; +using DotnetThirdPartyNotices.Models; using DotnetThirdPartyNotices.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; diff --git a/src/DotnetThirdPartyNotices/Properties/launchSettings.json b/DotnetThirdPartyNotices/Properties/launchSettings.json similarity index 100% rename from src/DotnetThirdPartyNotices/Properties/launchSettings.json rename to DotnetThirdPartyNotices/Properties/launchSettings.json diff --git a/src/DotnetThirdPartyNotices/Services/GithubRawLicenseResolver.cs b/DotnetThirdPartyNotices/Services/GithubRawLicenseResolver.cs similarity index 53% rename from src/DotnetThirdPartyNotices/Services/GithubRawLicenseResolver.cs rename to DotnetThirdPartyNotices/Services/GithubRawLicenseResolver.cs index ba49b13..0f89d3f 100644 --- a/src/DotnetThirdPartyNotices/Services/GithubRawLicenseResolver.cs +++ b/DotnetThirdPartyNotices/Services/GithubRawLicenseResolver.cs @@ -1,20 +1,26 @@ -namespace DotnetThirdPartyNotices.Services; +using DotnetThirdPartyNotices.Models; +using System.Net.Http.Headers; +using System.Text; + +namespace DotnetThirdPartyNotices.Services; internal class GithubRawLicenseResolver(IHttpClientFactory httpClientFactory) : ILicenseUriLicenseResolver { - public bool CanResolve(Uri uri) => uri.Host == "github.com"; + public Task CanResolveAsync(Uri uri, ResolverOptions resolverOptions, CancellationToken cancellationToken) => Task.FromResult(uri.Host == "github.com"); - public async Task Resolve(Uri uri) + public async Task ResolveAsync(Uri uri, ResolverOptions resolverOptions, CancellationToken cancellationToken) { var uriBuilder = new UriBuilder(uri) { Host = "raw.githubusercontent.com" }; uriBuilder.Path = uriBuilder.Path.Replace("/blob", string.Empty); var httpClient = httpClientFactory.CreateClient(); // https://developer.github.com/v3/#user-agent-required httpClient.DefaultRequestHeaders.Add("User-Agent", "DotnetLicense"); - var httpResponseMessage = await httpClient.GetAsync(uriBuilder.Uri); + if (!string.IsNullOrEmpty(resolverOptions.GitHubToken)) + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", resolverOptions.GitHubToken); + var httpResponseMessage = await httpClient.GetAsync(uriBuilder.Uri, cancellationToken); if (!httpResponseMessage.IsSuccessStatusCode || httpResponseMessage.Content.Headers.ContentType?.MediaType != "text/plain") return null; - return await httpResponseMessage.Content.ReadAsStringAsync(); + return await httpResponseMessage.Content.ReadAsStringAsync(cancellationToken); } } diff --git a/src/DotnetThirdPartyNotices/Services/GithubRepositoryLicenseResolver.cs b/DotnetThirdPartyNotices/Services/GithubRepositoryLicenseResolver.cs similarity index 63% rename from src/DotnetThirdPartyNotices/Services/GithubRepositoryLicenseResolver.cs rename to DotnetThirdPartyNotices/Services/GithubRepositoryLicenseResolver.cs index a8452de..cd33e23 100644 --- a/src/DotnetThirdPartyNotices/Services/GithubRepositoryLicenseResolver.cs +++ b/DotnetThirdPartyNotices/Services/GithubRepositoryLicenseResolver.cs @@ -1,5 +1,7 @@ -using System; +using DotnetThirdPartyNotices.Models; +using System; using System.Net.Http; +using System.Net.Http.Headers; using System.Text; using System.Text.Json; using System.Threading.Tasks; @@ -8,9 +10,9 @@ namespace DotnetThirdPartyNotices.Services; internal class GithubRepositoryLicenseResolver(IHttpClientFactory httpClientFactory) : IProjectUriLicenseResolver, IRepositoryUriLicenseResolver { - public bool CanResolve(Uri uri) => uri.Host == "github.com"; + public Task CanResolveAsync(Uri uri, ResolverOptions resolverOptions, CancellationToken cancellationToken) => Task.FromResult(uri.Host == "github.com"); - public async Task Resolve(Uri licenseUri) + public async Task ResolveAsync(Uri licenseUri, ResolverOptions resolverOptions, CancellationToken cancellationToken) { var repositoryPath = licenseUri.AbsolutePath.TrimEnd('/'); if (repositoryPath.EndsWith(".git")) @@ -22,10 +24,12 @@ internal class GithubRepositoryLicenseResolver(IHttpClientFactory httpClientFact }; // https://developer.github.com/v3/#user-agent-required httpClient.DefaultRequestHeaders.Add("User-Agent", "DotnetLicense"); - var response = await httpClient.GetAsync(uriBuilder.Uri); + if (!string.IsNullOrEmpty(resolverOptions.GitHubToken)) + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", resolverOptions.GitHubToken); + var response = await httpClient.GetAsync(uriBuilder.Uri, cancellationToken); if (!response.IsSuccessStatusCode) return null; - var json = await response.Content.ReadAsStringAsync(); + var json = await response.Content.ReadAsStringAsync(cancellationToken); var jsonDocument = JsonDocument.Parse(json); var rootElement = jsonDocument.RootElement; var encoding = rootElement.GetProperty("encoding").GetString(); diff --git a/DotnetThirdPartyNotices/Services/IFileVersionInfoLicenseResolver.cs b/DotnetThirdPartyNotices/Services/IFileVersionInfoLicenseResolver.cs new file mode 100644 index 0000000..e1dc371 --- /dev/null +++ b/DotnetThirdPartyNotices/Services/IFileVersionInfoLicenseResolver.cs @@ -0,0 +1,11 @@ +using DotnetThirdPartyNotices.Models; +using System.Diagnostics; +using System.Threading.Tasks; + +namespace DotnetThirdPartyNotices.Services; + +internal interface IFileVersionInfoLicenseResolver : ILicenseResolver +{ + Task CanResolveAsync(FileVersionInfo fileVersionInfo, CancellationToken cancellationToken); + Task ResolveAsync(FileVersionInfo fileVersionInfo, ResolverOptions resolverOptions, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/src/DotnetThirdPartyNotices/Services/ILicenseResolver.cs b/DotnetThirdPartyNotices/Services/ILicenseResolver.cs similarity index 100% rename from src/DotnetThirdPartyNotices/Services/ILicenseResolver.cs rename to DotnetThirdPartyNotices/Services/ILicenseResolver.cs diff --git a/DotnetThirdPartyNotices/Services/ILicenseService.cs b/DotnetThirdPartyNotices/Services/ILicenseService.cs new file mode 100644 index 0000000..82e7a73 --- /dev/null +++ b/DotnetThirdPartyNotices/Services/ILicenseService.cs @@ -0,0 +1,8 @@ +using DotnetThirdPartyNotices.Models; + +namespace DotnetThirdPartyNotices.Services; + +internal interface ILicenseService +{ + Task ResolveFromResolvedFileInfoAsync(ResolvedFileInfo resolvedFileInfo, ResolverOptions resolverOptions, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/src/DotnetThirdPartyNotices/Services/ILicenseUriLicenseResolver.cs b/DotnetThirdPartyNotices/Services/ILicenseUriLicenseResolver.cs similarity index 100% rename from src/DotnetThirdPartyNotices/Services/ILicenseUriLicenseResolver.cs rename to DotnetThirdPartyNotices/Services/ILicenseUriLicenseResolver.cs diff --git a/src/DotnetThirdPartyNotices/Services/ILocalPackageService.cs b/DotnetThirdPartyNotices/Services/ILocalPackageService.cs similarity index 100% rename from src/DotnetThirdPartyNotices/Services/ILocalPackageService.cs rename to DotnetThirdPartyNotices/Services/ILocalPackageService.cs diff --git a/src/DotnetThirdPartyNotices/Services/IProjectService.cs b/DotnetThirdPartyNotices/Services/IProjectService.cs similarity index 100% rename from src/DotnetThirdPartyNotices/Services/IProjectService.cs rename to DotnetThirdPartyNotices/Services/IProjectService.cs diff --git a/src/DotnetThirdPartyNotices/Services/IProjectUriLicenseResolver.cs b/DotnetThirdPartyNotices/Services/IProjectUriLicenseResolver.cs similarity index 100% rename from src/DotnetThirdPartyNotices/Services/IProjectUriLicenseResolver.cs rename to DotnetThirdPartyNotices/Services/IProjectUriLicenseResolver.cs diff --git a/src/DotnetThirdPartyNotices/Services/IRepositoryUriLicenseResolver.cs b/DotnetThirdPartyNotices/Services/IRepositoryUriLicenseResolver.cs similarity index 100% rename from src/DotnetThirdPartyNotices/Services/IRepositoryUriLicenseResolver.cs rename to DotnetThirdPartyNotices/Services/IRepositoryUriLicenseResolver.cs diff --git a/DotnetThirdPartyNotices/Services/IUriLicenseResolver.cs b/DotnetThirdPartyNotices/Services/IUriLicenseResolver.cs new file mode 100644 index 0000000..d66efda --- /dev/null +++ b/DotnetThirdPartyNotices/Services/IUriLicenseResolver.cs @@ -0,0 +1,9 @@ +using DotnetThirdPartyNotices.Models; + +namespace DotnetThirdPartyNotices.Services; + +internal interface IUriLicenseResolver +{ + Task CanResolveAsync(Uri licenseUri, ResolverOptions resolverOptions, CancellationToken cancellationToken); + Task ResolveAsync(Uri licenseUri, ResolverOptions resolverOptions, CancellationToken cancellationToken); +} diff --git a/src/DotnetThirdPartyNotices/Services/LicenseService.cs b/DotnetThirdPartyNotices/Services/LicenseService.cs similarity index 80% rename from src/DotnetThirdPartyNotices/Services/LicenseService.cs rename to DotnetThirdPartyNotices/Services/LicenseService.cs index c534dc4..2cfb683 100644 --- a/src/DotnetThirdPartyNotices/Services/LicenseService.cs +++ b/DotnetThirdPartyNotices/Services/LicenseService.cs @@ -8,21 +8,21 @@ internal partial class LicenseService(ILogger logger, IEnumerabl { private static readonly Dictionary LicenseCache = []; - public async Task ResolveFromResolvedFileInfo(ResolvedFileInfo resolvedFileInfo) + public async Task ResolveFromResolvedFileInfoAsync(ResolvedFileInfo resolvedFileInfo, ResolverOptions resolverOptions, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(resolvedFileInfo); if (resolvedFileInfo.NuSpec != null && LicenseCache.TryGetValue(resolvedFileInfo.NuSpec.Id, out string? value)) return value; - return (await ResolveFromLicenseRelativePathAsync(resolvedFileInfo)) - ?? (await ResolveFromLicenseUrlAsync(resolvedFileInfo, false)) - ?? (await ResolveFromRepositoryUrlAsync(resolvedFileInfo, false)) - ?? (await ResolveFromProjectUrlAsync(resolvedFileInfo, false)) - ?? (await ResolveFromPackagePathAsync(resolvedFileInfo)) - ?? (await ResolveFromSourcePathAsync(resolvedFileInfo)) - ?? (await ResolveFromFileVersionInfoAsync(resolvedFileInfo)) - ?? (await ResolveFromLicenseUrlAsync(resolvedFileInfo, true)) - ?? (await ResolveFromRepositoryUrlAsync(resolvedFileInfo, true)) - ?? (await ResolveFromProjectUrlAsync(resolvedFileInfo, true)); + return (await ResolveFromLicenseRelativePathAsync(resolvedFileInfo, cancellationToken)) + ?? (await ResolveFromLicenseUrlAsync(resolvedFileInfo, false, resolverOptions, cancellationToken)) + ?? (await ResolveFromRepositoryUrlAsync(resolvedFileInfo, false, resolverOptions, cancellationToken)) + ?? (await ResolveFromProjectUrlAsync(resolvedFileInfo, false, resolverOptions, cancellationToken)) + ?? (await ResolveFromPackagePathAsync(resolvedFileInfo, cancellationToken)) + ?? (await ResolveFromSourcePathAsync(resolvedFileInfo, cancellationToken)) + ?? (await ResolveFromFileVersionInfoAsync(resolvedFileInfo, resolverOptions, cancellationToken)) + ?? (await ResolveFromLicenseUrlAsync(resolvedFileInfo, true, resolverOptions, cancellationToken)) + ?? (await ResolveFromRepositoryUrlAsync(resolvedFileInfo, true, resolverOptions, cancellationToken)) + ?? (await ResolveFromProjectUrlAsync(resolvedFileInfo, true, resolverOptions, cancellationToken)); } private static string? UnifyLicense(string? license) @@ -50,8 +50,9 @@ internal partial class LicenseService(ILogger logger, IEnumerabl return license; } - private async Task ResolveFromPackagePathAsync(ResolvedFileInfo resolvedFileInfo) + private async Task ResolveFromPackagePathAsync(ResolvedFileInfo resolvedFileInfo, CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); if (string.IsNullOrEmpty(resolvedFileInfo.PackagePath)) return null; if (LicenseCache.TryGetValue(resolvedFileInfo.PackagePath, out string? value)) @@ -73,8 +74,9 @@ internal partial class LicenseService(ILogger logger, IEnumerabl return license; } - private async Task ResolveFromSourcePathAsync(ResolvedFileInfo resolvedFileInfo) + private async Task ResolveFromSourcePathAsync(ResolvedFileInfo resolvedFileInfo, CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); if (string.IsNullOrEmpty(resolvedFileInfo.SourcePath)) return null; if (LicenseCache.TryGetValue(resolvedFileInfo.SourcePath, out string? value)) @@ -107,7 +109,7 @@ internal partial class LicenseService(ILogger logger, IEnumerabl } } - private async Task ResolveFromFileVersionInfoAsync(ResolvedFileInfo resolvedFileInfo) + private async Task ResolveFromFileVersionInfoAsync(ResolvedFileInfo resolvedFileInfo, ResolverOptions resolverOptions, CancellationToken cancellationToken) { if(resolvedFileInfo.VersionInfo == null) return null; @@ -117,9 +119,9 @@ internal partial class LicenseService(ILogger logger, IEnumerabl { foreach (var resolver in fileVersionInfoLicenseResolvers) { - if (!resolver.CanResolve(resolvedFileInfo.VersionInfo)) + if (!await resolver.CanResolveAsync(resolvedFileInfo.VersionInfo, cancellationToken)) continue; - var license = await resolver.Resolve(resolvedFileInfo.VersionInfo); + var license = await resolver.ResolveAsync(resolvedFileInfo.VersionInfo, resolverOptions, cancellationToken); license = UnifyLicense(license); if (license != null) { @@ -136,8 +138,9 @@ internal partial class LicenseService(ILogger logger, IEnumerabl } } - private async Task ResolveFromLicenseRelativePathAsync(ResolvedFileInfo resolvedFileInfo) + private async Task ResolveFromLicenseRelativePathAsync(ResolvedFileInfo resolvedFileInfo, CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); if (string.IsNullOrEmpty(resolvedFileInfo.PackagePath) || string.IsNullOrEmpty(resolvedFileInfo.NuSpec?.LicenseRelativePath)) return null; var licenseFullPath = Path.Combine(resolvedFileInfo.PackagePath, resolvedFileInfo.NuSpec.LicenseRelativePath); @@ -163,7 +166,7 @@ internal partial class LicenseService(ILogger logger, IEnumerabl } } - private async Task ResolveFromProjectUrlAsync(ResolvedFileInfo resolvedFileInfo, bool useFinalUrl) + private async Task ResolveFromProjectUrlAsync(ResolvedFileInfo resolvedFileInfo, bool useFinalUrl, ResolverOptions resolverOptions, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(resolvedFileInfo.NuSpec?.ProjectUrl)) return null; @@ -174,8 +177,8 @@ internal partial class LicenseService(ILogger logger, IEnumerabl try { var license = useFinalUrl - ? await ResolveFromFinalUrlAsync(uri, projectUriLicenseResolvers) - : await ResolveFromUrlAsync(uri, projectUriLicenseResolvers); + ? await ResolveFromFinalUrlAsync(uri, projectUriLicenseResolvers, resolverOptions, cancellationToken) + : await ResolveFromUrlAsync(uri, projectUriLicenseResolvers, resolverOptions, cancellationToken); license = UnifyLicense(license); if (license == null) return null; @@ -190,30 +193,32 @@ internal partial class LicenseService(ILogger logger, IEnumerabl } } - private async Task ResolveFromUrlAsync(Uri uri, IEnumerable urlLicenseResolvers) + private async Task ResolveFromUrlAsync(Uri uri, IEnumerable urlLicenseResolvers, ResolverOptions resolverOptions, CancellationToken cancellationToken) { foreach (var resolver in urlLicenseResolvers) { - if (!resolver.CanResolve(uri)) + if (!await resolver.CanResolveAsync(uri, resolverOptions, cancellationToken)) continue; - var license = await resolver.Resolve(uri); + var license = await resolver.ResolveAsync(uri, resolverOptions, cancellationToken); license = UnifyLicense(license); if (license != null) return license; + cancellationToken.ThrowIfCancellationRequested(); } return null; } - private async Task ResolveFromFinalUrlAsync(Uri uri, IEnumerable urlLicenseResolvers) + private async Task ResolveFromFinalUrlAsync(Uri uri, IEnumerable urlLicenseResolvers, ResolverOptions resolverOptions, CancellationToken cancellationToken) { using var httpClient = httpClientFactory.CreateClient(); - var httpResponseMessage = await httpClient.GetAsync(uri); + var httpResponseMessage = await httpClient.GetAsync(uri, cancellationToken); if (!httpResponseMessage.IsSuccessStatusCode && uri.AbsolutePath.EndsWith(".txt")) { + cancellationToken.ThrowIfCancellationRequested(); // try without .txt extension var fixedUri = new UriBuilder(uri); fixedUri.Path = fixedUri.Path.Remove(fixedUri.Path.Length - 4); - httpResponseMessage = await httpClient.GetAsync(fixedUri.Uri); + httpResponseMessage = await httpClient.GetAsync(fixedUri.Uri, cancellationToken); if (!httpResponseMessage.IsSuccessStatusCode) return null; } @@ -221,22 +226,24 @@ internal partial class LicenseService(ILogger logger, IEnumerabl { foreach (var resolver in urlLicenseResolvers) { - if (!resolver.CanResolve(httpResponseMessage.RequestMessage.RequestUri)) + if (!await resolver.CanResolveAsync(httpResponseMessage.RequestMessage.RequestUri, resolverOptions, cancellationToken)) continue; - var license = await resolver.Resolve(httpResponseMessage.RequestMessage.RequestUri); + var license = await resolver.ResolveAsync(httpResponseMessage.RequestMessage.RequestUri, resolverOptions, cancellationToken); license = UnifyLicense(license); if (license != null) return license; + cancellationToken.ThrowIfCancellationRequested(); } } // Finally, if no license uri can be found despite all the redirects, try to blindly get it if (httpResponseMessage.Content.Headers.ContentType?.MediaType != "text/plain") return null; - var license2 = await httpResponseMessage.Content.ReadAsStringAsync(); + var license2 = await httpResponseMessage.Content.ReadAsStringAsync(cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); return UnifyLicense(license2); } - private async Task ResolveFromRepositoryUrlAsync(ResolvedFileInfo resolvedFileInfo, bool useFinalUrl) + private async Task ResolveFromRepositoryUrlAsync(ResolvedFileInfo resolvedFileInfo, bool useFinalUrl, ResolverOptions resolverOptions, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(resolvedFileInfo.NuSpec?.RepositoryUrl)) return null; @@ -247,8 +254,8 @@ internal partial class LicenseService(ILogger logger, IEnumerabl try { var license = useFinalUrl - ? await ResolveFromFinalUrlAsync(uri, repositoryUriLicenseResolvers) - : await ResolveFromUrlAsync(uri, repositoryUriLicenseResolvers); + ? await ResolveFromFinalUrlAsync(uri, repositoryUriLicenseResolvers, resolverOptions, cancellationToken) + : await ResolveFromUrlAsync(uri, repositoryUriLicenseResolvers, resolverOptions, cancellationToken); license = UnifyLicense(license); if (license == null) return null; @@ -263,8 +270,9 @@ internal partial class LicenseService(ILogger logger, IEnumerabl } } - private async Task ResolveFromLicenseUrlAsync(ResolvedFileInfo resolvedFileInfo, bool useFinalUrl) + private async Task ResolveFromLicenseUrlAsync(ResolvedFileInfo resolvedFileInfo, bool useFinalUrl, ResolverOptions resolverOptions, CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); if (string.IsNullOrEmpty(resolvedFileInfo.NuSpec?.LicenseUrl)) return null; if (LicenseCache.TryGetValue(resolvedFileInfo.NuSpec.LicenseUrl, out string? value)) @@ -274,8 +282,8 @@ internal partial class LicenseService(ILogger logger, IEnumerabl try { var license = useFinalUrl - ? await ResolveFromFinalUrlAsync(uri, licenseUriLicenseResolvers) - : await ResolveFromUrlAsync(uri, licenseUriLicenseResolvers); + ? await ResolveFromFinalUrlAsync(uri, licenseUriLicenseResolvers, resolverOptions, cancellationToken) + : await ResolveFromUrlAsync(uri, licenseUriLicenseResolvers, resolverOptions, cancellationToken); license = UnifyLicense(license); if (license == null) return null; diff --git a/src/DotnetThirdPartyNotices/Services/LocalPackageService.cs b/DotnetThirdPartyNotices/Services/LocalPackageService.cs similarity index 100% rename from src/DotnetThirdPartyNotices/Services/LocalPackageService.cs rename to DotnetThirdPartyNotices/Services/LocalPackageService.cs diff --git a/src/DotnetThirdPartyNotices/Services/NetFrameworkLicenseResolver.cs b/DotnetThirdPartyNotices/Services/NetFrameworkLicenseResolver.cs similarity index 54% rename from src/DotnetThirdPartyNotices/Services/NetFrameworkLicenseResolver.cs rename to DotnetThirdPartyNotices/Services/NetFrameworkLicenseResolver.cs index f5ad7d3..2ea1f55 100644 --- a/src/DotnetThirdPartyNotices/Services/NetFrameworkLicenseResolver.cs +++ b/DotnetThirdPartyNotices/Services/NetFrameworkLicenseResolver.cs @@ -1,4 +1,5 @@ -using System; +using DotnetThirdPartyNotices.Models; +using System; using System.Diagnostics; using System.IO; using System.Reflection; @@ -10,11 +11,11 @@ internal class NetFrameworkLicenseResolver : ILicenseUriLicenseResolver, IFileVe { private static string? _licenseContent; - public bool CanResolve(Uri licenseUri) => licenseUri.ToString().Contains("LinkId=529443"); - public bool CanResolve(FileVersionInfo fileVersionInfo) => fileVersionInfo.ProductName == "Microsoft® .NET Framework"; + public Task CanResolveAsync(Uri licenseUri, ResolverOptions resolverOptions, CancellationToken cancellationToken) => Task.FromResult(licenseUri.ToString().Contains("LinkId=529443")); + public Task CanResolveAsync(FileVersionInfo fileVersionInfo, CancellationToken cancellationToken) => Task.FromResult(fileVersionInfo.ProductName == "Microsoft® .NET Framework"); - public Task Resolve(Uri licenseUri) => GetLicenseContent(); - public Task Resolve(FileVersionInfo fileVersionInfo) => GetLicenseContent(); + public Task ResolveAsync(Uri licenseUri, ResolverOptions resolverOptions, CancellationToken cancellationToken) => GetLicenseContent(); + public Task ResolveAsync(FileVersionInfo fileVersionInfo, ResolverOptions resolverOptions, CancellationToken cancellationToken) => GetLicenseContent(); public async Task GetLicenseContent() { diff --git a/src/DotnetThirdPartyNotices/Services/OpenSourceOrgLicenseResolver.cs b/DotnetThirdPartyNotices/Services/OpenSourceOrgLicenseResolver.cs similarity index 60% rename from src/DotnetThirdPartyNotices/Services/OpenSourceOrgLicenseResolver.cs rename to DotnetThirdPartyNotices/Services/OpenSourceOrgLicenseResolver.cs index 79683ba..b7e4134 100644 --- a/src/DotnetThirdPartyNotices/Services/OpenSourceOrgLicenseResolver.cs +++ b/DotnetThirdPartyNotices/Services/OpenSourceOrgLicenseResolver.cs @@ -1,5 +1,7 @@ -using System; +using DotnetThirdPartyNotices.Models; +using System; using System.Net.Http; +using System.Net.Http.Headers; using System.Text.Json; using System.Threading.Tasks; @@ -7,11 +9,11 @@ namespace DotnetThirdPartyNotices.Services; internal class OpenSourceOrgLicenseResolver : ILicenseUriLicenseResolver { - public bool CanResolve(Uri licenseUri) => licenseUri.Host == "opensource.org"; + public Task CanResolveAsync(Uri licenseUri, ResolverOptions resolverOptions, CancellationToken cancellationToken) => Task.FromResult(licenseUri.Host == "opensource.org"); internal static readonly char[] separator = ['/']; - public async Task Resolve(Uri licenseUri) + public async Task ResolveAsync(Uri licenseUri, ResolverOptions resolverOptions, CancellationToken cancellationToken) { var s = licenseUri.AbsolutePath.Split(separator, StringSplitOptions.RemoveEmptyEntries); if (s[0] != "licenses") @@ -20,10 +22,12 @@ internal class OpenSourceOrgLicenseResolver : ILicenseUriLicenseResolver HttpClient httpClient = new() { BaseAddress = new Uri("https://api.github.com") }; // https://developer.github.com/v3/#user-agent-required httpClient.DefaultRequestHeaders.Add("User-Agent", "DotnetLicense"); - var httpResponseMessage = await httpClient.GetAsync($"licenses/{licenseId}"); + if (!string.IsNullOrEmpty(resolverOptions.GitHubToken)) + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", resolverOptions.GitHubToken); + var httpResponseMessage = await httpClient.GetAsync($"licenses/{licenseId}", cancellationToken); if (!httpResponseMessage.IsSuccessStatusCode) return null; - var content = await httpResponseMessage.Content.ReadAsStringAsync(); + var content = await httpResponseMessage.Content.ReadAsStringAsync(cancellationToken); var jsonDocument = JsonDocument.Parse(content); return jsonDocument.RootElement.GetProperty("body").GetString(); } diff --git a/src/DotnetThirdPartyNotices/Services/ProjectService.cs b/DotnetThirdPartyNotices/Services/ProjectService.cs similarity index 100% rename from src/DotnetThirdPartyNotices/Services/ProjectService.cs rename to DotnetThirdPartyNotices/Services/ProjectService.cs diff --git a/src/DotnetThirdPartyNotices/dotnet_library_license.txt b/DotnetThirdPartyNotices/dotnet_library_license.txt similarity index 100% rename from src/DotnetThirdPartyNotices/dotnet_library_license.txt rename to DotnetThirdPartyNotices/dotnet_library_license.txt diff --git a/README.md b/README.md index 96b8868..7cb2d53 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,8 @@ Options: - `--output-filename` allow to change output filename - `--copy-to-outdir` allow to copy output file to output directory in Release configuration - `--filter` allow to use regex to filter project files +- `--configuration` allow to change configuration name (tool uses Release by default) +- `--github-token` allow to use GitHub's token Note that if you use the solution folder and don't use `--copy-to-outdir` then licenses from all projects will be merged to single file. diff --git a/src/DotnetThirdPartyNotices/Services/IFileVersionInfoLicenseResolver.cs b/src/DotnetThirdPartyNotices/Services/IFileVersionInfoLicenseResolver.cs deleted file mode 100644 index 01ae395..0000000 --- a/src/DotnetThirdPartyNotices/Services/IFileVersionInfoLicenseResolver.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Diagnostics; -using System.Threading.Tasks; - -namespace DotnetThirdPartyNotices.Services; - -internal interface IFileVersionInfoLicenseResolver : ILicenseResolver -{ - bool CanResolve(FileVersionInfo fileVersionInfo); - Task Resolve(FileVersionInfo fileVersionInfo); -} \ No newline at end of file diff --git a/src/DotnetThirdPartyNotices/Services/ILicenseService.cs b/src/DotnetThirdPartyNotices/Services/ILicenseService.cs deleted file mode 100644 index db30bf8..0000000 --- a/src/DotnetThirdPartyNotices/Services/ILicenseService.cs +++ /dev/null @@ -1,8 +0,0 @@ -using DotnetThirdPartyNotices.Models; - -namespace DotnetThirdPartyNotices.Services; - -internal interface ILicenseService -{ - Task ResolveFromResolvedFileInfo(ResolvedFileInfo resolvedFileInfo); -} \ No newline at end of file diff --git a/src/DotnetThirdPartyNotices/Services/IUriLicenseResolver.cs b/src/DotnetThirdPartyNotices/Services/IUriLicenseResolver.cs deleted file mode 100644 index f816542..0000000 --- a/src/DotnetThirdPartyNotices/Services/IUriLicenseResolver.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace DotnetThirdPartyNotices.Services; - -internal interface IUriLicenseResolver -{ - bool CanResolve(Uri licenseUri); - Task Resolve(Uri licenseUri); -}