using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; using System.Text; using System.Text.Json; using System.Xml.Linq; using static Centron.Scripts.RunHelper; using static Centron.Scripts.CentronPaths; namespace Centron.Scripts; public static class DependencyGraph { public record Dependency(string Name, string Version, string ProjectUrl, string License); public static List ReadDependencyGraphs(params string[] projectFilePaths) { return projectFilePaths.SelectMany(ReadDependencyGraph).Distinct().ToList(); } public static List ReadDependencyGraph(string projectFilePath) { var projectAssetsJsonFilePath = Path.Combine(Path.GetDirectoryName(projectFilePath), "obj", "project.assets.json"); var projectAssetsJsonContent = File.ReadAllText(projectAssetsJsonFilePath); var filter = new Func[] { IsMicrosoftPackage, IsCentronPackage, IsRiverbirdPackage, IsRuntimePackage, IsLanguagePackage, IsNetStandardLibraryPackage, }; var dependencies = JsonDocument.Parse(projectAssetsJsonContent) .RootElement.GetProperty("targets") .EnumerateObject() .SelectMany(f => f.Value.EnumerateObject() .Select(f => f.Name.Split('/')) .Select(f => new Dependency(f[0], f[1], null, null))) .Where(f => filter.All(d => d(f) is false)) .Select(f => LoadAdditionalInfo(f)) .Distinct() // We might have duplicates if multiple targets are found in the project.assets.json file, so remove them .ToList(); return dependencies; } private static readonly HttpClient _httpClient = new(); private static Dependency LoadAdditionalInfo(Dependency dependency) { if (dependency.Name.StartsWith("DevExpress")) return dependency with { ProjectUrl = "https://www.devexpress.com/", License = "DevExpress" }; if (dependency.Name.StartsWith("FastReport")) return dependency with { ProjectUrl = "https://www.fast-report.com/", License = "FastReport" }; try { // See: https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#download-package-manifest-nuspec using var request = new HttpRequestMessage(HttpMethod.Get, $"https://api.nuget.org/v3-flatcontainer/{dependency.Name.ToLower()}/{dependency.Version}/{dependency.Name.ToLower()}.nuspec"); using var response = _httpClient.Send(request); using var responseStream = response.Content.ReadAsStream(); using var reader = new StreamReader(responseStream); var nuspecXml = reader.ReadToEnd(); var xmlDocument = XDocument.Parse(nuspecXml); var xmlNamespace = "{" + xmlDocument.Root.GetDefaultNamespace() + "}"; var metadataElement = xmlDocument.Root.Element($"{xmlNamespace}metadata"); var license = metadataElement.Element($"{xmlNamespace}license")?.Value; var licenseUrl = metadataElement.Element($"{xmlNamespace}licenseUrl")?.Value; license = TryGuessLicense(dependency, license, licenseUrl) ?? license; var projectUrl = metadataElement.Element($"{xmlNamespace}projectUrl")?.Value; var sourceCodeUrl = metadataElement.Element($"{xmlNamespace}repository")?.Attribute("url")?.Value; var url = string.IsNullOrWhiteSpace(projectUrl) is false ? projectUrl : sourceCodeUrl; return dependency with { ProjectUrl = url, License = license }; } catch { // Something went wrong, but we don't want the build to fail because of that return dependency; } } private static string TryGuessLicense(Dependency dependency, string license, string licenseUrl) { if (dependency is { Name: "Antlr3.Runtime", Version: "3.5.1" } && licenseUrl is "https://raw.githubusercontent.com/antlr/antlrcs/master/LICENSE.txt") return "BSD-3-Clause"; if (dependency is { Name: "AutoMapper", Version: "8.1.1" } && licenseUrl is "https://github.com/AutoMapper/AutoMapper/blob/master/LICENSE.txt") return "MIT"; if (dependency is { Name: "Castle.Core", Version: "4.4.1" } && licenseUrl is "http://www.apache.org/licenses/LICENSE-2.0.html") return "Apache-2.0"; if (dependency is { Name: "FluentNHibernate", Version: "3.2.1" } && licenseUrl is "https://aka.ms/deprecateLicenseUrl") return "BSD-3-Clause"; if (dependency is { Name: "Iesi.Collections", Version: "4.0.4" } && licenseUrl is null) return "Public Domain"; if (dependency is { Name: "libfintx", Version: "1.1.0" } && licenseUrl is "https://opensource.org/license/LGPL-3.0") return "LGPL-3.0"; if (dependency is { Name: "Remotion.Linq", Version: "2.2.0" } && licenseUrl is "http://opensource.org/licenses/Apache-2.0") return "Apache-2.0"; if (dependency is { Name: "Remotion.Linq.EagerFetching", Version: "2.2.0" } && licenseUrl is "http://opensource.org/licenses/LGPL-2.1") return "LGPL-2.1"; if (dependency is { Name: "SSH.NET", Version: "2020.0.2" } && licenseUrl is "https://github.com/sshnet/SSH.NET/blob/master/LICENSE") return "MIT"; if (dependency is { Name: "SshNet.Security.Cryptography", Version: "1.3.0" } && licenseUrl is "https://github.com/sshnet/Cryptography/blob/master/LICENSE") return "MIT"; if (dependency is { Name: "Castle.Windsor", Version: "5.0.1" } && licenseUrl is "http://www.apache.org/licenses/LICENSE-2.0.html") return "Apache-2.0"; if (dependency is { Name: "OpenMcdf", Version: "2.3.0" } && licenseUrl is "https://opensource.org/licenses/MPL-2.0") return "MPL-2.0"; if (dependency is { Name: "RtfPipe", Version: "2.0.7677.4303" } && licenseUrl is null) return "MIT"; if (dependency is { Name: "Simple-MAPI.NET", Version: "1.2.1" } && licenseUrl is "https://aka.ms/deprecateLicenseUrl") return "MIT"; return null; } private static bool IsMicrosoftPackage(Dependency package) { return package.Name.StartsWith("Microsoft") || package.Name.StartsWith("System"); } private static bool IsCentronPackage(Dependency package) { return package.Name.StartsWith("Centron"); } private static bool IsRiverbirdPackage(Dependency package) { return package.Name.StartsWith("Riverbird"); } private static bool IsRuntimePackage(Dependency package) { return package.Name.StartsWith("runtime"); } private static bool IsLanguagePackage(Dependency package) { return package.Name.EndsWith(".de"); } private static bool IsNetStandardLibraryPackage(Dependency package) { return package.Name == "NETStandard.Library"; } public static void Write(List dependencies, string filePath) { var content = new StringBuilder(); content.AppendLine($"{"Dependency",-50}{"Versionsnummer",-15}{"Lizenz",-15}{"URL",-100}"); foreach (var dependency in dependencies.OrderBy(f => f.Name).ThenBy(f => f.License)) { content.AppendLine($"{dependency.Name,-50}{dependency.Version,-15}{dependency.License,-15}{dependency.ProjectUrl,-100}"); } if (!Directory.Exists(Path.GetDirectoryName(filePath))) Directory.CreateDirectory(Path.GetDirectoryName(filePath)); File.WriteAllText(filePath, content.ToString()); } }