|
| 1 | +using System.Collections; |
| 2 | +using System.IO.Compression; |
| 3 | +using System.Reflection; |
| 4 | +using System.Runtime.Loader; |
| 5 | +using System.Text.RegularExpressions; |
| 6 | + |
| 7 | +namespace mdresgen; |
| 8 | + |
| 9 | +internal static partial class IconDiff |
| 10 | +{ |
| 11 | + [GeneratedRegex(@"MaterialDesignThemes\.(?<Version>\d+\.\d+\.\d+)\.nupkg")] |
| 12 | + private static partial Regex FileNameRegex(); |
| 13 | + |
| 14 | + public static async Task RunAsync() |
| 15 | + { |
| 16 | + var nugetDir = Path.Combine(PathHelper.RepositoryRoot, "nugets"); |
| 17 | + var nugets =( |
| 18 | + from file in Directory.EnumerateFiles(nugetDir) |
| 19 | + let match = FileNameRegex().Match(Path.GetFileName(file)) |
| 20 | + where match.Success |
| 21 | + let version = Version.Parse(match.Groups["Version"].Value) |
| 22 | + orderby version |
| 23 | + select (File: new FileInfo(file), Version: version)).ToList(); |
| 24 | + |
| 25 | + var oldNuget = nugets.First(); |
| 26 | + var newNuget = nugets.Last(); |
| 27 | + |
| 28 | + string output = await CompareNuGets(oldNuget.File, newNuget.File); |
| 29 | + |
| 30 | + await File.WriteAllTextAsync(Path.Combine(PathHelper.RepositoryRoot, $"IconChanges-{GetVersionString(oldNuget.Version)}--{GetVersionString(newNuget.Version)}.md"), output); |
| 31 | + |
| 32 | + static string GetVersionString(Version version) |
| 33 | + => $"{version.Major}.{version.Minor}.{version.Build}"; |
| 34 | + } |
| 35 | + |
| 36 | + private static async Task<string> CompareNuGets(FileInfo previousNuget, FileInfo currentNuget) |
| 37 | + { |
| 38 | + Console.WriteLine($"Comparing previous {previousNuget.Name} to {currentNuget.Name}"); |
| 39 | + var previousValues = await ProcessNuGet(previousNuget) ?? throw new InvalidOperationException($"Failed to find icons in previous NuGet {previousNuget.FullName}"); |
| 40 | + var newValues = await ProcessNuGet(currentNuget) ?? throw new InvalidOperationException($"Failed to find icons in current NuGet {currentNuget.FullName}"); |
| 41 | + |
| 42 | + var previousValuesByName = new Dictionary<string, int>(); |
| 43 | + foreach (var kvp in previousValues) |
| 44 | + { |
| 45 | + foreach (string aliases in kvp.Value.Aliases) |
| 46 | + { |
| 47 | + previousValuesByName[aliases] = kvp.Key; |
| 48 | + } |
| 49 | + } |
| 50 | + var newValuesByName = new Dictionary<string, int>(); |
| 51 | + foreach (var kvp in newValues) |
| 52 | + { |
| 53 | + foreach (string aliases in kvp.Value.Aliases) |
| 54 | + { |
| 55 | + newValuesByName[aliases] = kvp.Key; |
| 56 | + } |
| 57 | + } |
| 58 | + |
| 59 | + var newItems = newValuesByName.Keys.Except(previousValuesByName.Keys) |
| 60 | + .OrderBy(x => x) |
| 61 | + .ToList(); |
| 62 | + |
| 63 | + var removedItems = previousValuesByName.Keys.Except(newValuesByName.Keys) |
| 64 | + .OrderBy(x => x) |
| 65 | + .ToList(); |
| 66 | + |
| 67 | + var visuallyChanged = newValuesByName.Keys.Intersect(previousValuesByName.Keys) |
| 68 | + .Where(key => newValues[newValuesByName[key]].Path != previousValues[previousValuesByName[key]].Path) |
| 69 | + .OrderBy(x => x) |
| 70 | + .ToList(); |
| 71 | + |
| 72 | + StringBuilder output = new(); |
| 73 | + output.AppendLine("## Pack Icon Changes"); |
| 74 | + WriteIconChanges("New icons", newItems, newValuesByName); |
| 75 | + |
| 76 | + WriteIconChanges("Icons with visual changes", visuallyChanged, newValuesByName); |
| 77 | + |
| 78 | + WriteIconChanges("Removed icons", removedItems, previousValuesByName); |
| 79 | + |
| 80 | + return output.ToString(); |
| 81 | + |
| 82 | + void WriteIconChanges(string header, List<string> icons, Dictionary<string, int> iconsByName) |
| 83 | + { |
| 84 | + Console.WriteLine($"{header} => {icons.Count}"); |
| 85 | + output.AppendLine($"### {header} ({icons.Count})"); |
| 86 | + if (icons.Any()) |
| 87 | + { |
| 88 | + foreach (var iconGroup in icons.GroupBy(name => iconsByName[name])) |
| 89 | + { |
| 90 | + output.AppendLine($"- {string.Join(", ", iconGroup)}"); |
| 91 | + } |
| 92 | + } |
| 93 | + else |
| 94 | + { |
| 95 | + output.AppendLine("_None_"); |
| 96 | + } |
| 97 | + } |
| 98 | + } |
| 99 | + |
| 100 | + private static async Task<IReadOnlyDictionary<int, (HashSet<string> Aliases, string? Path)>?> ProcessNuGet(FileInfo nuget) |
| 101 | + { |
| 102 | + ZipArchive zipArchive = ZipFile.OpenRead(nuget.FullName); |
| 103 | + var entry = zipArchive.Entries.Where(x => x.FullName.EndsWith("MaterialDesignThemes.Wpf.dll")) |
| 104 | + .OrderByDescending(x => GetMajorTfmVersion(x.FullName)) |
| 105 | + .First(); |
| 106 | + |
| 107 | + using MemoryStream ms = new(); |
| 108 | + using (var entryStream = entry.Open()) |
| 109 | + { |
| 110 | + await entryStream.CopyToAsync(ms); |
| 111 | + } |
| 112 | + ms.Position = 0; |
| 113 | + return ProcessDll(ms); |
| 114 | + |
| 115 | + //This technically puts netcore before net framework, but since we are already at net7 and only care about latest |
| 116 | + //that does not matter. |
| 117 | + static int? GetMajorTfmVersion(string entryName) |
| 118 | + { |
| 119 | + if (!entryName.StartsWith("lib/net")) return null; |
| 120 | + string tfm = entryName[7..]; |
| 121 | + char c = tfm.FirstOrDefault(char.IsDigit); |
| 122 | + if (c != default) |
| 123 | + { |
| 124 | + return int.Parse($"{c}"); |
| 125 | + } |
| 126 | + return null; |
| 127 | + } |
| 128 | + } |
| 129 | + |
| 130 | + private static IReadOnlyDictionary<int, (HashSet<string> Aliases, string? Path)>? ProcessDll(Stream assemblyStream) |
| 131 | + { |
| 132 | + AssemblyLoadContext context = new AssemblyLoadContext(Guid.NewGuid().ToString(), true); |
| 133 | + |
| 134 | + Assembly assembly = context.LoadFromStream(assemblyStream); |
| 135 | + |
| 136 | + Type? packIconKind = assembly.GetType("MaterialDesignThemes.Wpf.PackIconKind"); |
| 137 | + Type? packIconDataFactory = assembly.GetType("MaterialDesignThemes.Wpf.PackIconDataFactory"); |
| 138 | + |
| 139 | + if (packIconKind is null) return null; |
| 140 | + if (packIconDataFactory is null) return null; |
| 141 | + |
| 142 | + var rv = new Dictionary<int, (HashSet<string>, string?)>(); |
| 143 | + |
| 144 | + MethodInfo? createMethod = packIconDataFactory.GetMethod("Create", BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Static); |
| 145 | + |
| 146 | + var pathDictionary = (IDictionary?)createMethod?.Invoke(null, Array.Empty<object?>()); |
| 147 | + |
| 148 | + if (pathDictionary is null) return null; |
| 149 | + |
| 150 | + foreach (string enumName in Enum.GetNames(packIconKind)) |
| 151 | + { |
| 152 | + object @enum = Enum.Parse(packIconKind, enumName); |
| 153 | + if (rv.TryGetValue((int)@enum, out var found)) |
| 154 | + { |
| 155 | + found.Item1.Add(enumName); |
| 156 | + continue; |
| 157 | + } |
| 158 | + |
| 159 | + string? path = (string?)pathDictionary[@enum]; |
| 160 | + rv[(int)@enum] = (new HashSet<string> { enumName }, path); |
| 161 | + } |
| 162 | + |
| 163 | + context.Unload(); |
| 164 | + |
| 165 | + return rv; |
| 166 | + } |
| 167 | +} |
0 commit comments