Răsfoiți Sursa

Handle added packages in API diff (#20428)

Julien Lebosquain 1 săptămână în urmă
părinte
comite
d034e5f4e8
2 a modificat fișierele cu 105 adăugiri și 13 ștergeri
  1. 101 12
      nukebuild/ApiDiffHelper.cs
  2. 4 1
      nukebuild/Build.cs

+ 101 - 12
nukebuild/ApiDiffHelper.cs

@@ -304,44 +304,95 @@ public static class ApiDiffHelper
             {
                 using var packageReader = new PackageArchiveReader(currentArchive);
                 packageId = packageReader.NuspecReader.GetId();
+
+                baselineFolderPath = outputFolderPath / "baseline" / packageId;
+                Directory.CreateDirectory(baselineFolderPath);
+
                 currentFolderPath = outputFolderPath / "current" / packageId;
+                Directory.CreateDirectory(currentFolderPath);
+
                 currentFolderNames = ExtractDiffableAssembliesFromPackage(currentArchive, currentFolderPath);
             }
 
-            // Download baseline package
-            memoryStream.Position = 0L;
-            memoryStream.SetLength(0L);
-            await DownloadBaselinePackageAsync(memoryStream, downloadContext, packageId, baselineVersion);
-            memoryStream.Position = 0L;
+            var packageExists = await downloadContext.FindPackageByIdResource.DoesPackageExistAsync(
+                packageId,
+                baselineVersion,
+                downloadContext.CacheContext,
+                NullLogger.Instance,
+                CancellationToken.None);
 
-            // Extract baseline package
-            using (var baselineArchive = new ZipArchive(memoryStream, ZipArchiveMode.Read, leaveOpen: true))
+            if (packageExists)
             {
-                baselineFolderPath = outputFolderPath / "baseline" / packageId;
+                // Download baseline package
+                memoryStream.Position = 0L;
+                memoryStream.SetLength(0L);
+                await DownloadBaselinePackageAsync(memoryStream, downloadContext, packageId, baselineVersion);
+                memoryStream.Position = 0L;
+
+                // Extract baseline package
+                using var baselineArchive = new ZipArchive(memoryStream, ZipArchiveMode.Read, leaveOpen: true);
                 baselineFolderNames = ExtractDiffableAssembliesFromPackage(baselineArchive, baselineFolderPath);
             }
+            else
+            {
+                Information("Baseline package {Id} {Version} does not exist. Assuming new package.", packageId, baselineVersion);
+                baselineFolderNames = [];
+            }
 
             if (currentFolderNames.Count == 0 && baselineFolderNames.Count == 0)
                 continue;
 
             var frameworkDiffs = new List<FrameworkDiffInfo>();
 
+            // Match frameworks
             foreach (var (framework, currentFolderName) in currentFolderNames)
             {
-                // Ignore new frameworks that didn't exist in the baseline package. Empty folders make the ApiDiff tool crash.
                 if (!baselineFolderNames.TryGetValue(framework, out var baselineFolderName))
-                    continue;
+                    baselineFolderName = currentFolderName;
 
-                frameworkDiffs.Add(new FrameworkDiffInfo(
+                var frameworkDiff = new FrameworkDiffInfo(
                     framework,
                     baselineFolderPath / FolderLib / baselineFolderName,
-                    currentFolderPath / FolderLib / currentFolderName));
+                    currentFolderPath / FolderLib / currentFolderName);
+
+                EnsureAssemblies(frameworkDiff);
+
+                frameworkDiffs.Add(frameworkDiff);
             }
 
             packageDiffs.Add(new PackageDiffInfo(packageId, [..frameworkDiffs]));
         }
 
         return new GlobalDiffInfo(baselineVersion, currentVersion, packageDiffs.DrainToImmutable());
+
+        // Ensure that both sides of a framework diff have matching assemblies.
+        // For any missing, generate an empty assembly to diff against.
+        // (The API diff tool supports added and removed assemblies in theory but actually throws if one side doesn't have any.)
+        static void EnsureAssemblies(FrameworkDiffInfo frameworkDiff)
+        {
+            Directory.CreateDirectory(frameworkDiff.BaselineFolderPath);
+            Directory.CreateDirectory(frameworkDiff.CurrentFolderPath);
+
+            var baselineFileNames = GetFileNames(frameworkDiff.BaselineFolderPath);
+            var currentFileNames = GetFileNames(frameworkDiff.CurrentFolderPath);
+
+            GenerateMissingAssemblies(currentFileNames.Except(baselineFileNames), frameworkDiff.BaselineFolderPath);
+            GenerateMissingAssemblies(baselineFileNames.Except(currentFileNames), frameworkDiff.CurrentFolderPath);
+
+            static string[] GetFileNames(string folderPath)
+                => Directory.EnumerateFiles(folderPath, "*.dll").Select(Path.GetFileName)!.ToArray<string>();
+
+            void GenerateMissingAssemblies(IEnumerable<string> missingFileNames, string folderPath)
+            {
+                foreach (var missingFileName in missingFileNames)
+                {
+                    GenerateEmptyAssembly(
+                        Path.GetFileNameWithoutExtension(missingFileName),
+                        frameworkDiff.Framework.GetShortFolderName(),
+                        Path.Join(folderPath, missingFileName));
+                }
+            }
+        }
     }
 
     static async Task<NuGetDownloadContext> CreateNuGetDownloadContextAsync()
@@ -453,6 +504,44 @@ public static class ApiDiffHelper
                 value;
     }
 
+    static void GenerateEmptyAssembly(string name, string framework, string outputFilePath)
+    {
+        var projectContents =
+            $"""
+             <Project Sdk="Microsoft.NET.Sdk">
+              <PropertyGroup>
+                <TargetFramework>{framework}</TargetFramework>
+                <Configuration>Release</Configuration>
+                <DebugType>None</DebugType>
+              </PropertyGroup>
+             </Project>
+             """;
+
+        var tempDirPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
+        var projectFilePath = Path.Join(tempDirPath, $"{name}.csproj");
+
+        Directory.CreateDirectory(tempDirPath);
+
+        try
+        {
+            File.WriteAllText(projectFilePath, projectContents);
+
+            using var process = ProcessTasks.StartProcess(
+                "dotnet",
+                $"build \"{projectFilePath}\" --output \"{tempDirPath}\"",
+                tempDirPath);
+
+            process.AssertZeroExitCode();
+
+            File.Copy(Path.Join(tempDirPath, $"{name}.dll"), outputFilePath);
+        }
+        finally
+        {
+            if (Directory.Exists(tempDirPath))
+                Directory.Delete(tempDirPath, true);
+        }
+    }
+
     public sealed class GlobalDiffInfo(
         NuGetVersion baselineVersion,
         NuGetVersion currentVersion,

+ 4 - 1
nukebuild/Build.cs

@@ -336,11 +336,14 @@ partial class Build : NukeBuild
         .DependsOn(CreateNugetPackages)
         .Executes(async () =>
         {
+            var apiDiffPath = Parameters.ArtifactsDir / "api-diff";
+            apiDiffPath.DeleteDirectory();
+
             GlobalDiff = await ApiDiffHelper.DownloadAndExtractPackagesAsync(
                 Directory.EnumerateFiles(Parameters.NugetRoot, "*.nupkg").Select(path => (AbsolutePath)path),
                 NuGetVersion.Parse(Parameters.Version),
                 Parameters.IsReleaseBranch,
-                Parameters.ArtifactsDir / "api-diff" / "assemblies",
+                apiDiffPath / "assemblies",
                 Parameters.ForceApiValidationBaseline is { } forcedBaseline ? NuGetVersion.Parse(forcedBaseline) : null);
         });