diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 94c7c1e..ad00392 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -8,14 +8,16 @@ jobs: name: Build, pack and publish runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-dotnet@v1 + - name: Checkout code + uses: actions/checkout@v3 + - name: Setup .NET + uses: actions/setup-dotnet@v3 with: - dotnet-version: '6.0.x' + dotnet-version: '8.0.x' - name: Publish on version change id: publish_nuget - uses: alirezanet/publish-nuget@v3.0.4 + uses: alirezanet/publish-nuget@v3.1.0 with: # Filepath of the project to be packaged, relative to root of repository PROJECT_FILE_PATH: src/DotnetThirdPartyNotices/DotnetThirdPartyNotices.csproj diff --git a/src/DotnetThirdPartyNotices/Directory.Packages.props b/src/DotnetThirdPartyNotices/Directory.Packages.props new file mode 100644 index 0000000..28afc6b --- /dev/null +++ b/src/DotnetThirdPartyNotices/Directory.Packages.props @@ -0,0 +1,30 @@ + + + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/DotnetThirdPartyNotices/DotnetThirdPartyNotices.csproj b/src/DotnetThirdPartyNotices/DotnetThirdPartyNotices.csproj index a73858e..28be4c2 100644 --- a/src/DotnetThirdPartyNotices/DotnetThirdPartyNotices.csproj +++ b/src/DotnetThirdPartyNotices/DotnetThirdPartyNotices.csproj @@ -1,14 +1,14 @@ - + Exe - net6.0 + net8.0 default true dotnet-thirdpartynotices DotnetThirdPartyNotices A .NET tool to generate file with third party legal notices - 0.2.7 + 0.2.8 MIT git https://github.com/bugproof/DotnetThirdPartyNotices @@ -22,14 +22,14 @@ - - - - - - - - + + + + + + + + diff --git a/src/DotnetThirdPartyNotices/Extensions/ProjectExtensions.cs b/src/DotnetThirdPartyNotices/Extensions/ProjectExtensions.cs index 39b795f..6a9e051 100644 --- a/src/DotnetThirdPartyNotices/Extensions/ProjectExtensions.cs +++ b/src/DotnetThirdPartyNotices/Extensions/ProjectExtensions.cs @@ -61,13 +61,7 @@ private static IEnumerable ResolveFilesUsingResolveAssemblyRef if (item.GetMetadataValue("ResolvedFrom") == "{HintPathFromItem}" && item.GetMetadataValue("HintPath").StartsWith("..\\packages")) { - var packagePath = Utils.GetPackagePathFromAssemblyPath(assemblyPath); - if (packagePath == null) - throw new ApplicationException($"Cannot find package path from assembly path ({assemblyPath})"); - - var nuPkgFileName = Directory.GetFiles(packagePath, "*.nupkg", SearchOption.TopDirectoryOnly).Single(); - - var nuSpec = NuSpec.FromNupkg(nuPkgFileName); + var nuSpec = NuSpec.FromAssemble(assemblyPath) ?? throw new ApplicationException( $"Cannot find package path from assembly path ({assemblyPath})" ); resolvedFileInfo.NuSpec = nuSpec; resolvedFileInfos.Add(resolvedFileInfo); } @@ -97,14 +91,7 @@ private static IEnumerable ResolveFilesUsingComputeFilesToPubl // Skip if it's not a NuGet package continue; } - - var packagePath = Utils.GetPackagePathFromAssemblyPath(assemblyPath); - if (packagePath == null) - throw new ApplicationException($"Cannot find package path from assembly path ({assemblyPath})"); - - // TODO: don't think this is reliable because I'm not sure if .nuspec will always be there, or if it will always be named tha way - var nuSpecFilePath = Path.Combine(packagePath, $"{packageName}.nuspec"); // Directory.GetFiles(packageFolder, "*.nuspec", SearchOption.TopDirectoryOnly).SingleOrDefault(); - var nuSpec = NuSpec.FromFile(nuSpecFilePath); + 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 diff --git a/src/DotnetThirdPartyNotices/Extensions/ResolvedFileInfoExtensions.cs b/src/DotnetThirdPartyNotices/Extensions/ResolvedFileInfoExtensions.cs index e1ad53f..998f7d4 100644 --- a/src/DotnetThirdPartyNotices/Extensions/ResolvedFileInfoExtensions.cs +++ b/src/DotnetThirdPartyNotices/Extensions/ResolvedFileInfoExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using System.Reflection; using System.Threading.Tasks; @@ -62,15 +63,16 @@ public static async Task ResolveLicense(this ResolvedFileInfo resolvedFi if (resolvedFileInfo == null) throw new ArgumentNullException(nameof(resolvedFileInfo)); string license = null; if (resolvedFileInfo.NuSpec != null) - license = await ResolveLicense(resolvedFileInfo.NuSpec); + license = await ResolveLicenseFromNuspec(resolvedFileInfo); - return license ?? await ResolveLicenseFromFileVersionInfo(resolvedFileInfo.VersionInfo); + return license ?? await ResolveLicense(resolvedFileInfo.VersionInfo); } private static readonly Dictionary LicenseCache = new(); - private static async Task ResolveLicense(NuSpec nuSpec) + private static async Task ResolveLicenseFromNuspec( ResolvedFileInfo resolvedFileInfo ) { + var nuSpec = resolvedFileInfo.NuSpec; if (LicenseCache.ContainsKey(nuSpec.Id)) return LicenseCache[nuSpec.Id]; @@ -78,11 +80,24 @@ private static async Task ResolveLicense(NuSpec nuSpec) 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(licenseUrl)) + if (!string.IsNullOrEmpty(nuSpec.LicenseUrl)) { - if (LicenseCache.ContainsKey(licenseUrl)) - return LicenseCache[licenseUrl]; + if (LicenseCache.TryGetValue(licenseUrl, out string value)) + return value; var license = await ResolveLicenseFromLicenseUri(new Uri(nuSpec.LicenseUrl)); if (license != null) @@ -96,8 +111,8 @@ private static async Task ResolveLicense(NuSpec nuSpec) // Try to get the license from repository url if (!string.IsNullOrEmpty(repositoryUrl)) { - if (LicenseCache.ContainsKey(repositoryUrl )) - return LicenseCache[repositoryUrl]; + if (LicenseCache.TryGetValue(repositoryUrl, out string value)) + return value; var license = await ResolveLicenseFromRepositoryUri(new Uri(repositoryUrl)); if (license != null) { @@ -108,17 +123,32 @@ private static async Task ResolveLicense(NuSpec nuSpec) } // Otherwise try to get the license from project url - if (string.IsNullOrEmpty(projectUrl)) return null; + if (string.IsNullOrEmpty(projectUrl)) + { + if (LicenseCache.TryGetValue(projectUrl, out string value)) + return value; - if (LicenseCache.ContainsKey(projectUrl)) - return LicenseCache[projectUrl]; + var license2 = await ResolveLicenseFromProjectUri(new Uri(projectUrl)); + if (license2 != null) + { + LicenseCache[nuSpec.Id] = license2; + LicenseCache[nuSpec.ProjectUrl] = license2; + return license2; + } + } - var license2 = await ResolveLicenseFromProjectUri(new Uri(projectUrl)); - if (license2 == null) return null; + return null; + } - LicenseCache[nuSpec.Id] = license2; - LicenseCache[nuSpec.ProjectUrl] = license2; - return license2; + 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) @@ -149,6 +179,15 @@ private static async Task ResolveLicenseFromRepositoryUri(Uri repository 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)) diff --git a/src/DotnetThirdPartyNotices/GithubService.cs b/src/DotnetThirdPartyNotices/GithubService.cs index 80b7a1a..6f82241 100644 --- a/src/DotnetThirdPartyNotices/GithubService.cs +++ b/src/DotnetThirdPartyNotices/GithubService.cs @@ -34,7 +34,10 @@ public async Task GetLicenseContentFromRepositoryPath(string repositoryP repositoryPath = repositoryPath.TrimEnd('/'); if (repositoryPath.EndsWith(".git")) repositoryPath = repositoryPath[..^4]; - var json = await _httpClient.GetStringAsync($"repos{repositoryPath}/license"); + 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; diff --git a/src/DotnetThirdPartyNotices/LicenseResolvers/LocalPackageLicenseResolver.cs b/src/DotnetThirdPartyNotices/LicenseResolvers/LocalPackageLicenseResolver.cs index cca802c..eaf96f3 100644 --- a/src/DotnetThirdPartyNotices/LicenseResolvers/LocalPackageLicenseResolver.cs +++ b/src/DotnetThirdPartyNotices/LicenseResolvers/LocalPackageLicenseResolver.cs @@ -11,22 +11,23 @@ namespace DotnetThirdPartyNotices.LicenseResolvers; internal class LocalPackageLicenseResolver : IFileVersionInfoLicenseResolver { - public bool CanResolve( FileVersionInfo fileVersionInfo ) => true; + public bool CanResolve( FileVersionInfo fileVersionInfo ) => GetLicensePath(fileVersionInfo) != null; - public async Task Resolve( FileVersionInfo fileVersionInfo ) + public async Task Resolve(FileVersionInfo fileVersionInfo) { - var packageName = Path.GetFileNameWithoutExtension(fileVersionInfo.FileName); - var directoryParts = Path.GetDirectoryName(fileVersionInfo.FileName ).Split('\\', StringSplitOptions.RemoveEmptyEntries); - for ( var i = 0; i < directoryParts.Length; i++ ) + 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 { - var directoryPath = string.Join('\\', directoryParts.SkipLast(i)); - var licensePath = Directory.EnumerateFiles(directoryPath, "license.txt", SearchOption.TopDirectoryOnly) - .FirstOrDefault(); - if (licensePath != null) - return await File.ReadAllTextAsync(licensePath); - if (directoryPath.EndsWith($"\\{packageName}", StringComparison.OrdinalIgnoreCase)) - break; - } - return null; + MatchCasing = MatchCasing.CaseInsensitive, + RecurseSubdirectories = false + } ).FirstOrDefault( x => x.EndsWith( "\\license.txt", StringComparison.OrdinalIgnoreCase ) || x.EndsWith( "\\license.md", StringComparison.OrdinalIgnoreCase ) ); } } diff --git a/src/DotnetThirdPartyNotices/NuGetVersion.cs b/src/DotnetThirdPartyNotices/NuGetVersion.cs deleted file mode 100644 index 36b4ab9..0000000 --- a/src/DotnetThirdPartyNotices/NuGetVersion.cs +++ /dev/null @@ -1,122 +0,0 @@ -using System; -using System.Linq; - -namespace DotnetThirdPartyNotices; - -// based on https://github.com/NuGetArchive/NuGet.Versioning/blob/0f25e04c3a33d2dff11cbb97e1c0827cf5bf6da6/src/NuGet.Versioning/NuGetVersionFactory.cs -internal static class NuGetVersion -{ - public static bool IsValid(string value) - { - if (value == null) return false; - - // trim the value before passing it in since we not strict here - var sections = ParseSections(value.Trim()); - - // null indicates the string did not meet the rules - if (sections == null || string.IsNullOrEmpty(sections.Item1)) return false; - var versionPart = sections.Item1; - - if (versionPart.IndexOf('.') < 0) - { - // System.Version requires at least a 2 part version to parse. - versionPart += ".0"; - } - - if (!Version.TryParse(versionPart, out _)) return false; - // labels - if (sections.Item2 != null && !sections.Item2.All(s => IsValidPart(s, false))) - { - return false; - } - - return sections.Item3 == null || IsValid(sections.Item3, true); - } - - internal static bool IsLetterOrDigitOrDash(char c) - { - int x = c; - - // "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-" - return (x >= 48 && x <= 57) || (x >= 65 && x <= 90) || (x >= 97 && x <= 122) || x == 45; - } - - internal static bool IsValid(string s, bool allowLeadingZeros) - { - return s.Split('.').All(p => IsValidPart(p, allowLeadingZeros)); - } - - internal static bool IsValidPart(string s, bool allowLeadingZeros) - { - return IsValidPart(s.ToCharArray(), allowLeadingZeros); - } - - internal static bool IsValidPart(char[] chars, bool allowLeadingZeros) - { - var result = chars.Length != 0; - - // 0 is fine, but 00 is not. - // 0A counts as an alpha numeric string where zeros are not counted - if (!allowLeadingZeros && chars.Length > 1 && chars[0] == '0' && chars.All(char.IsDigit)) - { - // no leading zeros in labels allowed - result = false; - } - else - { - result &= chars.All(IsLetterOrDigitOrDash); - } - - return result; - } - - internal static Tuple ParseSections(string value) - { - string versionString = null; - string[] releaseLabels = null; - string buildMetadata = null; - - var dashPos = -1; - var plusPos = -1; - - var chars = value.ToCharArray(); - - for (var i = 0; i < chars.Length; i++) - { - var end = (i == chars.Length - 1); - - if (dashPos < 0) - { - if (!end && chars[i] != '-' && chars[i] != '+') continue; - var endPos = i + (end ? 1 : 0); - versionString = value.Substring(0, endPos); - - dashPos = i; - - if (chars[i] == '+') - { - plusPos = i; - } - } - else if (plusPos < 0) - { - if (!end && chars[i] != '+') continue; - var start = dashPos + 1; - var endPos = i + (end ? 1 : 0); - var releaseLabel = value.Substring(start, endPos - start); - - releaseLabels = releaseLabel.Split('.'); - - plusPos = i; - } - else if (end) - { - var start = plusPos + 1; - var endPos = i + (end ? 1 : 0); - buildMetadata = value.Substring(start, endPos - start); - } - } - - return new Tuple(versionString, releaseLabels, buildMetadata); - } -} \ No newline at end of file diff --git a/src/DotnetThirdPartyNotices/NuSpec.cs b/src/DotnetThirdPartyNotices/NuSpec.cs index 93dfe4d..9d515e4 100644 --- a/src/DotnetThirdPartyNotices/NuSpec.cs +++ b/src/DotnetThirdPartyNotices/NuSpec.cs @@ -14,6 +14,7 @@ public record NuSpec 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) { @@ -31,20 +32,21 @@ private static NuSpec FromTextReader(TextReader streamReader) 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 + 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) { - if (fileName == null) throw new ArgumentNullException(nameof(fileName)); + ArgumentNullException.ThrowIfNull( fileName ); using var xmlReader = new StreamReader(fileName); return FromTextReader(xmlReader); } public static NuSpec FromNupkg(string fileName) { - if (fileName == null) throw new ArgumentNullException(nameof(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")); @@ -52,4 +54,16 @@ public static NuSpec FromNupkg(string fileName) 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/Utils.cs b/src/DotnetThirdPartyNotices/Utils.cs index b3bf728..ff18dfb 100644 --- a/src/DotnetThirdPartyNotices/Utils.cs +++ b/src/DotnetThirdPartyNotices/Utils.cs @@ -1,18 +1,47 @@ -using System.IO; +using System; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; namespace DotnetThirdPartyNotices; -internal static class Utils +internal static partial class Utils { - public static string GetPackagePathFromAssemblyPath(string assemblyPath) + public static string GetNuspecPath( string assemblyPath ) { - var parentDirectoryInfo = Directory.GetParent(assemblyPath); - var isValid = false; - while (parentDirectoryInfo != null && !(isValid = NuGetVersion.IsValid(parentDirectoryInfo.Name))) - { - parentDirectoryInfo = parentDirectoryInfo.Parent; - } + var package = GetPackagePath( assemblyPath ); + return package != null + ? Directory.EnumerateFiles( package, "*.nuspec", SearchOption.TopDirectoryOnly ).FirstOrDefault() + : null; + } - return isValid ? parentDirectoryInfo.FullName : 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