diff --git a/README.md b/README.md
index 45048c2..a9de450 100644
--- a/README.md
+++ b/README.md
@@ -12,25 +12,26 @@ dotnet tool install -g DotnetThirdPartyNotices
## Get started
-Go inside the project directory and run:
+Go inside the project or solution directory and run:
```
dotnet-thirdpartynotices
```
-To change the name of the file that will be generated:
-
-```
-dotnet-thirdpartynotices --output-filename "third party notices.txt"
-```
-
If your project is in a different directory:
```
dotnet-thirdpartynotices
-dotnet-thirdpartynotices --output-filename "third party notices.txt"
```
+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
+
+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.
+
## How it works
### 1. Resolve assemblies
diff --git a/src/DotnetThirdPartyNotices/Commands/ScanCommand.cs b/src/DotnetThirdPartyNotices/Commands/ScanCommand.cs
new file mode 100644
index 0000000..4491600
--- /dev/null
+++ b/src/DotnetThirdPartyNotices/Commands/ScanCommand.cs
@@ -0,0 +1,141 @@
+using DotnetThirdPartyNotices.Models;
+using DotnetThirdPartyNotices.Services;
+using Microsoft.Build.Evaluation;
+using Microsoft.Build.Locator;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Collections.Generic;
+using System.CommandLine;
+using System.CommandLine.Invocation;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+
+namespace DotnetThirdPartyNotices.Commands;
+
+internal class ScanCommand : Command
+{
+ public ScanCommand() : base("scan", "A tool to generate file with third party legal notices for .NET projects")
+ {
+ 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("--filter", () => string.Empty, "Filter project files"));
+ }
+
+ internal new class Handler(ILogger logger, IProjectService projectService, ILicenseService licenseService) : ICommandHandler
+ {
+ public string? ScanDir { get; set; }
+ public string? OutputFilename { get; set; }
+ public bool CopyToOutDir { get; set; }
+ public string? Filter { get; set; }
+ public bool Merge { get; set; }
+ private readonly Dictionary> _licenseContents = [];
+ private readonly List _unresolvedFiles = [];
+
+ public int Invoke(InvocationContext context)
+ {
+ return 0;
+ }
+
+ public async Task InvokeAsync(InvocationContext context)
+ {
+ MSBuildLocator.RegisterDefaults();
+ ScanDir ??= Directory.GetCurrentDirectory();
+ var projectFilePaths = projectService.GetProjectFilePaths(ScanDir);
+ projectFilePaths = GetFilteredProjectPathes(projectFilePaths);
+ if (projectFilePaths.Length == 0)
+ {
+ logger.LogError("No C# or F# project file found in the directory");
+ return 0;
+ }
+ foreach (var projectFilePath in projectFilePaths)
+ await ScanProjectAsync(projectFilePath);
+ if (!CopyToOutDir)
+ await GenerateOutputFileAsync(OutputFilename);
+ return 0;
+ }
+
+ private string[] GetFilteredProjectPathes(string[] projectPathes)
+ {
+ if (string.IsNullOrEmpty(Filter))
+ return projectPathes;
+ var filterRegex = new Regex(Filter, RegexOptions.None, TimeSpan.FromMilliseconds(300));
+ return projectPathes.Where(x => filterRegex.IsMatch(x)).ToArray();
+ }
+
+ private async Task ScanProjectAsync(string projectFilePath)
+ {
+ var stopWatch = new Stopwatch();
+ stopWatch.Start();
+ logger.LogInformation("Resolving files for {ProjectName}...", Path.GetFileName(projectFilePath));
+ var project = new Project(projectFilePath);
+ project.SetProperty("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)
+ {
+ logger.LogInformation("Resolving license for {RelativeOutputPath}", resolvedFileInfo.RelativeOutputPath);
+ if (resolvedFileInfo.NuSpec != null)
+ {
+ logger.LogInformation("Package: {NuSpecId}", resolvedFileInfo.NuSpec.Id);
+ }
+ else
+ {
+ logger.LogWarning("Package not found");
+ }
+ var licenseContent = await licenseService.ResolveFromResolvedFileInfo(resolvedFileInfo);
+ if (licenseContent == null)
+ {
+ _unresolvedFiles.Add(resolvedFileInfo);
+ logger.LogError("No license found for {RelativeOutputPath}. Source path: {SourcePath}. Verify this manually", resolvedFileInfo.RelativeOutputPath, resolvedFileInfo.SourcePath);
+ continue;
+ }
+ if (!_licenseContents.ContainsKey(licenseContent))
+ _licenseContents[licenseContent] = [];
+ _licenseContents[licenseContent].Add(resolvedFileInfo);
+ }
+ 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)));
+ }
+
+ private async Task GenerateOutputFileAsync(string? outputFilePath)
+ {
+ if (outputFilePath == null)
+ return;
+ logger.LogInformation("Resolved {LicenseContentsCount} licenses for {Sum}/{ResolvedFilesCount} files", _licenseContents.Count, _licenseContents.Values.Sum(v => v.Count), _licenseContents.Values.Sum(v => v.Count) + _unresolvedFiles.Count);
+ logger.LogInformation("Unresolved files: {UnresolvedFilesCount}", _unresolvedFiles.Count);
+ var stopWatch = new Stopwatch();
+ stopWatch.Start();
+ var stringBuilder = new StringBuilder();
+ foreach (var (licenseContent, resolvedFileInfos) in _licenseContents)
+ {
+ var longestNameLen = 0;
+ foreach (var resolvedFileInfo in resolvedFileInfos)
+ {
+ var strLen = resolvedFileInfo.RelativeOutputPath?.Length ?? 0;
+ if (strLen > longestNameLen)
+ longestNameLen = strLen;
+ stringBuilder.AppendLine(resolvedFileInfo.RelativeOutputPath);
+ }
+ stringBuilder.AppendLine(new string('-', longestNameLen));
+ stringBuilder.AppendLine(licenseContent);
+ stringBuilder.AppendLine();
+ }
+ stopWatch.Stop();
+ logger.LogInformation("Generate licenses in {StopwatchElapsedMilliseconds}ms", stopWatch.ElapsedMilliseconds);
+ if (stringBuilder.Length == 0)
+ return;
+ logger.LogInformation("Writing to {OutputFilename}...", outputFilePath);
+ await System.IO.File.WriteAllTextAsync(outputFilePath, stringBuilder.ToString());
+ _licenseContents.Clear();
+ _unresolvedFiles.Clear();
+ }
+ }
+}
diff --git a/src/DotnetThirdPartyNotices/Directory.Packages.props b/src/DotnetThirdPartyNotices/Directory.Packages.props
index 28afc6b..b7a153e 100644
--- a/src/DotnetThirdPartyNotices/Directory.Packages.props
+++ b/src/DotnetThirdPartyNotices/Directory.Packages.props
@@ -9,12 +9,18 @@
+
+
+
+
+
+
@@ -27,4 +33,4 @@
-
+
\ No newline at end of file
diff --git a/src/DotnetThirdPartyNotices/DotnetThirdPartyNotices.csproj b/src/DotnetThirdPartyNotices/DotnetThirdPartyNotices.csproj
index 28be4c2..d9070d4 100644
--- a/src/DotnetThirdPartyNotices/DotnetThirdPartyNotices.csproj
+++ b/src/DotnetThirdPartyNotices/DotnetThirdPartyNotices.csproj
@@ -3,18 +3,25 @@
Exe
net8.0
+ enable
+ enable
default
true
dotnet-thirdpartynotices
DotnetThirdPartyNotices
A .NET tool to generate file with third party legal notices
- 0.2.8
+ 0.2.9
MIT
git
https://github.com/bugproof/DotnetThirdPartyNotices
+ https://github.com/bugproof/DotnetThirdPartyNotices
DotnetThirdPartyNotices
DotnetThirdPartyNotices
1591
+ bugproof
+ bugproof
+ en
+ True
@@ -24,12 +31,10 @@
-
-
-
-
+
+
-
+
diff --git a/src/DotnetThirdPartyNotices/Extensions/AssemblyExtensions.cs b/src/DotnetThirdPartyNotices/Extensions/AssemblyExtensions.cs
deleted file mode 100644
index 530a0c7..0000000
--- a/src/DotnetThirdPartyNotices/Extensions/AssemblyExtensions.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Reflection;
-
-namespace DotnetThirdPartyNotices.Extensions;
-
-internal static class AssemblyExtensions
-{
- public static IEnumerable GetInstances(this Assembly assembly) where T : class => assembly.GetTypes()
- .Where(t => t.IsClass && typeof(T).IsAssignableFrom(t))
- .Select(Activator.CreateInstance)
- .OfType();
-}
\ No newline at end of file
diff --git a/src/DotnetThirdPartyNotices/Extensions/ProjectExtensions.cs b/src/DotnetThirdPartyNotices/Extensions/ProjectExtensions.cs
deleted file mode 100644
index 6a9e051..0000000
--- a/src/DotnetThirdPartyNotices/Extensions/ProjectExtensions.cs
+++ /dev/null
@@ -1,110 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
-using System.Linq;
-using DotnetThirdPartyNotices.Models;
-using Microsoft.Build.Evaluation;
-using Microsoft.Build.Execution;
-using Microsoft.Build.Framework;
-using Microsoft.Build.Logging;
-using Serilog;
-using ILogger = Microsoft.Build.Framework.ILogger;
-
-namespace DotnetThirdPartyNotices.Extensions;
-
-internal static class ProjectExtensions
-{
- public static IEnumerable ResolveFiles(this Project project)
- {
- var targetFrameworksProperty = project.GetProperty("TargetFrameworks");
- if (targetFrameworksProperty != null)
- {
- var targetFrameworks = targetFrameworksProperty.EvaluatedValue.Split(';');
- project.SetProperty("TargetFramework", targetFrameworks[0]);
- }
-
- var projectInstance = project.CreateProjectInstance();
- var targetFrameworkIdentifier = projectInstance.GetPropertyValue("TargetFrameworkIdentifier");
-
- Log.Information("Target framework: {TargetFrameworkIdentifier}", targetFrameworkIdentifier);
-
- switch (targetFrameworkIdentifier)
- {
- case TargetFrameworkIdentifiers.NetCore:
- case TargetFrameworkIdentifiers.NetStandard:
- return ResolveFilesUsingComputeFilesToPublish(projectInstance);
- case TargetFrameworkIdentifiers.NetFramework:
- return ResolveFilesUsingResolveAssemblyReferences(projectInstance);
- default:
- throw new InvalidOperationException("Unsupported target framework.");
- }
- }
-
- private static IEnumerable ResolveFilesUsingResolveAssemblyReferences(ProjectInstance projectInstance)
- {
- var resolvedFileInfos = new List();
-
- projectInstance.Build("ResolveAssemblyReferences", new ILogger[] { new ConsoleLogger(LoggerVerbosity.Minimal) });
-
- foreach (var item in projectInstance.GetItems("ReferenceCopyLocalPaths"))
- {
- var assemblyPath = item.EvaluatedInclude;
- var versionInfo = FileVersionInfo.GetVersionInfo(assemblyPath);
-
- var resolvedFileInfo = new ResolvedFileInfo
- {
- VersionInfo = versionInfo,
- SourcePath = assemblyPath,
- RelativeOutputPath = Path.GetFileName(assemblyPath)
- };
-
- if (item.GetMetadataValue("ResolvedFrom") == "{HintPathFromItem}" && item.GetMetadataValue("HintPath").StartsWith("..\\packages"))
- {
- var nuSpec = NuSpec.FromAssemble(assemblyPath) ?? throw new ApplicationException( $"Cannot find package path from assembly path ({assemblyPath})" );
- resolvedFileInfo.NuSpec = nuSpec;
- resolvedFileInfos.Add(resolvedFileInfo);
- }
- else
- {
- resolvedFileInfos.Add(resolvedFileInfo);
- }
- }
-
- return resolvedFileInfos;
- }
-
- private static IEnumerable ResolveFilesUsingComputeFilesToPublish(ProjectInstance projectInstance)
- {
- var resolvedFileInfos = new List();
-
- projectInstance.Build("ComputeFilesToPublish", new ILogger[] { new ConsoleLogger(LoggerVerbosity.Minimal) });
-
- foreach (var item in projectInstance.GetItems("ResolvedFileToPublish"))
- {
- var assemblyPath = item.EvaluatedInclude;
-
- var packageName = item.GetMetadataValue(item.HasMetadata("PackageName") ? "PackageName" : "NugetPackageId");
- var packageVersion = item.GetMetadataValue(item.HasMetadata("PackageName") ? "PackageVersion" : "NugetPackageVersion");
- if (packageName == string.Empty || packageVersion == string.Empty)
- {
- // Skip if it's not a NuGet package
- continue;
- }
- var nuSpec = NuSpec.FromAssemble( assemblyPath ) ?? throw new ApplicationException( $"Cannot find package path from assembly path ({assemblyPath})" ); ;
-
- var relativePath = item.GetMetadataValue("RelativePath");
- var resolvedFileInfo = new ResolvedFileInfo
- {
- SourcePath = assemblyPath,
- VersionInfo = FileVersionInfo.GetVersionInfo(assemblyPath),
- NuSpec = nuSpec,
- RelativeOutputPath = relativePath
- };
-
- resolvedFileInfos.Add(resolvedFileInfo);
- }
-
- return resolvedFileInfos;
- }
-}
\ No newline at end of file
diff --git a/src/DotnetThirdPartyNotices/Extensions/ResolvedFileInfoExtensions.cs b/src/DotnetThirdPartyNotices/Extensions/ResolvedFileInfoExtensions.cs
deleted file mode 100644
index 998f7d4..0000000
--- a/src/DotnetThirdPartyNotices/Extensions/ResolvedFileInfoExtensions.cs
+++ /dev/null
@@ -1,211 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
-using System.Linq;
-using System.Reflection;
-using System.Threading.Tasks;
-using DotnetThirdPartyNotices.LicenseResolvers.Interfaces;
-using DotnetThirdPartyNotices.Models;
-
-namespace DotnetThirdPartyNotices.Extensions;
-
-internal static class ResolvedFileInfoExtensions
-{
- // Create instances only once
- private static readonly Lazy> LicenseResolvers =
- new(() => GetInstancesFromExecutingAssembly().ToList());
-
- private static readonly Lazy> LicenseUriLicenseResolvers =
- new(() => LicenseResolvers.Value.OfType().ToList());
-
- private static readonly Lazy> ProjectUriLicenseResolvers =
- new(() => LicenseResolvers.Value.OfType().ToList());
-
- private static readonly Lazy> RepositoryUriLicenseResolvers =
- new(() => LicenseResolvers.Value.OfType().ToList());
-
- private static readonly Lazy> FileVersionInfoLicenseResolvers =
- new(() => LicenseResolvers.Value.OfType().ToList());
-
- private static IEnumerable GetInstancesFromExecutingAssembly() where T : class
- {
- return Assembly.GetExecutingAssembly().GetInstances();
- }
-
- private static bool TryFindLicenseUriLicenseResolver(Uri licenseUri, out ILicenseUriLicenseResolver resolver)
- {
- resolver = LicenseUriLicenseResolvers.Value.FirstOrDefault(r => r.CanResolve(licenseUri));
- return resolver != null;
- }
-
- private static bool TryFindRepositoryUriLicenseResolver( Uri licenseUri, out IRepositoryUriLicenseResolver resolver )
- {
- resolver = RepositoryUriLicenseResolvers.Value.FirstOrDefault(r => r.CanResolve(licenseUri));
- return resolver != null;
- }
-
- private static bool TryFindProjectUriLicenseResolver(Uri projectUri, out IProjectUriLicenseResolver resolver)
- {
- resolver = ProjectUriLicenseResolvers.Value.FirstOrDefault(r => r.CanResolve(projectUri));
- return resolver != null;
- }
-
- private static bool TryFindFileVersionInfoLicenseResolver(
- FileVersionInfo fileVersionInfo, out IFileVersionInfoLicenseResolver resolver)
- {
- resolver = FileVersionInfoLicenseResolvers.Value.FirstOrDefault(r => r.CanResolve(fileVersionInfo));
- return resolver != null;
- }
-
- public static async Task ResolveLicense(this ResolvedFileInfo resolvedFileInfo)
- {
- if (resolvedFileInfo == null) throw new ArgumentNullException(nameof(resolvedFileInfo));
- string license = null;
- if (resolvedFileInfo.NuSpec != null)
- license = await ResolveLicenseFromNuspec(resolvedFileInfo);
-
- return license ?? await ResolveLicense(resolvedFileInfo.VersionInfo);
- }
-
- private static readonly Dictionary LicenseCache = new();
-
- private static async Task ResolveLicenseFromNuspec( ResolvedFileInfo resolvedFileInfo )
- {
- var nuSpec = resolvedFileInfo.NuSpec;
- if (LicenseCache.ContainsKey(nuSpec.Id))
- return LicenseCache[nuSpec.Id];
-
- var licenseUrl = nuSpec.LicenseUrl;
- var repositoryUrl = nuSpec.RepositoryUrl;
- var projectUrl = nuSpec.ProjectUrl;
-
- if (!string.IsNullOrEmpty(nuSpec.LicenseRelativePath))
- {
- if (LicenseCache.TryGetValue(nuSpec.LicenseRelativePath, out string value))
- return value;
- var license3 = await ResolveLicenseFromRelativePath(resolvedFileInfo.VersionInfo, nuSpec.LicenseRelativePath);
- if (license3 != null)
- {
- LicenseCache[nuSpec.Id] = license3;
- LicenseCache[nuSpec.LicenseRelativePath] = license3;
- return license3;
- }
- }
-
- // Try to get the license from license url
- if (!string.IsNullOrEmpty(nuSpec.LicenseUrl))
- {
- if (LicenseCache.TryGetValue(licenseUrl, out string value))
- return value;
-
- var license = await ResolveLicenseFromLicenseUri(new Uri(nuSpec.LicenseUrl));
- if (license != null)
- {
- LicenseCache[licenseUrl] = license;
- LicenseCache[nuSpec.Id] = license;
- return license;
- }
- }
-
- // Try to get the license from repository url
- if (!string.IsNullOrEmpty(repositoryUrl))
- {
- if (LicenseCache.TryGetValue(repositoryUrl, out string value))
- return value;
- var license = await ResolveLicenseFromRepositoryUri(new Uri(repositoryUrl));
- if (license != null)
- {
- LicenseCache[repositoryUrl] = license;
- LicenseCache[nuSpec.Id] = license;
- return license;
- }
- }
-
- // Otherwise try to get the license from project url
- if (string.IsNullOrEmpty(projectUrl))
- {
- if (LicenseCache.TryGetValue(projectUrl, out string value))
- return value;
-
- var license2 = await ResolveLicenseFromProjectUri(new Uri(projectUrl));
- if (license2 != null)
- {
- LicenseCache[nuSpec.Id] = license2;
- LicenseCache[nuSpec.ProjectUrl] = license2;
- return license2;
- }
- }
-
- return null;
- }
-
- private static async Task ResolveLicense(FileVersionInfo fileVersionInfo)
- {
- if (LicenseCache.ContainsKey(fileVersionInfo.FileName))
- return LicenseCache[fileVersionInfo.FileName];
- var license = await ResolveLicenseFromFileVersionInfo(fileVersionInfo);
- if(license == null)
- return null;
- LicenseCache[fileVersionInfo.FileName] = license;
- return license;
- }
-
- private static async Task ResolveLicenseFromLicenseUri(Uri licenseUri)
- {
- if (TryFindLicenseUriLicenseResolver(licenseUri, out var licenseUriLicenseResolver))
- return await licenseUriLicenseResolver.Resolve(licenseUri);
-
- // TODO: redirect uris should be checked at the very end to save us from redundant requests (when no resolver for anything can be found)
- var redirectUri = await licenseUri.GetRedirectUri();
- if (redirectUri != null)
- return await ResolveLicenseFromLicenseUri(redirectUri);
-
- // Finally, if no license uri can be found despite all the redirects, try to blindly get it
- return await licenseUri.GetPlainText();
- }
-
- private static async Task ResolveLicenseFromRepositoryUri(Uri repositoryUri)
- {
- if (TryFindRepositoryUriLicenseResolver(repositoryUri, out var repositoryUriLicenseResolver))
- return await repositoryUriLicenseResolver.Resolve(repositoryUri);
-
- // TODO: redirect uris should be checked at the very end to save us from redundant requests (when no resolver for anything can be found)
- var redirectUri = await repositoryUri.GetRedirectUri();
- if (redirectUri != null)
- return await ResolveLicenseFromLicenseUri(redirectUri);
-
- // Finally, if no license uri can be found despite all the redirects, try to blindly get it
- return await repositoryUri.GetPlainText();
- }
-
- private static async Task ResolveLicenseFromRelativePath(FileVersionInfo fileVersionInfo, string relativePath)
- {
- var packagePath = Utils.GetPackagePath( fileVersionInfo.FileName );
- var licenseFullPath = Path.Combine( packagePath, relativePath );
- if (!licenseFullPath.EndsWith(".txt") && !licenseFullPath.EndsWith( ".md" ) || !File.Exists( licenseFullPath ))
- return null;
- return await File.ReadAllTextAsync( licenseFullPath );
- }
-
- private static async Task ResolveLicenseFromProjectUri(Uri projectUri)
- {
- if (TryFindProjectUriLicenseResolver(projectUri, out var projectUriLicenseResolver))
- return await projectUriLicenseResolver.Resolve(projectUri);
-
- // TODO: redirect uris should be checked at the very end to save us from redundant requests (when no resolver for anything can be found)
- var redirectUri = await projectUri.GetRedirectUri();
- if (redirectUri != null)
- return await ResolveLicenseFromProjectUri(redirectUri);
-
- return null;
- }
-
- private static async Task ResolveLicenseFromFileVersionInfo(FileVersionInfo fileVersionInfo)
- {
- if (!TryFindFileVersionInfoLicenseResolver(fileVersionInfo, out var fileVersionInfoLicenseResolver))
- return null;
-
- return await fileVersionInfoLicenseResolver.Resolve(fileVersionInfo);
- }
-}
\ No newline at end of file
diff --git a/src/DotnetThirdPartyNotices/Extensions/UriExtensions.cs b/src/DotnetThirdPartyNotices/Extensions/UriExtensions.cs
deleted file mode 100644
index e05a1f1..0000000
--- a/src/DotnetThirdPartyNotices/Extensions/UriExtensions.cs
+++ /dev/null
@@ -1,69 +0,0 @@
-using System;
-using System.Net.Http;
-using System.Threading.Tasks;
-
-namespace DotnetThirdPartyNotices.Extensions;
-
-internal static class UriExtensions
-{
- public static bool IsGithubUri(this Uri uri) => uri.Host == "github.com";
-
- public static Uri ToRawGithubUserContentUri(this Uri uri)
- {
- if (!IsGithubUri(uri))
- throw new InvalidOperationException();
-
- var uriBuilder = new UriBuilder(uri) { Host = "raw.githubusercontent.com" };
- uriBuilder.Path = uriBuilder.Path.Replace("/blob", string.Empty);
- return uriBuilder.Uri;
- }
-
- public static async Task GetRedirectUri(this Uri uri)
- {
- using var httpClientHandler = new HttpClientHandler
- {
- AllowAutoRedirect = false
- };
- using var httpClient = new HttpClient(httpClientHandler);
- var httpResponseMessage = await httpClient.GetAsync(uri);
-
- var statusCode = (int)httpResponseMessage.StatusCode;
-
- if (statusCode is < 300 or > 399)
- {
- return null;
- }
-
- var redirectUri = httpResponseMessage.Headers.Location;
- if (!redirectUri.IsAbsoluteUri)
- {
- redirectUri = new Uri(httpResponseMessage.RequestMessage.RequestUri, redirectUri);
- }
-
- return redirectUri;
- }
-
- public static async Task GetPlainText(this Uri uri)
- {
- using var httpClient = new HttpClient();
- var httpResponseMessage = await httpClient.GetAsync(uri);
- if (!httpResponseMessage.IsSuccessStatusCode && uri.AbsolutePath.EndsWith(".txt"))
- {
- // try without .txt extension
- var fixedUri = new UriBuilder(uri);
- fixedUri.Path = fixedUri.Path.Remove(fixedUri.Path.Length - 4);
- httpResponseMessage = await httpClient.GetAsync(fixedUri.Uri);
- if (!httpResponseMessage.IsSuccessStatusCode)
- {
- return null;
- }
- }
-
- if (httpResponseMessage.Content.Headers.ContentType.MediaType != "text/plain")
- {
- return null;
- }
-
- return await httpResponseMessage.Content.ReadAsStringAsync();
- }
-}
\ No newline at end of file
diff --git a/src/DotnetThirdPartyNotices/GithubService.cs b/src/DotnetThirdPartyNotices/GithubService.cs
deleted file mode 100644
index 6f82241..0000000
--- a/src/DotnetThirdPartyNotices/GithubService.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-using System;
-using System.Net.Http;
-using System.Text;
-using System.Threading.Tasks;
-using System.Text.Json;
-
-namespace DotnetThirdPartyNotices;
-
-internal class GithubService : IDisposable
-{
- private readonly HttpClient _httpClient;
-
- public GithubService()
- {
- _httpClient = new HttpClient {BaseAddress = new Uri("https://api.github.com")};
- // https://developer.github.com/v3/#user-agent-required
- _httpClient.DefaultRequestHeaders.Add("User-Agent", "DotnetLicense");
- }
-
- public void Dispose()
- {
- _httpClient?.Dispose();
- }
-
- public async Task GetLicenseContentFromId(string licenseId)
- {
- var json = await _httpClient.GetStringAsync($"licenses/{licenseId}");
- var jsonDocument = JsonDocument.Parse(json);
- return jsonDocument.RootElement.GetProperty("body").GetString();
- }
-
- public async Task GetLicenseContentFromRepositoryPath(string repositoryPath)
- {
- repositoryPath = repositoryPath.TrimEnd('/');
- if (repositoryPath.EndsWith(".git"))
- repositoryPath = repositoryPath[..^4];
- var response = await _httpClient.GetAsync($"repos{repositoryPath}/license");
- if (!response.IsSuccessStatusCode)
- return null;
- var json = await response.Content.ReadAsStringAsync();
- var jsonDocument = JsonDocument.Parse(json);
-
- var rootElement = jsonDocument.RootElement;
- var encoding = rootElement.GetProperty("encoding").GetString();
- var content = rootElement.GetProperty("content").GetString();
-
- if (encoding != "base64") return content;
-
- var bytes = Convert.FromBase64String(content);
- return Encoding.UTF8.GetString(bytes);
- }
-
-}
\ No newline at end of file
diff --git a/src/DotnetThirdPartyNotices/LicenseResolvers/GithubLicenseResolver.cs b/src/DotnetThirdPartyNotices/LicenseResolvers/GithubLicenseResolver.cs
deleted file mode 100644
index 7024455..0000000
--- a/src/DotnetThirdPartyNotices/LicenseResolvers/GithubLicenseResolver.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-using System;
-using System.Threading.Tasks;
-using DotnetThirdPartyNotices.Extensions;
-using DotnetThirdPartyNotices.LicenseResolvers.Interfaces;
-
-namespace DotnetThirdPartyNotices.LicenseResolvers;
-
-internal class GithubLicenseResolver : ILicenseUriLicenseResolver, IProjectUriLicenseResolver, IRepositoryUriLicenseResolver
-{
- bool ILicenseUriLicenseResolver.CanResolve(Uri uri) => uri.IsGithubUri();
- bool IProjectUriLicenseResolver.CanResolve(Uri uri) => uri.IsGithubUri();
- bool IRepositoryUriLicenseResolver.CanResolve(Uri uri) => uri.IsGithubUri();
-
- Task ILicenseUriLicenseResolver.Resolve(Uri licenseUri)
- {
- licenseUri = licenseUri.ToRawGithubUserContentUri();
-
- return licenseUri.GetPlainText();
- }
-
- async Task IProjectUriLicenseResolver.Resolve(Uri projectUri)
- {
- using var githubService = new GithubService();
- return await githubService.GetLicenseContentFromRepositoryPath(projectUri.AbsolutePath);
- }
-
- async Task IRepositoryUriLicenseResolver.Resolve(Uri projectUri)
- {
- using var githubService = new GithubService();
- return await githubService.GetLicenseContentFromRepositoryPath(projectUri.AbsolutePath);
- }
-}
\ No newline at end of file
diff --git a/src/DotnetThirdPartyNotices/LicenseResolvers/Interfaces/ILicenseResolver.cs b/src/DotnetThirdPartyNotices/LicenseResolvers/Interfaces/ILicenseResolver.cs
deleted file mode 100644
index 552949d..0000000
--- a/src/DotnetThirdPartyNotices/LicenseResolvers/Interfaces/ILicenseResolver.cs
+++ /dev/null
@@ -1,5 +0,0 @@
-namespace DotnetThirdPartyNotices.LicenseResolvers.Interfaces;
-
-internal interface ILicenseResolver
-{
-}
\ No newline at end of file
diff --git a/src/DotnetThirdPartyNotices/LicenseResolvers/Interfaces/ILicenseUriLicenseResolver.cs b/src/DotnetThirdPartyNotices/LicenseResolvers/Interfaces/ILicenseUriLicenseResolver.cs
deleted file mode 100644
index 7d69c67..0000000
--- a/src/DotnetThirdPartyNotices/LicenseResolvers/Interfaces/ILicenseUriLicenseResolver.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using System;
-using System.Threading.Tasks;
-
-namespace DotnetThirdPartyNotices.LicenseResolvers.Interfaces;
-
-internal interface ILicenseUriLicenseResolver : ILicenseResolver
-{
- bool CanResolve(Uri licenseUri);
- Task Resolve(Uri licenseUri);
-}
\ No newline at end of file
diff --git a/src/DotnetThirdPartyNotices/LicenseResolvers/Interfaces/IProjectUriLicenseResolver.cs b/src/DotnetThirdPartyNotices/LicenseResolvers/Interfaces/IProjectUriLicenseResolver.cs
deleted file mode 100644
index ee54616..0000000
--- a/src/DotnetThirdPartyNotices/LicenseResolvers/Interfaces/IProjectUriLicenseResolver.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using System;
-using System.Threading.Tasks;
-
-namespace DotnetThirdPartyNotices.LicenseResolvers.Interfaces;
-
-internal interface IProjectUriLicenseResolver : ILicenseResolver
-{
- bool CanResolve(Uri projectUri);
- Task Resolve(Uri projectUri);
-}
\ No newline at end of file
diff --git a/src/DotnetThirdPartyNotices/LicenseResolvers/Interfaces/IRepositoryUriLicenseResolver.cs b/src/DotnetThirdPartyNotices/LicenseResolvers/Interfaces/IRepositoryUriLicenseResolver.cs
deleted file mode 100644
index 4d395f7..0000000
--- a/src/DotnetThirdPartyNotices/LicenseResolvers/Interfaces/IRepositoryUriLicenseResolver.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace DotnetThirdPartyNotices.LicenseResolvers.Interfaces;
-
-internal interface IRepositoryUriLicenseResolver : ILicenseResolver
-{
- bool CanResolve(Uri projectUri);
- Task Resolve(Uri projectUri);
-}
\ No newline at end of file
diff --git a/src/DotnetThirdPartyNotices/LicenseResolvers/LocalPackageLicenseResolver.cs b/src/DotnetThirdPartyNotices/LicenseResolvers/LocalPackageLicenseResolver.cs
deleted file mode 100644
index eaf96f3..0000000
--- a/src/DotnetThirdPartyNotices/LicenseResolvers/LocalPackageLicenseResolver.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using DotnetThirdPartyNotices.LicenseResolvers.Interfaces;
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace DotnetThirdPartyNotices.LicenseResolvers;
-
-internal class LocalPackageLicenseResolver : IFileVersionInfoLicenseResolver
-{
- public bool CanResolve( FileVersionInfo fileVersionInfo ) => GetLicensePath(fileVersionInfo) != null;
-
- public async Task Resolve(FileVersionInfo fileVersionInfo)
- {
- var licensePath = GetLicensePath(fileVersionInfo);
- if (licensePath == null)
- return null;
- return await File.ReadAllTextAsync( licensePath );
- }
-
- private string GetLicensePath( FileVersionInfo fileVersionInfo )
- {
- var directoryPath = Utils.GetPackagePath( fileVersionInfo.FileName ) ?? Path.GetDirectoryName( fileVersionInfo.FileName );
- return Directory.EnumerateFiles( directoryPath, "license.*", new EnumerationOptions
- {
- MatchCasing = MatchCasing.CaseInsensitive,
- RecurseSubdirectories = false
- } ).FirstOrDefault( x => x.EndsWith( "\\license.txt", StringComparison.OrdinalIgnoreCase ) || x.EndsWith( "\\license.md", StringComparison.OrdinalIgnoreCase ) );
- }
-}
diff --git a/src/DotnetThirdPartyNotices/LicenseResolvers/OpenSourceOrgLicenseResolver.cs b/src/DotnetThirdPartyNotices/LicenseResolvers/OpenSourceOrgLicenseResolver.cs
deleted file mode 100644
index d925a8a..0000000
--- a/src/DotnetThirdPartyNotices/LicenseResolvers/OpenSourceOrgLicenseResolver.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using System;
-using System.Threading.Tasks;
-using DotnetThirdPartyNotices.LicenseResolvers.Interfaces;
-
-namespace DotnetThirdPartyNotices.LicenseResolvers;
-
-internal class OpenSourceOrgLicenseResolver : ILicenseUriLicenseResolver
-{
- public bool CanResolve(Uri licenseUri) => licenseUri.Host == "opensource.org";
-
- public async Task Resolve(Uri licenseUri)
- {
- var s = licenseUri.AbsolutePath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
-
- if (s[0] != "licenses") return null;
-
- var licenseId = s[1];
- using var githubService = new GithubService();
- return await githubService.GetLicenseContentFromId(licenseId);
- }
-}
\ No newline at end of file
diff --git a/src/DotnetThirdPartyNotices/Models/NuSpec.cs b/src/DotnetThirdPartyNotices/Models/NuSpec.cs
new file mode 100644
index 0000000..27fc7a8
--- /dev/null
+++ b/src/DotnetThirdPartyNotices/Models/NuSpec.cs
@@ -0,0 +1,16 @@
+namespace DotnetThirdPartyNotices.Models;
+
+public record NuSpec
+{
+ public NuSpec(string id)
+ {
+ Id = id;
+ }
+
+ public string Id { get; }
+ public string? Version { get; init; }
+ public string? LicenseUrl { get; init; }
+ public string? ProjectUrl { get; init; }
+ public string? RepositoryUrl { get; init; }
+ public string? LicenseRelativePath { get; init; }
+}
\ No newline at end of file
diff --git a/src/DotnetThirdPartyNotices/Models/ResolvedFileInfo.cs b/src/DotnetThirdPartyNotices/Models/ResolvedFileInfo.cs
index bdcf646..12059da 100644
--- a/src/DotnetThirdPartyNotices/Models/ResolvedFileInfo.cs
+++ b/src/DotnetThirdPartyNotices/Models/ResolvedFileInfo.cs
@@ -4,8 +4,9 @@ namespace DotnetThirdPartyNotices.Models;
internal class ResolvedFileInfo
{
- public string SourcePath { get; set; }
- public string RelativeOutputPath { get; set; }
- public FileVersionInfo VersionInfo { get; set; }
- public NuSpec NuSpec { get; set; }
+ public string? SourcePath { get; init; }
+ public string? RelativeOutputPath { get; init; }
+ public FileVersionInfo? VersionInfo { get; init; }
+ public NuSpec? NuSpec { get; init; }
+ public string? PackagePath { get; init; }
}
\ No newline at end of file
diff --git a/src/DotnetThirdPartyNotices/TargetFrameworkIdentifiers.cs b/src/DotnetThirdPartyNotices/Models/TargetFrameworkIdentifiers.cs
similarity index 82%
rename from src/DotnetThirdPartyNotices/TargetFrameworkIdentifiers.cs
rename to src/DotnetThirdPartyNotices/Models/TargetFrameworkIdentifiers.cs
index b1ad56a..60ca410 100644
--- a/src/DotnetThirdPartyNotices/TargetFrameworkIdentifiers.cs
+++ b/src/DotnetThirdPartyNotices/Models/TargetFrameworkIdentifiers.cs
@@ -1,4 +1,4 @@
-namespace DotnetThirdPartyNotices;
+namespace DotnetThirdPartyNotices.Models;
internal static class TargetFrameworkIdentifiers
{
diff --git a/src/DotnetThirdPartyNotices/NuSpec.cs b/src/DotnetThirdPartyNotices/NuSpec.cs
deleted file mode 100644
index 9d515e4..0000000
--- a/src/DotnetThirdPartyNotices/NuSpec.cs
+++ /dev/null
@@ -1,69 +0,0 @@
-using System;
-using System.IO;
-using System.IO.Compression;
-using System.Linq;
-using System.Xml;
-using System.Xml.Linq;
-
-namespace DotnetThirdPartyNotices;
-
-public record NuSpec
-{
- public string Id { get; init; }
- public string Version { get; init; }
- public string LicenseUrl { get; init; }
- public string ProjectUrl { get; init; }
- public string RepositoryUrl { get; init; }
- public string LicenseRelativePath { get; init; }
-
- private static NuSpec FromTextReader(TextReader streamReader)
- {
- using var xmlReader = XmlReader.Create(streamReader);
- var xDocument = XDocument.Load(xmlReader);
- if (xDocument.Root == null) return null;
- var ns = xDocument.Root.GetDefaultNamespace();
-
- var metadata = xDocument.Root.Element(ns + "metadata");
- if (metadata == null) return null;
-
- return new NuSpec
- {
- Id = metadata.Element(ns + "id")?.Value,
- Version = metadata.Element(ns + "version")?.Value,
- LicenseUrl = metadata.Element(ns + "licenseUrl")?.Value,
- ProjectUrl = metadata.Element(ns + "projectUrl")?.Value,
- RepositoryUrl = metadata.Element(ns + "repository")?.Attribute("url")?.Value,
- LicenseRelativePath = metadata.Elements(ns + "license").Where(x => x.Attribute("type")?.Value == "file").FirstOrDefault()?.Value
- };
- }
-
- public static NuSpec FromFile(string fileName)
- {
- ArgumentNullException.ThrowIfNull( fileName );
- using var xmlReader = new StreamReader(fileName);
- return FromTextReader(xmlReader);
- }
-
- public static NuSpec FromNupkg(string fileName)
- {
- ArgumentNullException.ThrowIfNull( fileName );
- using var zipToCreate = new FileStream(fileName, FileMode.Open, FileAccess.Read);
- using var zip = new ZipArchive(zipToCreate, ZipArchiveMode.Read);
- var zippedNuspec = zip.Entries.Single(e => e.FullName.EndsWith(".nuspec"));
- using var stream = zippedNuspec.Open();
- using var streamReader = new StreamReader(stream);
- return FromTextReader(streamReader);
- }
-
- public static NuSpec FromAssemble(string assemblePath)
- {
- if (assemblePath == null) throw new ArgumentNullException(nameof(assemblePath));
- var nuspec = Utils.GetNuspecPath(assemblePath);
- if (nuspec != null)
- return FromFile( nuspec );
- var nupkg = Utils.GetNupkgPath(assemblePath);
- if(nupkg != null)
- return FromNupkg(nupkg);
- return null;
- }
-}
\ No newline at end of file
diff --git a/src/DotnetThirdPartyNotices/Program.cs b/src/DotnetThirdPartyNotices/Program.cs
index 954d109..43986ed 100644
--- a/src/DotnetThirdPartyNotices/Program.cs
+++ b/src/DotnetThirdPartyNotices/Program.cs
@@ -1,142 +1,52 @@
-using DotnetThirdPartyNotices.Extensions;
-using DotnetThirdPartyNotices.Models;
-using Microsoft.Build.Evaluation;
-using Microsoft.Build.Locator;
-using System.Collections.Generic;
+using DotnetThirdPartyNotices.Commands;
+using DotnetThirdPartyNotices.Services;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Serilog;
+using Serilog.Events;
using System.CommandLine;
-using System.Diagnostics;
-using System.IO;
+using System.CommandLine.Builder;
+using System.CommandLine.Hosting;
+using System.CommandLine.Parsing;
using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using Serilog;
+using System.Reflection;
Log.Logger = new LoggerConfiguration()
- .MinimumLevel.Information()
+ .MinimumLevel.Warning()
+ .MinimumLevel.Override("DotnetThirdPartyNotices", LogEventLevel.Debug)
+ .Enrich.FromLogContext()
.WriteTo.Console()
.CreateLogger();
-var scanDirArgument = new Argument("scan-dir", "Path of the directory to look for projects (optional)") {Arity = ArgumentArity.ZeroOrOne};
-var outputFileOption = new Option(
- "--output-filename",
- () => "third-party-notices.txt",
- "Output filename");
-var copyToOutDirOption =
- new Option("--copy-to-outdir", () => false, "Copy to output directory in Release configuration");
-
-var rootCommand = new RootCommand("A tool to generate file with third party legal notices for .NET projects")
-{
- scanDirArgument,
- outputFileOption,
- copyToOutDirOption
-};
-
-rootCommand.SetHandler(async (scanDir, outputFilename, copyToOutDir) =>
-{
- MSBuildLocator.RegisterDefaults();
-
- await Run(scanDir, outputFilename, copyToOutDir);
-}, scanDirArgument, outputFileOption, copyToOutDirOption);
-
-return await rootCommand.InvokeAsync(args);
-
-static async Task Run(string scanDir, string outputFilename, bool copyToOutputDir)
-{
- var scanDirectory = scanDir ?? Directory.GetCurrentDirectory();
- Log.Information("Scan directory {ScanDirectory}", scanDirectory);
- var projectFilePath = Directory.GetFiles(scanDirectory, "*.*", SearchOption.TopDirectoryOnly)
- .SingleOrDefault(s => s.EndsWith(".csproj") || s.EndsWith(".fsproj"));
- if (projectFilePath == null)
- {
- Log.Error("No C# or F# project file found in the current directory");
- return;
- }
-
- var project = new Project(projectFilePath);
- project.SetProperty("Configuration", "Release");
- project.SetProperty("DesignTimeBuild", "true");
-
- Log.Information("Resolving files...");
-
- var stopwatch = new Stopwatch();
-
- stopwatch.Start();
-
- var licenseContents = new Dictionary>();
- var resolvedFiles = project.ResolveFiles().ToList();
-
- Log.Information("Resolved files count: {ResolvedFilesCount}", resolvedFiles.Count);
-
- var unresolvedFiles = new List();
-
- foreach (var resolvedFileInfo in resolvedFiles)
- {
- Log.Information("Resolving license for {RelativeOutputPath}", resolvedFileInfo.RelativeOutputPath);
- if (resolvedFileInfo.NuSpec != null)
- {
- Log.Information("Package: {NuSpecId}", resolvedFileInfo.NuSpec.Id);
- }
- else
- {
- Log.Warning("Package not found");
- }
-
- var licenseContent = await resolvedFileInfo.ResolveLicense();
- if (licenseContent == null)
- {
- unresolvedFiles.Add(resolvedFileInfo);
- Log.Error("No license found for {RelativeOutputPath}. Source path: {SourcePath}. Verify this manually",
- resolvedFileInfo.RelativeOutputPath, resolvedFileInfo.SourcePath);
- continue;
- }
-
- if (!licenseContents.ContainsKey(licenseContent))
- licenseContents[licenseContent] = new List();
-
- licenseContents[licenseContent].Add(resolvedFileInfo);
- }
-
- stopwatch.Stop();
-
- Log.Information("Resolved {LicenseContentsCount} licenses for {Sum}/{ResolvedFilesCount} files in {StopwatchElapsedMilliseconds}ms", licenseContents.Count, licenseContents.Values.Sum(v => v.Count), resolvedFiles.Count, stopwatch.ElapsedMilliseconds);
- Log.Information("Unresolved files: {UnresolvedFilesCount}", unresolvedFiles.Count);
-
- stopwatch.Start();
-
- var stringBuilder = new StringBuilder();
-
- foreach (var (licenseContent, resolvedFileInfos) in licenseContents)
- {
- var longestNameLen = 0;
- foreach (var resolvedFileInfo in resolvedFileInfos)
- {
- var strLen = resolvedFileInfo.RelativeOutputPath.Length;
- if (strLen > longestNameLen)
- longestNameLen = strLen;
-
- stringBuilder.AppendLine(resolvedFileInfo.RelativeOutputPath);
- }
-
- stringBuilder.AppendLine(new string('-', longestNameLen));
-
- stringBuilder.AppendLine(licenseContent);
- stringBuilder.AppendLine();
- }
-
- stopwatch.Stop();
-
- if (stringBuilder.Length > 0)
+var builder = new CommandLineBuilder(new ScanCommand())
+ .UseDefaults()
+ .UseHost(Host.CreateDefaultBuilder, builder =>
{
- if (copyToOutputDir)
+ builder.UseCommandHandler();
+ builder.ConfigureServices(x =>
{
- outputFilename = Path.Combine(
- Path.Combine(scanDirectory, project.GetPropertyValue("OutDir")),
- Path.GetFileName(outputFilename));
- }
-
- Log.Information("Writing to {OutputFilename}...", outputFilename);
- await File.WriteAllTextAsync(outputFilename, stringBuilder.ToString());
-
- Log.Information("Done in {StopwatchElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
- }
-}
\ No newline at end of file
+ x.AddLogging(x =>
+ {
+ x.ClearProviders();
+ x.AddSerilog(dispose: true);
+ });
+ x.AddSingleton();
+ x.AddSingleton();
+ x.AddSingleton();
+ x.AddHttpClient();
+ var assembly = Assembly.GetExecutingAssembly();
+ var types = assembly.GetTypes();
+ foreach (var type in types.Where(t => typeof(ILicenseUriLicenseResolver).IsAssignableFrom(t) && !t.IsInterface))
+ x.AddSingleton(typeof(ILicenseUriLicenseResolver), type);
+ foreach (var type in types.Where(t => typeof(IProjectUriLicenseResolver).IsAssignableFrom(t) && !t.IsInterface))
+ x.AddSingleton(typeof(IProjectUriLicenseResolver), type);
+ foreach (var type in types.Where(t => typeof(IRepositoryUriLicenseResolver).IsAssignableFrom(t) && !t.IsInterface))
+ x.AddSingleton(typeof(IRepositoryUriLicenseResolver), type);
+ foreach (var type in types.Where(t => typeof(IFileVersionInfoLicenseResolver).IsAssignableFrom(t) && !t.IsInterface))
+ x.AddSingleton(typeof(IFileVersionInfoLicenseResolver), type);
+ });
+ })
+ .Build();
+await builder.InvokeAsync(args);
+return 0;
\ No newline at end of file
diff --git a/src/DotnetThirdPartyNotices/Services/GithubRawLicenseResolver.cs b/src/DotnetThirdPartyNotices/Services/GithubRawLicenseResolver.cs
new file mode 100644
index 0000000..ba49b13
--- /dev/null
+++ b/src/DotnetThirdPartyNotices/Services/GithubRawLicenseResolver.cs
@@ -0,0 +1,20 @@
+namespace DotnetThirdPartyNotices.Services;
+
+internal class GithubRawLicenseResolver(IHttpClientFactory httpClientFactory) : ILicenseUriLicenseResolver
+{
+ public bool CanResolve(Uri uri) => uri.Host == "github.com";
+
+ public async Task Resolve(Uri uri)
+ {
+ 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 (!httpResponseMessage.IsSuccessStatusCode
+ || httpResponseMessage.Content.Headers.ContentType?.MediaType != "text/plain")
+ return null;
+ return await httpResponseMessage.Content.ReadAsStringAsync();
+ }
+}
diff --git a/src/DotnetThirdPartyNotices/Services/GithubRepositoryLicenseResolver.cs b/src/DotnetThirdPartyNotices/Services/GithubRepositoryLicenseResolver.cs
new file mode 100644
index 0000000..a8452de
--- /dev/null
+++ b/src/DotnetThirdPartyNotices/Services/GithubRepositoryLicenseResolver.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Net.Http;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
+
+namespace DotnetThirdPartyNotices.Services;
+
+internal class GithubRepositoryLicenseResolver(IHttpClientFactory httpClientFactory) : IProjectUriLicenseResolver, IRepositoryUriLicenseResolver
+{
+ public bool CanResolve(Uri uri) => uri.Host == "github.com";
+
+ public async Task Resolve(Uri licenseUri)
+ {
+ var repositoryPath = licenseUri.AbsolutePath.TrimEnd('/');
+ if (repositoryPath.EndsWith(".git"))
+ repositoryPath = repositoryPath[..^4];
+ var httpClient = httpClientFactory.CreateClient();
+ UriBuilder uriBuilder = new("https://api.github.com")
+ {
+ Path = $"repos{repositoryPath}/license"
+ };
+ // https://developer.github.com/v3/#user-agent-required
+ httpClient.DefaultRequestHeaders.Add("User-Agent", "DotnetLicense");
+ var response = await httpClient.GetAsync(uriBuilder.Uri);
+ if (!response.IsSuccessStatusCode)
+ return null;
+ var json = await response.Content.ReadAsStringAsync();
+ var jsonDocument = JsonDocument.Parse(json);
+ var rootElement = jsonDocument.RootElement;
+ var encoding = rootElement.GetProperty("encoding").GetString();
+ var content = rootElement.GetProperty("content").GetString();
+ if (content == null || encoding != "base64")
+ return content;
+ var bytes = Convert.FromBase64String(content);
+ return Encoding.UTF8.GetString(bytes);
+ }
+}
\ No newline at end of file
diff --git a/src/DotnetThirdPartyNotices/LicenseResolvers/Interfaces/IFileVersionInfoLicenseResolver.cs b/src/DotnetThirdPartyNotices/Services/IFileVersionInfoLicenseResolver.cs
similarity index 60%
rename from src/DotnetThirdPartyNotices/LicenseResolvers/Interfaces/IFileVersionInfoLicenseResolver.cs
rename to src/DotnetThirdPartyNotices/Services/IFileVersionInfoLicenseResolver.cs
index 7645946..01ae395 100644
--- a/src/DotnetThirdPartyNotices/LicenseResolvers/Interfaces/IFileVersionInfoLicenseResolver.cs
+++ b/src/DotnetThirdPartyNotices/Services/IFileVersionInfoLicenseResolver.cs
@@ -1,10 +1,10 @@
using System.Diagnostics;
using System.Threading.Tasks;
-namespace DotnetThirdPartyNotices.LicenseResolvers.Interfaces;
+namespace DotnetThirdPartyNotices.Services;
internal interface IFileVersionInfoLicenseResolver : ILicenseResolver
{
bool CanResolve(FileVersionInfo fileVersionInfo);
- Task Resolve(FileVersionInfo fileVersionInfo);
+ Task Resolve(FileVersionInfo fileVersionInfo);
}
\ No newline at end of file
diff --git a/src/DotnetThirdPartyNotices/Services/ILicenseResolver.cs b/src/DotnetThirdPartyNotices/Services/ILicenseResolver.cs
new file mode 100644
index 0000000..d2650bf
--- /dev/null
+++ b/src/DotnetThirdPartyNotices/Services/ILicenseResolver.cs
@@ -0,0 +1,5 @@
+namespace DotnetThirdPartyNotices.Services;
+
+internal interface ILicenseResolver
+{
+}
\ No newline at end of file
diff --git a/src/DotnetThirdPartyNotices/Services/ILicenseService.cs b/src/DotnetThirdPartyNotices/Services/ILicenseService.cs
new file mode 100644
index 0000000..db30bf8
--- /dev/null
+++ b/src/DotnetThirdPartyNotices/Services/ILicenseService.cs
@@ -0,0 +1,8 @@
+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/ILicenseUriLicenseResolver.cs b/src/DotnetThirdPartyNotices/Services/ILicenseUriLicenseResolver.cs
new file mode 100644
index 0000000..d9dd6e2
--- /dev/null
+++ b/src/DotnetThirdPartyNotices/Services/ILicenseUriLicenseResolver.cs
@@ -0,0 +1,6 @@
+namespace DotnetThirdPartyNotices.Services;
+
+internal interface ILicenseUriLicenseResolver : IUriLicenseResolver
+{
+
+}
\ No newline at end of file
diff --git a/src/DotnetThirdPartyNotices/Services/ILocalPackageService.cs b/src/DotnetThirdPartyNotices/Services/ILocalPackageService.cs
new file mode 100644
index 0000000..8577aa7
--- /dev/null
+++ b/src/DotnetThirdPartyNotices/Services/ILocalPackageService.cs
@@ -0,0 +1,14 @@
+using DotnetThirdPartyNotices.Models;
+
+namespace DotnetThirdPartyNotices.Services;
+
+public interface ILocalPackageService
+{
+ string? GetNupkgPathFromPackagePath(string? packagePath);
+ NuSpec? GetNuSpecFromNupkgPath(string? path);
+ NuSpec? GetNuSpecFromNuspecPath(string? path);
+ NuSpec? GetNuSpecFromPackagePath(string? path);
+ NuSpec? GetNuSpecFromStreamReader(TextReader? textReader);
+ string? GetNuspecPathFromPackagePath(string? packagePath);
+ string? GetPackagePathFromAssemblyPath(string? assemblyPath);
+}
\ No newline at end of file
diff --git a/src/DotnetThirdPartyNotices/Services/IProjectService.cs b/src/DotnetThirdPartyNotices/Services/IProjectService.cs
new file mode 100644
index 0000000..a93d908
--- /dev/null
+++ b/src/DotnetThirdPartyNotices/Services/IProjectService.cs
@@ -0,0 +1,11 @@
+using DotnetThirdPartyNotices.Models;
+using Microsoft.Build.Evaluation;
+using System.Collections.Generic;
+
+namespace DotnetThirdPartyNotices.Services;
+
+internal interface IProjectService
+{
+ string[] GetProjectFilePaths(string directoryPath);
+ IEnumerable ResolveFiles(Project project);
+}
\ No newline at end of file
diff --git a/src/DotnetThirdPartyNotices/Services/IProjectUriLicenseResolver.cs b/src/DotnetThirdPartyNotices/Services/IProjectUriLicenseResolver.cs
new file mode 100644
index 0000000..0f4a984
--- /dev/null
+++ b/src/DotnetThirdPartyNotices/Services/IProjectUriLicenseResolver.cs
@@ -0,0 +1,6 @@
+namespace DotnetThirdPartyNotices.Services;
+
+internal interface IProjectUriLicenseResolver : IUriLicenseResolver
+{
+
+}
\ No newline at end of file
diff --git a/src/DotnetThirdPartyNotices/Services/IRepositoryUriLicenseResolver.cs b/src/DotnetThirdPartyNotices/Services/IRepositoryUriLicenseResolver.cs
new file mode 100644
index 0000000..3b19854
--- /dev/null
+++ b/src/DotnetThirdPartyNotices/Services/IRepositoryUriLicenseResolver.cs
@@ -0,0 +1,6 @@
+namespace DotnetThirdPartyNotices.Services;
+
+internal interface IRepositoryUriLicenseResolver : IUriLicenseResolver
+{
+
+}
\ No newline at end of file
diff --git a/src/DotnetThirdPartyNotices/Services/IUriLicenseResolver.cs b/src/DotnetThirdPartyNotices/Services/IUriLicenseResolver.cs
new file mode 100644
index 0000000..f816542
--- /dev/null
+++ b/src/DotnetThirdPartyNotices/Services/IUriLicenseResolver.cs
@@ -0,0 +1,7 @@
+namespace DotnetThirdPartyNotices.Services;
+
+internal interface IUriLicenseResolver
+{
+ bool CanResolve(Uri licenseUri);
+ Task Resolve(Uri licenseUri);
+}
diff --git a/src/DotnetThirdPartyNotices/Services/LicenseService.cs b/src/DotnetThirdPartyNotices/Services/LicenseService.cs
new file mode 100644
index 0000000..30086f4
--- /dev/null
+++ b/src/DotnetThirdPartyNotices/Services/LicenseService.cs
@@ -0,0 +1,209 @@
+using DotnetThirdPartyNotices.Models;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Threading.Tasks;
+
+namespace DotnetThirdPartyNotices.Services;
+
+internal class LicenseService(IEnumerable licenseUriLicenseResolvers, IEnumerable projectUriLicenseResolvers, IEnumerable repositoryUriLicenseResolvers, IEnumerable fileVersionInfoLicenseResolvers, IHttpClientFactory httpClientFactory) : ILicenseService
+{
+ private static readonly Dictionary LicenseCache = [];
+
+ public async Task ResolveFromResolvedFileInfo(ResolvedFileInfo resolvedFileInfo)
+ {
+ 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 ResolveFromAssemblyPathAsync(resolvedFileInfo))
+ ?? (await ResolveFromFileVersionInfoAsync(resolvedFileInfo))
+ ?? (await ResolveFromLicenseUrlAsync(resolvedFileInfo, true))
+ ?? (await ResolveFromRepositoryUrlAsync(resolvedFileInfo, true))
+ ?? (await ResolveFromProjectUrlAsync(resolvedFileInfo, true));
+ }
+
+ private async Task ResolveFromPackagePathAsync(ResolvedFileInfo resolvedFileInfo)
+ {
+ if (string.IsNullOrEmpty(resolvedFileInfo.PackagePath))
+ return null;
+ if (LicenseCache.TryGetValue(resolvedFileInfo.PackagePath, out string? value))
+ return value;
+ var licensePath = Directory.EnumerateFiles(resolvedFileInfo.PackagePath, "license.*", new EnumerationOptions
+ {
+ MatchCasing = MatchCasing.CaseInsensitive,
+ RecurseSubdirectories = false
+ }).FirstOrDefault(x => x.EndsWith("\\license.txt", StringComparison.OrdinalIgnoreCase) || x.EndsWith("\\license.md", StringComparison.OrdinalIgnoreCase));
+ if (licensePath == null)
+ return null;
+ var license = await File.ReadAllTextAsync(licensePath);
+ if (resolvedFileInfo.NuSpec != null)
+ LicenseCache[resolvedFileInfo.NuSpec.Id] = license;
+ LicenseCache[resolvedFileInfo.PackagePath] = license;
+ return license;
+ }
+
+ private async Task ResolveFromAssemblyPathAsync(ResolvedFileInfo resolvedFileInfo)
+ {
+ if (string.IsNullOrEmpty(resolvedFileInfo.SourcePath))
+ return null;
+ var assemblyPath = Path.GetDirectoryName(resolvedFileInfo.SourcePath);
+ if(assemblyPath == null)
+ return null;
+ if (LicenseCache.TryGetValue(assemblyPath, out string? value))
+ return value;
+ var licensePath = Directory.EnumerateFiles(assemblyPath, "license.*", new EnumerationOptions
+ {
+ MatchCasing = MatchCasing.CaseInsensitive,
+ RecurseSubdirectories = false
+ }).FirstOrDefault(x => x.EndsWith("\\license.txt", StringComparison.OrdinalIgnoreCase) || x.EndsWith("\\license.md", StringComparison.OrdinalIgnoreCase));
+ if (licensePath == null)
+ return null;
+ var license = await File.ReadAllTextAsync(licensePath);
+ LicenseCache[assemblyPath] = license;
+ return license;
+ }
+
+ private async Task ResolveFromFileVersionInfoAsync(ResolvedFileInfo resolvedFileInfo)
+ {
+ if(resolvedFileInfo.VersionInfo == null)
+ return null;
+ if (LicenseCache.TryGetValue(resolvedFileInfo.VersionInfo.FileName, out string? value))
+ return value;
+ foreach (var resolver in fileVersionInfoLicenseResolvers)
+ {
+ if (!resolver.CanResolve(resolvedFileInfo.VersionInfo))
+ continue;
+ var license = await resolver.Resolve(resolvedFileInfo.VersionInfo);
+ if (license != null)
+ {
+ LicenseCache[resolvedFileInfo.VersionInfo.FileName] = license;
+ return license;
+ }
+ }
+ return null;
+ }
+
+ private async Task ResolveFromLicenseRelativePathAsync(ResolvedFileInfo resolvedFileInfo)
+ {
+ if (string.IsNullOrEmpty(resolvedFileInfo.PackagePath) || string.IsNullOrEmpty(resolvedFileInfo.NuSpec?.LicenseRelativePath))
+ return null;
+ var licenseFullPath = Path.Combine(resolvedFileInfo.PackagePath, resolvedFileInfo.NuSpec.LicenseRelativePath);
+ if (LicenseCache.TryGetValue(licenseFullPath, out string? value))
+ return value;
+ if (!licenseFullPath.EndsWith(".txt") && !licenseFullPath.EndsWith(".md") || !File.Exists(licenseFullPath))
+ return null;
+ var license = await File.ReadAllTextAsync(licenseFullPath);
+ if (string.IsNullOrEmpty(license))
+ return null;
+ LicenseCache[resolvedFileInfo.NuSpec.Id] = license;
+ LicenseCache[licenseFullPath] = license;
+ return license;
+ }
+
+ private async Task ResolveFromProjectUrlAsync(ResolvedFileInfo resolvedFileInfo, bool useFinalUrl)
+ {
+ if (string.IsNullOrEmpty(resolvedFileInfo.NuSpec?.ProjectUrl))
+ return null;
+ if (LicenseCache.TryGetValue(resolvedFileInfo.NuSpec.ProjectUrl, out string? value))
+ return value;
+ if (!Uri.TryCreate(resolvedFileInfo.NuSpec.ProjectUrl, UriKind.Absolute, out var uri))
+ return null;
+ var license = useFinalUrl
+ ? await ResolveFromFinalUrlAsync(uri, projectUriLicenseResolvers)
+ : await ResolveFromUrlAsync(uri, projectUriLicenseResolvers);
+ if (license != null)
+ {
+ LicenseCache[resolvedFileInfo.NuSpec.Id] = license;
+ LicenseCache[resolvedFileInfo.NuSpec.ProjectUrl] = license;
+ }
+ return license;
+ }
+
+ private async Task ResolveFromUrlAsync(Uri uri, IEnumerable urlLicenseResolvers)
+ {
+ foreach (var resolver in urlLicenseResolvers)
+ {
+ if (!resolver.CanResolve(uri))
+ continue;
+ var license = await resolver.Resolve(uri);
+ if (license != null)
+ return license;
+ }
+ return null;
+ }
+
+ private async Task ResolveFromFinalUrlAsync(Uri uri, IEnumerable urlLicenseResolvers)
+ {
+ using var httpClient = httpClientFactory.CreateClient();
+ var httpResponseMessage = await httpClient.GetAsync(uri);
+ if (!httpResponseMessage.IsSuccessStatusCode && uri.AbsolutePath.EndsWith(".txt"))
+ {
+ // try without .txt extension
+ var fixedUri = new UriBuilder(uri);
+ fixedUri.Path = fixedUri.Path.Remove(fixedUri.Path.Length - 4);
+ httpResponseMessage = await httpClient.GetAsync(fixedUri.Uri);
+ if (!httpResponseMessage.IsSuccessStatusCode)
+ return null;
+ }
+ if (httpResponseMessage.RequestMessage != null && httpResponseMessage.RequestMessage.RequestUri != null && httpResponseMessage.RequestMessage.RequestUri != uri)
+ {
+ foreach (var resolver in urlLicenseResolvers)
+ {
+ if (!resolver.CanResolve(httpResponseMessage.RequestMessage.RequestUri))
+ continue;
+ var license = await resolver.Resolve(httpResponseMessage.RequestMessage.RequestUri);
+ if (license != null)
+ return license;
+ }
+ }
+ // 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;
+ return await httpResponseMessage.Content.ReadAsStringAsync();
+ }
+
+ private async Task ResolveFromRepositoryUrlAsync(ResolvedFileInfo resolvedFileInfo, bool useFinalUrl)
+ {
+ if (string.IsNullOrEmpty(resolvedFileInfo.NuSpec?.RepositoryUrl))
+ return null;
+ if (LicenseCache.TryGetValue(resolvedFileInfo.NuSpec.RepositoryUrl, out string? value))
+ return value;
+ if (!Uri.TryCreate(resolvedFileInfo.NuSpec.RepositoryUrl, UriKind.Absolute, out var uri))
+ return null;
+ var license = useFinalUrl
+ ? await ResolveFromFinalUrlAsync(uri, repositoryUriLicenseResolvers)
+ : await ResolveFromUrlAsync(uri, repositoryUriLicenseResolvers);
+ if (license != null)
+ {
+ LicenseCache[resolvedFileInfo.NuSpec.Id] = license;
+ LicenseCache[resolvedFileInfo.NuSpec.RepositoryUrl] = license;
+ }
+ return license;
+ }
+
+ private async Task ResolveFromLicenseUrlAsync(ResolvedFileInfo resolvedFileInfo, bool useFinalUrl)
+ {
+ if (string.IsNullOrEmpty(resolvedFileInfo.NuSpec?.LicenseUrl))
+ return null;
+ if (LicenseCache.TryGetValue(resolvedFileInfo.NuSpec.LicenseUrl, out string? value))
+ return value;
+ if (!Uri.TryCreate(resolvedFileInfo.NuSpec.LicenseUrl, UriKind.Absolute, out var uri))
+ return null;
+ var license = useFinalUrl
+ ? await ResolveFromFinalUrlAsync(uri, licenseUriLicenseResolvers)
+ : await ResolveFromUrlAsync(uri, licenseUriLicenseResolvers);
+ if (license != null)
+ {
+ LicenseCache[resolvedFileInfo.NuSpec.Id] = license;
+ LicenseCache[resolvedFileInfo.NuSpec.LicenseUrl] = license;
+ }
+ return license;
+ }
+}
diff --git a/src/DotnetThirdPartyNotices/Services/LocalPackageService.cs b/src/DotnetThirdPartyNotices/Services/LocalPackageService.cs
new file mode 100644
index 0000000..2009fd6
--- /dev/null
+++ b/src/DotnetThirdPartyNotices/Services/LocalPackageService.cs
@@ -0,0 +1,108 @@
+using DotnetThirdPartyNotices.Models;
+using System;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Xml.Linq;
+
+namespace DotnetThirdPartyNotices.Services;
+
+public partial class LocalPackageService : ILocalPackageService
+{
+ public NuSpec? GetNuSpecFromPackagePath(string? path)
+ {
+ if (path == null)
+ return null;
+ var nuspecPath = GetNuspecPathFromPackagePath(path);
+ if (nuspecPath != null)
+ return GetNuSpecFromNuspecPath(nuspecPath);
+ var nupkgPath = GetNupkgPathFromPackagePath(path);
+ if (nupkgPath != null)
+ return GetNuSpecFromNupkgPath(nupkgPath);
+ return null;
+ }
+ public NuSpec? GetNuSpecFromNupkgPath(string? path)
+ {
+ if (path == null)
+ return null;
+ using var zipToCreate = new FileStream(path, FileMode.Open, FileAccess.Read);
+ using var zip = new ZipArchive(zipToCreate, ZipArchiveMode.Read);
+ var zippedNuspec = zip.Entries.Single(e => e.FullName.EndsWith(".nuspec"));
+ using var stream = zippedNuspec.Open();
+ using var streamReader = new StreamReader(stream);
+ return GetNuSpecFromStreamReader(streamReader);
+ }
+
+ public NuSpec? GetNuSpecFromNuspecPath(string? path)
+ {
+ if (path == null)
+ return null;
+ using var streamReader = new StreamReader(path);
+ return GetNuSpecFromStreamReader(streamReader);
+ }
+
+ public NuSpec? GetNuSpecFromStreamReader(TextReader? textReader)
+ {
+ if (textReader == null)
+ return null;
+ var xDocument = XDocument.Load(textReader);
+ if (xDocument.Root == null) return null;
+ var ns = xDocument.Root.GetDefaultNamespace();
+ var metadata = xDocument.Root.Element(ns + "metadata");
+ if (metadata == null)
+ return null;
+ var id = metadata.Element(ns + "id")?.Value;
+ if (id == null)
+ return null;
+ return new NuSpec(id)
+ {
+ Version = metadata.Element(ns + "version")?.Value,
+ LicenseUrl = metadata.Element(ns + "licenseUrl")?.Value,
+ ProjectUrl = metadata.Element(ns + "projectUrl")?.Value,
+ RepositoryUrl = metadata.Element(ns + "repository")?.Attribute("url")?.Value,
+ LicenseRelativePath = metadata.Elements(ns + "license").Where(x => x.Attribute("type")?.Value == "file").FirstOrDefault()?.Value
+ };
+ }
+
+
+ public string? GetNuspecPathFromPackagePath(string? packagePath)
+ {
+ return packagePath != null
+ ? Directory.EnumerateFiles(packagePath, "*.nuspec", SearchOption.TopDirectoryOnly).FirstOrDefault()
+ : null;
+ }
+
+ public string? GetNupkgPathFromPackagePath(string? packagePath)
+ {
+ return packagePath != null
+ ? Directory.EnumerateFiles(packagePath, "*.nupkg", SearchOption.TopDirectoryOnly).FirstOrDefault()
+ : null;
+ }
+
+ public string? GetPackagePathFromAssemblyPath(string? assemblyPath)
+ {
+ if(assemblyPath == null)
+ return null;
+ var directoryParts = Path.GetDirectoryName(assemblyPath)?.Split('\\', StringSplitOptions.RemoveEmptyEntries);
+ if(directoryParts == null)
+ return null;
+ // packages\{packageName}\{version}\lib\{targetFramework}\{packageName}.dll
+ // packages\{packageName}\{version}\runtimes\{runtime-identifier}\lib\{targetFramework}\{packageName}.dll
+ // packages\{packageName}\{version}\lib\{targetFramework}\{culture}\{packageName}.dll
+ var index = Array.FindLastIndex(directoryParts, x => NewNugetVersionRegex().IsMatch(x));
+ if (index > -1)
+ return string.Join('\\', directoryParts.Take(index + 1));
+ // packages\{packageName}.{version}\lib\{targetFramework}\{packageName}.dll
+ index = Array.FindLastIndex(directoryParts, x => OldNugetVersionRegex().IsMatch(x));
+ if (index > -1)
+ return string.Join('\\', directoryParts.Take(index + 1));
+ return null;
+ }
+
+ [GeneratedRegex(@"^\d+.\d+.\d+\S*$", RegexOptions.None)]
+ private static partial Regex NewNugetVersionRegex();
+
+ [GeneratedRegex(@"^\S+\.\d+.\d+.\d+\S*$")]
+ private static partial Regex OldNugetVersionRegex();
+}
\ No newline at end of file
diff --git a/src/DotnetThirdPartyNotices/LicenseResolvers/NetFrameworkLicenseResolver.cs b/src/DotnetThirdPartyNotices/Services/NetFrameworkLicenseResolver.cs
similarity index 67%
rename from src/DotnetThirdPartyNotices/LicenseResolvers/NetFrameworkLicenseResolver.cs
rename to src/DotnetThirdPartyNotices/Services/NetFrameworkLicenseResolver.cs
index f61ee5f..f5ad7d3 100644
--- a/src/DotnetThirdPartyNotices/LicenseResolvers/NetFrameworkLicenseResolver.cs
+++ b/src/DotnetThirdPartyNotices/Services/NetFrameworkLicenseResolver.cs
@@ -3,27 +3,27 @@
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
-using DotnetThirdPartyNotices.LicenseResolvers.Interfaces;
-namespace DotnetThirdPartyNotices.LicenseResolvers;
+namespace DotnetThirdPartyNotices.Services;
internal class NetFrameworkLicenseResolver : ILicenseUriLicenseResolver, IFileVersionInfoLicenseResolver
{
- private static string _licenseContent;
+ 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 Resolve(Uri licenseUri) => GetLicenseContent();
- public Task Resolve(FileVersionInfo fileVersionInfo) => GetLicenseContent();
+ public Task Resolve(Uri licenseUri) => GetLicenseContent();
+ public Task Resolve(FileVersionInfo fileVersionInfo) => GetLicenseContent();
- public async Task GetLicenseContent()
+ public async Task GetLicenseContent()
{
if (_licenseContent != null) // small optimization: avoid getting the resource on every call
return _licenseContent;
-
var executingAssembly = Assembly.GetExecutingAssembly();
- await using var stream = executingAssembly.GetManifestResourceStream(typeof(Utils), "dotnet_library_license.txt");
+ await using var stream = executingAssembly.GetManifestResourceStream(typeof(Program), "dotnet_library_license.txt");
+ if (stream == null)
+ return null;
using var streamReader = new StreamReader(stream);
_licenseContent = await streamReader.ReadToEndAsync();
return _licenseContent;
diff --git a/src/DotnetThirdPartyNotices/Services/OpenSourceOrgLicenseResolver.cs b/src/DotnetThirdPartyNotices/Services/OpenSourceOrgLicenseResolver.cs
new file mode 100644
index 0000000..79683ba
--- /dev/null
+++ b/src/DotnetThirdPartyNotices/Services/OpenSourceOrgLicenseResolver.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Net.Http;
+using System.Text.Json;
+using System.Threading.Tasks;
+
+namespace DotnetThirdPartyNotices.Services;
+
+internal class OpenSourceOrgLicenseResolver : ILicenseUriLicenseResolver
+{
+ public bool CanResolve(Uri licenseUri) => licenseUri.Host == "opensource.org";
+
+ internal static readonly char[] separator = ['/'];
+
+ public async Task Resolve(Uri licenseUri)
+ {
+ var s = licenseUri.AbsolutePath.Split(separator, StringSplitOptions.RemoveEmptyEntries);
+ if (s[0] != "licenses")
+ return null;
+ var licenseId = s[1];
+ 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 (!httpResponseMessage.IsSuccessStatusCode)
+ return null;
+ var content = await httpResponseMessage.Content.ReadAsStringAsync();
+ var jsonDocument = JsonDocument.Parse(content);
+ return jsonDocument.RootElement.GetProperty("body").GetString();
+ }
+}
\ No newline at end of file
diff --git a/src/DotnetThirdPartyNotices/Services/ProjectService.cs b/src/DotnetThirdPartyNotices/Services/ProjectService.cs
new file mode 100644
index 0000000..477dc55
--- /dev/null
+++ b/src/DotnetThirdPartyNotices/Services/ProjectService.cs
@@ -0,0 +1,116 @@
+using DotnetThirdPartyNotices.Models;
+using Microsoft.Build.Construction;
+using Microsoft.Build.Evaluation;
+using Microsoft.Build.Execution;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Logging;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+
+namespace DotnetThirdPartyNotices.Services;
+
+internal class ProjectService(ILogger logger, ILocalPackageService localPackageService) : IProjectService
+{
+ public IEnumerable ResolveFiles(Project project)
+ {
+ var targetFrameworksProperty = project.GetProperty("TargetFrameworks");
+ if (targetFrameworksProperty != null)
+ {
+ var targetFrameworks = targetFrameworksProperty.EvaluatedValue.Split(';');
+ project.SetProperty("TargetFramework", targetFrameworks[0]);
+ }
+ var projectInstance = project.CreateProjectInstance();
+ var targetFrameworkIdentifier = projectInstance.GetPropertyValue("TargetFrameworkIdentifier");
+ return targetFrameworkIdentifier switch
+ {
+ TargetFrameworkIdentifiers.NetCore or TargetFrameworkIdentifiers.NetStandard => ResolveFilesUsingComputeFilesToPublish(projectInstance),
+ TargetFrameworkIdentifiers.NetFramework => ResolveFilesUsingResolveAssemblyReferences(projectInstance),
+ _ => throw new InvalidOperationException("Unsupported target framework."),
+ };
+ }
+
+ private IEnumerable ResolveFilesUsingResolveAssemblyReferences(ProjectInstance projectInstance)
+ {
+ var resolvedFileInfos = new List();
+
+ if (!projectInstance.Build("ResolveAssemblyReferences", [new ConsoleLogger(LoggerVerbosity.Minimal)]))
+ return [];
+
+ foreach (var item in projectInstance.GetItems("ReferenceCopyLocalPaths"))
+ {
+ var assemblyPath = item.EvaluatedInclude;
+ var versionInfo = FileVersionInfo.GetVersionInfo(assemblyPath);
+ var packagePath = localPackageService.GetPackagePathFromAssemblyPath(assemblyPath);
+ var resolvedFileInfo = new ResolvedFileInfo
+ {
+ SourcePath = assemblyPath,
+ VersionInfo = versionInfo,
+ RelativeOutputPath = Path.GetFileName(assemblyPath),
+ PackagePath = packagePath,
+ NuSpec = item.GetMetadataValue("ResolvedFrom") == "{HintPathFromItem}" && item.GetMetadataValue("HintPath").StartsWith("..\\packages")
+ ? (localPackageService.GetNuSpecFromPackagePath(packagePath) ?? throw new ApplicationException($"Cannot find package path from assembly path ({assemblyPath})"))
+ : null
+ };
+ resolvedFileInfos.Add(resolvedFileInfo);
+ }
+
+ return resolvedFileInfos;
+ }
+
+ private IEnumerable ResolveFilesUsingComputeFilesToPublish(ProjectInstance projectInstance)
+ {
+ var resolvedFileInfos = new List();
+
+ projectInstance.Build("ComputeFilesToPublish", [new ConsoleLogger(LoggerVerbosity.Minimal)]);
+
+ foreach (var item in projectInstance.GetItems("ResolvedFileToPublish"))
+ {
+ var assemblyPath = item.EvaluatedInclude;
+
+ var packageName = item.GetMetadataValue(item.HasMetadata("PackageName") ? "PackageName" : "NugetPackageId");
+ var packageVersion = item.GetMetadataValue(item.HasMetadata("PackageName") ? "PackageVersion" : "NugetPackageVersion");
+ if (packageName == string.Empty || packageVersion == string.Empty)
+ {
+ // Skip if it's not a NuGet package
+ continue;
+ }
+ var relativePath = item.GetMetadataValue("RelativePath");
+ var packagePath = localPackageService.GetPackagePathFromAssemblyPath(assemblyPath);
+ var nuSpec = localPackageService.GetNuSpecFromPackagePath(packagePath) ?? throw new ApplicationException($"Cannot find package path from assembly path ({assemblyPath})");
+ var resolvedFileInfo = new ResolvedFileInfo
+ {
+ SourcePath = assemblyPath,
+ VersionInfo = FileVersionInfo.GetVersionInfo(assemblyPath),
+ NuSpec = nuSpec,
+ RelativeOutputPath = relativePath,
+ PackagePath = packagePath
+ };
+
+ resolvedFileInfos.Add(resolvedFileInfo);
+ }
+
+ return resolvedFileInfos;
+ }
+
+ public string[] GetProjectFilePaths(string directoryPath)
+ {
+ logger.LogInformation("Scan directory {ScanDirectory}", directoryPath);
+ var paths = Directory.EnumerateFiles(directoryPath, "*.*", SearchOption.TopDirectoryOnly)
+ .Where(s => s.EndsWith(".csproj") || s.EndsWith(".fsproj"))
+ .ToArray();
+ if (paths.Length > 0)
+ return paths;
+ var solutionFilePath = Directory.EnumerateFiles(directoryPath, "*.sln", SearchOption.TopDirectoryOnly)
+ .SingleOrDefault();
+ if (solutionFilePath == null)
+ return paths;
+ return SolutionFile.Parse(solutionFilePath).ProjectsInOrder
+ .Select(x => x.AbsolutePath)
+ .Where(x => x.EndsWith(".csproj") || x.EndsWith(".fsproj"))
+ .ToArray();
+ }
+}
diff --git a/src/DotnetThirdPartyNotices/Utils.cs b/src/DotnetThirdPartyNotices/Utils.cs
deleted file mode 100644
index ff18dfb..0000000
--- a/src/DotnetThirdPartyNotices/Utils.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-using System;
-using System.IO;
-using System.Linq;
-using System.Text.RegularExpressions;
-
-namespace DotnetThirdPartyNotices;
-
-internal static partial class Utils
-{
- public static string GetNuspecPath( string assemblyPath )
- {
- var package = GetPackagePath( assemblyPath );
- return package != null
- ? Directory.EnumerateFiles( package, "*.nuspec", SearchOption.TopDirectoryOnly ).FirstOrDefault()
- : null;
- }
-
- public static string GetNupkgPath( string assemblyPath )
- {
- var package = GetPackagePath( assemblyPath );
- return package != null
- ? Directory.EnumerateFiles( package, "*.nupkg", SearchOption.TopDirectoryOnly ).FirstOrDefault()
- : null;
- }
-
- public static string GetPackagePath( string assemblyPath )
- {
- var directoryParts = Path.GetDirectoryName( assemblyPath ).Split( '\\', StringSplitOptions.RemoveEmptyEntries );
- // packages\{packageName}\{version}\lib\{targetFramework}\{packageName}.dll
- // packages\{packageName}\{version}\runtimes\{runtime-identifier}\lib\{targetFramework}\{packageName}.dll
- // packages\{packageName}\{version}\lib\{targetFramework}\{culture}\{packageName}.dll
- var index = Array.FindLastIndex( directoryParts, x => NewNugetVersionRegex().IsMatch(x));
- if (index > -1)
- return string.Join('\\', directoryParts.Take(index + 1) );
- // packages\{packageName}.{version}\lib\{targetFramework}\{packageName}.dll
- index = Array.FindLastIndex(directoryParts, x => OldNugetVersionRegex().IsMatch(x));
- if (index > -1)
- return string.Join('\\', directoryParts.Take(index + 1));
- return null;
- }
-
- [GeneratedRegex( @"^\d+.\d+.\d+\S*$", RegexOptions.None )]
- private static partial Regex NewNugetVersionRegex();
-
- [GeneratedRegex( @"^\S+\.\d+.\d+.\d+\S*$" )]
- private static partial Regex OldNugetVersionRegex();
-}
\ No newline at end of file