Skip to content

Commit

Permalink
Implement helper OutputApiDiff target (AvaloniaUI#13818)
Browse files Browse the repository at this point in the history
  • Loading branch information
maxkatz6 authored Dec 22, 2023
1 parent 10b203b commit 02ddfad
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 25 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -217,3 +217,4 @@ node_modules
src/Browser/Avalonia.Browser.Blazor/webapp/package-lock.json
src/Browser/Avalonia.Browser.Blazor/wwwroot
src/Browser/Avalonia.Browser/wwwroot
api/diff
2 changes: 2 additions & 0 deletions .nuke/build.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
"CreateIntermediateNugetPackages",
"CreateNugetPackages",
"GenerateCppHeaders",
"OutputApiDiff",
"Package",
"RunCoreLibsTests",
"RunHtmlPreviewerTests",
Expand Down Expand Up @@ -117,6 +118,7 @@
"CreateIntermediateNugetPackages",
"CreateNugetPackages",
"GenerateCppHeaders",
"OutputApiDiff",
"Package",
"RunCoreLibsTests",
"RunHtmlPreviewerTests",
Expand Down
13 changes: 13 additions & 0 deletions Avalonia.sln
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,19 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{176582E8-46AF-416A-85C1-13A5C6744497}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
azure-pipelines.yml = azure-pipelines.yml
azure-pipelines-integrationtests.yml = azure-pipelines-integrationtests.yml
CODE_OF_CONDUCT.md = CODE_OF_CONDUCT.md
CONTRIBUTING.md = CONTRIBUTING.md
Directory.Build.props = Directory.Build.props
Directory.Build.targets = Directory.Build.targets
dirs.proj = dirs.proj
global.json = global.json
licence.md = licence.md
NOTICE.md = NOTICE.md
NuGet.Config = NuGet.Config
readme.md = readme.md
Settings.StyleCop = Settings.StyleCop
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater", "src\Avalonia.Controls.ItemsRepeater\Avalonia.Controls.ItemsRepeater.csproj", "{EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}"
Expand Down
150 changes: 126 additions & 24 deletions nukebuild/ApiDiffValidation.cs → nukebuild/ApiDiffHelper.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
Expand All @@ -10,9 +11,95 @@
using Nuke.Common.Tooling;
using static Serilog.Log;

public static class ApiDiffValidation
public static class ApiDiffHelper
{
private static readonly HttpClient s_httpClient = new();
static readonly HttpClient s_httpClient = new();

public static async Task GetDiff(
Tool apiDiffTool, string outputFolder,
string packagePath, string baselineVersion)
{
await using var baselineStream = await DownloadBaselinePackage(packagePath, baselineVersion);
if (baselineStream == null)
return;

if (!Directory.Exists(outputFolder))
{
Directory.CreateDirectory(outputFolder!);
}

using (var target = new ZipArchive(File.Open(packagePath, FileMode.Open, FileAccess.Read), ZipArchiveMode.Read))
using (var baseline = new ZipArchive(baselineStream, ZipArchiveMode.Read))
using (Helpers.UseTempDir(out var tempFolder))
{
var targetDlls = GetDlls(target);
var baselineDlls = GetDlls(baseline);

var pairs = new List<(string baseline, string target)>();

var packageId = GetPackageId(packagePath);

// Don't use Path.Combine with these left and right tool parameters.
// Microsoft.DotNet.ApiCompat.Tool is stupid and treats '/' and '\' as different assemblies in suppression files.
// So, always use Unix '/'
foreach (var baselineDll in baselineDlls)
{
var baselineDllPath = await ExtractDll("baseline", baselineDll, tempFolder);

var targetTfm = baselineDll.target;
if (s_tfmRedirects.FirstOrDefault(t => baselineDll.target.StartsWith(t.oldTfm)).newTfm is {} newTfm)
{
targetTfm = newTfm;
}

var targetDll = targetDlls.FirstOrDefault(e =>
e.target.StartsWith(targetTfm) && e.entry.Name == baselineDll.entry.Name);
if (targetDll?.entry is null)
{
throw new InvalidOperationException($"Some assemblies are missing in the new package {packageId}: {baselineDll.entry.Name} for {baselineDll.target}");
}

var targetDllPath = await ExtractDll("target", targetDll, tempFolder);

pairs.Add((baselineDllPath, targetDllPath));
}

await Task.WhenAll(pairs.Select(p => Task.Run(() =>
{
var baselineApi = p.baseline + ".api.cs";
var targetApi = p.target + ".api.cs";
var resultDiff = p.target + ".api.diff.cs";

GenerateApiListing(apiDiffTool, p.baseline, baselineApi, tempFolder);
GenerateApiListing(apiDiffTool, p.target, targetApi, tempFolder);

var args = $"""-c core.autocrlf=false diff --no-index --minimal """;
args += """--ignore-matching-lines="^\[assembly: System.Reflection.AssemblyVersionAttribute" """;
args += $""" --output {resultDiff} {baselineApi} {targetApi}""";

using (var gitProcess = new Process())
{
gitProcess.StartInfo = new ProcessStartInfo
{
CreateNoWindow = true,
RedirectStandardError = false,
RedirectStandardOutput = false,
FileName = "git",
Arguments = args,
WorkingDirectory = tempFolder
};
gitProcess.Start();
gitProcess.WaitForExit();
}

var resultFile = new FileInfo(Path.Combine(tempFolder, resultDiff));
if (resultFile.Length > 0)
{
resultFile.CopyTo(Path.Combine(outputFolder, Path.GetFileName(resultDiff)), true);
}
})));
}
}

private static readonly (string oldTfm, string newTfm)[] s_tfmRedirects = new[]
{
Expand All @@ -25,12 +112,6 @@ public static async Task ValidatePackage(
Tool apiCompatTool, string packagePath, string baselineVersion,
string suppressionFilesFolder, bool updateSuppressionFile)
{
if (baselineVersion is null)
{
throw new InvalidOperationException(
"Build \"api-baseline\" parameter must be set when running Nuke CreatePackages");
}

if (!Directory.Exists(suppressionFilesFolder))
{
Directory.CreateDirectory(suppressionFilesFolder!);
Expand Down Expand Up @@ -58,13 +139,7 @@ public static async Task ValidatePackage(
// So, always use Unix '/'
foreach (var baselineDll in baselineDlls)
{
var baselineDllPath = $"baseline/{baselineDll.target}/{baselineDll.entry.Name}";
var baselineDllRealPath = Path.Combine(tempFolder, baselineDllPath);
Directory.CreateDirectory(Path.GetDirectoryName(baselineDllRealPath)!);
await using (var baselineDllFile = File.Create(baselineDllRealPath))
{
await baselineDll.entry.Open().CopyToAsync(baselineDllFile);
}
var baselineDllPath = await ExtractDll("baseline", baselineDll, tempFolder);

var targetTfm = baselineDll.target;
if (s_tfmRedirects.FirstOrDefault(t => baselineDll.target.StartsWith(t.oldTfm)).newTfm is {} newTfm)
Expand All @@ -79,13 +154,7 @@ public static async Task ValidatePackage(
throw new InvalidOperationException($"Some assemblies are missing in the new package {packageId}: {baselineDll.entry.Name} for {baselineDll.target}");
}

var targetDllPath = $"target/{targetDll.target}/{targetDll.entry.Name}";
var targetDllRealPath = Path.Combine(tempFolder, targetDllPath);
Directory.CreateDirectory(Path.GetDirectoryName(targetDllRealPath)!);
await using (var targetDllFile = File.Create(targetDllRealPath))
{
await targetDll.entry.Open().CopyToAsync(targetDllFile);
}
var targetDllPath = await ExtractDll("target", targetDll, tempFolder);

left.Add(baselineDllPath);
right.Add(targetDllPath);
Expand Down Expand Up @@ -116,7 +185,9 @@ public static async Task ValidatePackage(
}
}

private static IReadOnlyCollection<(string target, ZipArchiveEntry entry)> GetDlls(ZipArchive archive)
record DllEntry(string target, ZipArchiveEntry entry);

static IReadOnlyCollection<DllEntry> GetDlls(ZipArchive archive)
{
return archive.Entries
.Where(e => Path.GetExtension(e.FullName) == ".dll"
Expand All @@ -130,12 +201,18 @@ public static async Task ValidatePackage(
)
.GroupBy(e => (e.target, e.entry.Name))
.Select(g => g.MaxBy(e => e.isRef))
.Select(e => (e.target, e.entry))
.Select(e => new DllEntry(e.target, e.entry))
.ToArray();
}

static async Task<Stream> DownloadBaselinePackage(string packagePath, string baselineVersion)
{
if (baselineVersion is null)
{
throw new InvalidOperationException(
"Build \"api-baseline\" parameter must be set when running Nuke CreatePackages");
}

/*
Gets package name from versions like:
Avalonia.0.10.0-preview1
Expand Down Expand Up @@ -167,6 +244,31 @@ static async Task<Stream> DownloadBaselinePackage(string packagePath, string bas
}
}

static async Task<string> ExtractDll(string basePath, DllEntry dllEntry, string targetFolder)
{
var dllPath = $"{basePath}/{dllEntry.target}/{dllEntry.entry.Name}";
var dllRealPath = Path.Combine(targetFolder, dllPath);
Directory.CreateDirectory(Path.GetDirectoryName(dllRealPath)!);
await using (var dllFile = File.Create(dllRealPath))
{
await dllEntry.entry.Open().CopyToAsync(dllFile);
}

return dllPath;
}

static void GenerateApiListing(Tool apiDiffTool, string inputFile, string outputFile, string workingDif)
{
var args = $""" --assembly={inputFile} --output-path={outputFile} --include-assembly-attributes=true""";
var result = apiDiffTool(args, workingDif)
.Where(t => t.Type == OutputType.Err).ToArray();
if (result.Any())
{
throw new AggregateException($"GetApi tool failed task has failed",
result.Select(r => new Exception(r.Text)));
}
}

static string GetPackageId(string packagePath)
{
return Regex.Replace(
Expand Down
16 changes: 15 additions & 1 deletion nukebuild/Build.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
using static Nuke.Common.Tools.VSWhere.VSWhereTasks;
using static Serilog.Log;
using MicroCom.CodeGenerator;
using Nuke.Common.IO;

/*
Before editing this file, install support plugin for your IDE,
Expand All @@ -33,6 +34,9 @@ partial class Build : NukeBuild

[PackageExecutable("Microsoft.DotNet.ApiCompat.Tool", "Microsoft.DotNet.ApiCompat.Tool.dll", Framework = "net6.0")]
Tool ApiCompatTool;

[PackageExecutable("Microsoft.DotNet.GenAPI.Tool", "Microsoft.DotNet.GenAPI.Tool.dll", Framework = "net8.0")]
Tool ApiGenTool;

protected override void OnBuildInitialized()
{
Expand Down Expand Up @@ -283,11 +287,21 @@ void DoMemoryTest()
.Executes(async () =>
{
await Task.WhenAll(
Directory.GetFiles(Parameters.NugetRoot, "*.nupkg").Select(nugetPackage => ApiDiffValidation.ValidatePackage(
Directory.GetFiles(Parameters.NugetRoot, "*.nupkg").Select(nugetPackage => ApiDiffHelper.ValidatePackage(
ApiCompatTool, nugetPackage, Parameters.ApiValidationBaseline,
Parameters.ApiValidationSuppressionFiles, Parameters.UpdateApiValidationSuppression)));
});

Target OutputApiDiff => _ => _
.DependsOn(CreateNugetPackages)
.Executes(async () =>
{
await Task.WhenAll(
Directory.GetFiles(Parameters.NugetRoot, "*.nupkg").Select(nugetPackage => ApiDiffHelper.GetDiff(
ApiGenTool, RootDirectory / "api" / "diff",
nugetPackage, Parameters.ApiValidationBaseline)));
});

Target RunTests => _ => _
.DependsOn(RunCoreLibsTests)
.DependsOn(RunRenderTests)
Expand Down
3 changes: 3 additions & 0 deletions nukebuild/_build.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
<NoWarn>$(NoWarn);CS0649;CS0169;SYSLIB0011</NoWarn>
<NukeTelemetryVersion>1</NukeTelemetryVersion>
<TargetFramework>net7.0</TargetFramework>
<!-- Necessary for Microsoft.DotNet.GenAPI.Tool -->
<RestoreAdditionalProjectSources>https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet8-transport/nuget/v3/index.json</RestoreAdditionalProjectSources>
</PropertyGroup>

<Import Project="..\build\JetBrains.dotMemoryUnit.props" />
Expand All @@ -24,6 +26,7 @@
</PackageReference>

<PackageDownload Include="Microsoft.DotNet.ApiCompat.Tool" Version="[7.0.305]" />
<PackageDownload Include="Microsoft.DotNet.GenAPI.Tool" Version="[8.0.101-servicing.23580.12]" />
</ItemGroup>

<ItemGroup>
Expand Down

0 comments on commit 02ddfad

Please sign in to comment.