ApiDiffValidation.cs 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.IO.Compression;
  5. using System.Linq;
  6. using System.Net.Http;
  7. using System.Text.RegularExpressions;
  8. using System.Threading.Tasks;
  9. using Nuke.Common.Tooling;
  10. public static class ApiDiffValidation
  11. {
  12. private static readonly HttpClient s_httpClient = new();
  13. public static async Task ValidatePackage(
  14. Tool apiCompatTool, string packagePath, string baselineVersion,
  15. string suppressionFilesFolder, bool updateSuppressionFile)
  16. {
  17. if (baselineVersion is null)
  18. {
  19. throw new InvalidOperationException(
  20. "Build \"api-baseline\" parameter must be set when running Nuke CreatePackages");
  21. }
  22. if (!Directory.Exists(suppressionFilesFolder))
  23. {
  24. Directory.CreateDirectory(suppressionFilesFolder!);
  25. }
  26. await using (var baselineStream = await DownloadBaselinePackage(packagePath, baselineVersion))
  27. using (var target = new ZipArchive(File.Open(packagePath, FileMode.Open, FileAccess.Read), ZipArchiveMode.Read))
  28. using (var baseline = new ZipArchive(baselineStream, ZipArchiveMode.Read))
  29. using (Helpers.UseTempDir(out var tempFolder))
  30. {
  31. var targetDlls = GetDlls(target);
  32. var baselineDlls = GetDlls(baseline);
  33. var left = new List<string>();
  34. var right = new List<string>();
  35. var suppressionFile = Path.Combine(suppressionFilesFolder, Path.GetFileName(packagePath) + ".xml");
  36. foreach (var baselineDll in baselineDlls)
  37. {
  38. var baselineDllPath = Path.Combine("baseline", baselineDll.target, baselineDll.entry.Name);
  39. var baselineDllRealPath = Path.Combine(tempFolder, baselineDllPath);
  40. Directory.CreateDirectory(Path.GetDirectoryName(baselineDllRealPath)!);
  41. await using (var baselineDllFile = File.Create(baselineDllRealPath))
  42. {
  43. await baselineDll.entry.Open().CopyToAsync(baselineDllFile);
  44. }
  45. var targetDll = targetDlls.FirstOrDefault(e =>
  46. e.target == baselineDll.target && e.entry.Name == baselineDll.entry.Name);
  47. if (targetDll.entry is null)
  48. {
  49. throw new InvalidOperationException($"Some assemblies are missing in the new package: {baselineDll.entry.Name} for {baselineDll.target}");
  50. }
  51. var targetDllPath = Path.Combine("target", targetDll.target, targetDll.entry.Name);
  52. var targetDllRealPath = Path.Combine(tempFolder, targetDllPath);
  53. Directory.CreateDirectory(Path.GetDirectoryName(targetDllRealPath)!);
  54. await using (var targetDllFile = File.Create(targetDllRealPath))
  55. {
  56. await targetDll.entry.Open().CopyToAsync(targetDllFile);
  57. }
  58. left.Add(baselineDllPath);
  59. right.Add(targetDllPath);
  60. }
  61. if (left.Any())
  62. {
  63. var args = $""" -l={string.Join(',', left)} -r="{string.Join(',', right)}" """;
  64. if (File.Exists(suppressionFile))
  65. {
  66. args += $""" --suppression-file="{suppressionFile}" """;
  67. }
  68. if (updateSuppressionFile)
  69. {
  70. args += $""" --suppression-output-file="{suppressionFile}" --generate-suppression-file=true """;
  71. }
  72. var result = apiCompatTool(args, tempFolder)
  73. .Where(t => t.Type == OutputType.Err).ToArray();
  74. if (result.Any())
  75. {
  76. throw new AggregateException(
  77. $"ApiDiffValidation task has failed for \"{Path.GetFileName(packagePath)}\" package",
  78. result.Select(r => new Exception(r.Text)));
  79. }
  80. }
  81. }
  82. }
  83. private static IReadOnlyCollection<(string target, ZipArchiveEntry entry)> GetDlls(ZipArchive archive)
  84. {
  85. return archive.Entries
  86. .Where(e => Path.GetExtension(e.FullName) == ".dll")
  87. .Select(e => (
  88. entry: e,
  89. isRef: e.FullName.Contains("ref/"),
  90. target: Path.GetDirectoryName(e.FullName)!.Split('/').Last())
  91. )
  92. .GroupBy(e => (e.target, e.entry.Name))
  93. .Select(g => g.MaxBy(e => e.isRef))
  94. .Select(e => (e.target, e.entry))
  95. .ToArray();
  96. }
  97. static async Task<Stream> DownloadBaselinePackage(string packagePath, string baselineVersion)
  98. {
  99. Build.Information("Downloading {0} baseline package for version {1}", Path.GetFileName(packagePath), baselineVersion);
  100. try
  101. {
  102. var packageId = Regex.Replace(
  103. Path.GetFileNameWithoutExtension(packagePath),
  104. """(\.\d+\.\d+\.\d+)$""", "");
  105. using var response = await s_httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get,
  106. $"https://www.nuget.org/api/v2/package/{packageId}/{baselineVersion}"), HttpCompletionOption.ResponseHeadersRead);
  107. response.EnsureSuccessStatusCode();
  108. await using var stream = await response.Content.ReadAsStreamAsync();
  109. var memoryStream = new MemoryStream();
  110. await stream.CopyToAsync(memoryStream);
  111. memoryStream.Seek(0, SeekOrigin.Begin);
  112. return memoryStream;
  113. }
  114. catch (Exception ex)
  115. {
  116. throw new InvalidOperationException($"Downloading baseline package for {packagePath} failed.\r" + ex.Message, ex);
  117. }
  118. }
  119. }