Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Api validation tool #12072

Merged
merged 10 commits into from
Jul 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 16 additions & 12 deletions .nuke/build.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,18 @@
"build": {
"type": "object",
"properties": {
"Configuration": {
"type": "string",
"description": "configuration"
"api-baseline": {
"type": "string"
},
"configuration": {
"type": "string"
},
"Continue": {
"type": "boolean",
"description": "Indicates to continue a previously failed build attempt"
},
"ForceNugetVersion": {
"type": "string",
"description": "force-nuget-version"
"force-nuget-version": {
"type": "string"
},
"Help": {
"type": "boolean",
Expand Down Expand Up @@ -89,17 +90,16 @@
"RunRenderTests",
"RunTests",
"RunToolsTests",
"ValidateApiDiff",
"ZipFiles"
]
}
},
"SkipPreviewer": {
"type": "boolean",
"description": "skip-previewer"
"skip-previewer": {
"type": "boolean"
},
"SkipTests": {
"type": "boolean",
"description": "skip-tests"
"skip-tests": {
"type": "boolean"
},
"Target": {
"type": "array",
Expand All @@ -124,10 +124,14 @@
"RunRenderTests",
"RunTests",
"RunToolsTests",
"ValidateApiDiff",
"ZipFiles"
]
}
},
"update-api-suppression": {
"type": "boolean"
},
"Verbosity": {
"type": "string",
"description": "Logging verbosity during build execution. Default is 'Normal'",
Expand Down
145 changes: 145 additions & 0 deletions nukebuild/ApiDiffValidation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Nuke.Common.Tooling;

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

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!);
}

await using (var baselineStream = await DownloadBaselinePackage(packagePath, baselineVersion))
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 left = new List<string>();
var right = new List<string>();

var suppressionFile = Path.Combine(suppressionFilesFolder, Path.GetFileName(packagePath) + ".xml");

foreach (var baselineDll in baselineDlls)
{
var baselineDllPath = Path.Combine("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 targetDll = targetDlls.FirstOrDefault(e =>
e.target == baselineDll.target && e.entry.Name == baselineDll.entry.Name);
if (targetDll.entry is null)
{
throw new InvalidOperationException($"Some assemblies are missing in the new package: {baselineDll.entry.Name} for {baselineDll.target}");
}

var targetDllPath = Path.Combine("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);
}

left.Add(baselineDllPath);
right.Add(targetDllPath);
}

if (left.Any())
{
var args = $""" -l={string.Join(',', left)} -r="{string.Join(',', right)}" """;
if (File.Exists(suppressionFile))
{
args += $""" --suppression-file="{suppressionFile}" """;
}

if (updateSuppressionFile)
{
args += $""" --suppression-output-file="{suppressionFile}" --generate-suppression-file=true """;
}

var result = apiCompatTool(args, tempFolder)
.Where(t => t.Type == OutputType.Err).ToArray();
if (result.Any())
{
throw new AggregateException(
$"ApiDiffValidation task has failed for \"{Path.GetFileName(packagePath)}\" package",
result.Select(r => new Exception(r.Text)));
}
}
}
}

private static IReadOnlyCollection<(string target, ZipArchiveEntry entry)> GetDlls(ZipArchive archive)
{
return archive.Entries
.Where(e => Path.GetExtension(e.FullName) == ".dll"
// Exclude analyzers and build task, as we don't care about breaking changes there
&& !e.FullName.Contains("analyzers/") && !e.Name.Contains("Avalonia.Build.Tasks"))
.Select(e => (
entry: e,
isRef: e.FullName.Contains("ref/"),
target: Path.GetDirectoryName(e.FullName)!.Split('/').Last())
)
.GroupBy(e => (e.target, e.entry.Name))
.Select(g => g.MaxBy(e => e.isRef))
.Select(e => (e.target, e.entry))
.ToArray();
}

static async Task<Stream> DownloadBaselinePackage(string packagePath, string baselineVersion)
{
Build.Information("Downloading {0} baseline package for version {1}", Path.GetFileName(packagePath), baselineVersion);

try
{
/*
Gets package name from versions like:
Avalonia.0.10.0-preview1
Avalonia.11.0.999-cibuild0037534-beta
Avalonia.11.0.0
*/
var packageId = Regex.Replace(
Path.GetFileNameWithoutExtension(packagePath),
"""(\.\d+\.\d+\.\d+(?:-.+)?)$""", "");

using var response = await s_httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get,
$"https://www.nuget.org/api/v2/package/{packageId}/{baselineVersion}"), HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();

await using var stream = await response.Content.ReadAsStreamAsync();
var memoryStream = new MemoryStream();
await stream.CopyToAsync(memoryStream);
memoryStream.Seek(0, SeekOrigin.Begin);
return memoryStream;
}
catch (Exception ex)
{
throw new InvalidOperationException($"Downloading baseline package for {packagePath} failed.\r" + ex.Message, ex);
}
}
}
19 changes: 17 additions & 2 deletions nukebuild/Build.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ running and debugging a particular target (optionally without deps) would be way
partial class Build : NukeBuild
{
BuildParameters Parameters { get; set; }

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

protected override void OnBuildInitialized()
{
Parameters = new BuildParameters(this);
Expand Down Expand Up @@ -278,7 +282,17 @@ void DoMemoryTest()
RefAssemblyGenerator.GenerateRefAsmsInPackage(Parameters.NugetRoot / "Avalonia." +
Parameters.Version + ".nupkg");
});


Target ValidateApiDiff => _ => _
.DependsOn(CreateNugetPackages)
.Executes(async () =>
{
await Task.WhenAll(
Directory.GetFiles(Parameters.NugetRoot).Select(nugetPackage => ApiDiffValidation.ValidatePackage(
ApiCompatTool, nugetPackage, Parameters.ApiValidationBaseline,
Parameters.ApiValidationSuppressionFiles, Parameters.UpdateApiValidationSuppression)));
});

Target RunTests => _ => _
.DependsOn(RunCoreLibsTests)
.DependsOn(RunRenderTests)
Expand All @@ -288,7 +302,8 @@ void DoMemoryTest()

Target Package => _ => _
.DependsOn(RunTests)
.DependsOn(CreateNugetPackages);
.DependsOn(CreateNugetPackages)
.DependsOn(ValidateApiDiff);

Target CiAzureLinux => _ => _
.DependsOn(RunTests);
Expand Down
22 changes: 17 additions & 5 deletions nukebuild/BuildParameters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,24 @@

public partial class Build
{
[Parameter("configuration")]
[Parameter(Name = "configuration")]
public string Configuration { get; set; }

[Parameter("skip-tests")]
[Parameter(Name = "skip-tests")]
public bool SkipTests { get; set; }

[Parameter("force-nuget-version")]
[Parameter(Name = "force-nuget-version")]
public string ForceNugetVersion { get; set; }

[Parameter("skip-previewer")]
[Parameter(Name = "skip-previewer")]
public bool SkipPreviewer { get; set; }

[Parameter(Name = "api-baseline")]
public string ApiValidationBaseline { get; set; }

[Parameter(Name = "update-api-suppression")]
public bool? UpdateApiValidationSuppression { get; set; }

public class BuildParameters
{
public string Configuration { get; }
Expand Down Expand Up @@ -57,7 +63,9 @@ public class BuildParameters
public string FileZipSuffix { get; }
public AbsolutePath ZipCoreArtifacts { get; }
public AbsolutePath ZipNuGetArtifacts { get; }

public string ApiValidationBaseline { get; }
public bool UpdateApiValidationSuppression { get; }
public AbsolutePath ApiValidationSuppressionFiles { get; }

public BuildParameters(Build b)
{
Expand Down Expand Up @@ -103,6 +111,9 @@ public BuildParameters(Build b)
// VERSION
Version = b.ForceNugetVersion ?? GetVersion();

ApiValidationBaseline = b.ApiValidationBaseline ?? new Version(new Version(Version).Major, 0).ToString();
UpdateApiValidationSuppression = b.UpdateApiValidationSuppression ?? IsLocalBuild;

if (IsRunningOnAzure)
{
if (!IsNuGetRelease)
Expand All @@ -125,6 +136,7 @@ public BuildParameters(Build b)
FileZipSuffix = Version + ".zip";
ZipCoreArtifacts = ZipRoot / ("Avalonia-" + FileZipSuffix);
ZipNuGetArtifacts = ZipRoot / ("Avalonia-NuGet-" + FileZipSuffix);
ApiValidationSuppressionFiles = RootDirectory / "api";
}

string GetVersion()
Expand Down
4 changes: 2 additions & 2 deletions nukebuild/Shims.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@

public partial class Build
{
static void Information(string info)
internal static void Information(string info)
{
Logger.Info(info);
}

static void Information(string info, params object[] args)
internal static void Information(string info, params object[] args)
{
Logger.Info(info, args);
}
Expand Down
2 changes: 2 additions & 0 deletions nukebuild/_build.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

<PackageDownload Include="Microsoft.DotNet.ApiCompat.Tool" Version="[7.0.305]" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading