diff --git a/nukebuild/ApiDiffHelper.cs b/nukebuild/ApiDiffHelper.cs index 4a51630557..d16495969e 100644 --- a/nukebuild/ApiDiffHelper.cs +++ b/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(); + // 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(); + + void GenerateMissingAssemblies(IEnumerable missingFileNames, string folderPath) + { + foreach (var missingFileName in missingFileNames) + { + GenerateEmptyAssembly( + Path.GetFileNameWithoutExtension(missingFileName), + frameworkDiff.Framework.GetShortFolderName(), + Path.Join(folderPath, missingFileName)); + } + } + } } static async Task CreateNuGetDownloadContextAsync() @@ -453,6 +504,44 @@ public static class ApiDiffHelper value; } + static void GenerateEmptyAssembly(string name, string framework, string outputFilePath) + { + var projectContents = + $""" + + + {framework} + Release + None + + + """; + + 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, diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 20800d98bc..4792aa0445 100644 --- a/nukebuild/Build.cs +++ b/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); });