Browse Source

Improved API diff (#19490)

* Improved API diff

* Merge API diff files

* Ignore platform version for API diff

* Start with fresh suppression files
Julien Lebosquain 2 months ago
parent
commit
66724d4785

+ 5 - 3
.nuke/build.schema.json

@@ -6,9 +6,6 @@
     "build": {
     "build": {
       "type": "object",
       "type": "object",
       "properties": {
       "properties": {
-        "api-baseline": {
-          "type": "string"
-        },
         "configuration": {
         "configuration": {
           "type": "string"
           "type": "string"
         },
         },
@@ -16,6 +13,9 @@
           "type": "boolean",
           "type": "boolean",
           "description": "Indicates to continue a previously failed build attempt"
           "description": "Indicates to continue a previously failed build attempt"
         },
         },
+        "force-api-baseline": {
+          "type": "string"
+        },
         "force-nuget-version": {
         "force-nuget-version": {
           "type": "string"
           "type": "string"
         },
         },
@@ -83,6 +83,7 @@
               "CompileNative",
               "CompileNative",
               "CreateIntermediateNugetPackages",
               "CreateIntermediateNugetPackages",
               "CreateNugetPackages",
               "CreateNugetPackages",
+              "DownloadApiBaselinePackages",
               "GenerateCppHeaders",
               "GenerateCppHeaders",
               "OutputApiDiff",
               "OutputApiDiff",
               "OutputVersion",
               "OutputVersion",
@@ -121,6 +122,7 @@
               "CompileNative",
               "CompileNative",
               "CreateIntermediateNugetPackages",
               "CreateIntermediateNugetPackages",
               "CreateNugetPackages",
               "CreateNugetPackages",
+              "DownloadApiBaselinePackages",
               "GenerateCppHeaders",
               "GenerateCppHeaders",
               "OutputApiDiff",
               "OutputApiDiff",
               "OutputVersion",
               "OutputVersion",

+ 1 - 2
NuGet.Config

@@ -3,7 +3,6 @@
 <configuration>
 <configuration>
   <packageSources>
   <packageSources>
     <clear />
     <clear />
-    <add key="api.nuget.org" value="https://api.nuget.org/v3/index.json" />    
-    <add key="skiasharp" value="https://aka.ms/skiasharp-eap/index.json" />
+    <add key="api.nuget.org" value="https://api.nuget.org/v3/index.json" />
   </packageSources>
   </packageSources>
 </configuration>
 </configuration>

+ 0 - 22
api/Avalonia.Android.nupkg.xml

@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- https://learn.microsoft.com/en-us/dotnet/fundamentals/package-validation/diagnostic-ids -->
-<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Android.AvaloniaMainActivity`1</Target>
-    <Left>baseline/net8.0-android34.0/Avalonia.Android.dll</Left>
-    <Right>target/net8.0-android34.0/Avalonia.Android.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0002</DiagnosticId>
-    <Target>M:Avalonia.Android.AndroidViewControlHandle.get_HandleDescriptor</Target>
-    <Left>baseline/net8.0-android34.0/Avalonia.Android.dll</Left>
-    <Right>target/net8.0-android34.0/Avalonia.Android.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0007</DiagnosticId>
-    <Target>T:Avalonia.Android.AndroidViewControlHandle</Target>
-    <Left>baseline/net8.0-android34.0/Avalonia.Android.dll</Left>
-    <Right>target/net8.0-android34.0/Avalonia.Android.dll</Right>
-  </Suppression>
-</Suppressions>

+ 0 - 22
api/Avalonia.Browser.nupkg.xml

@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- https://learn.microsoft.com/en-us/dotnet/fundamentals/package-validation/diagnostic-ids -->
-<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
-  <Suppression>
-    <DiagnosticId>CP0002</DiagnosticId>
-    <Target>M:Avalonia.Browser.JSObjectControlHandle.get_Handle</Target>
-    <Left>baseline/net8.0-browser1.0/Avalonia.Browser.dll</Left>
-    <Right>target/net8.0-browser1.0/Avalonia.Browser.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0002</DiagnosticId>
-    <Target>M:Avalonia.Browser.JSObjectControlHandle.get_HandleDescriptor</Target>
-    <Left>baseline/net8.0-browser1.0/Avalonia.Browser.dll</Left>
-    <Right>target/net8.0-browser1.0/Avalonia.Browser.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0007</DiagnosticId>
-    <Target>T:Avalonia.Browser.JSObjectControlHandle</Target>
-    <Left>baseline/net8.0-browser1.0/Avalonia.Browser.dll</Left>
-    <Right>target/net8.0-browser1.0/Avalonia.Browser.dll</Right>
-  </Suppression>
-</Suppressions>

+ 0 - 10
api/Avalonia.FreeDesktop.nupkg.xml

@@ -1,10 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- https://learn.microsoft.com/en-us/dotnet/fundamentals/package-validation/diagnostic-ids -->
-<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Tmds.DBus.SourceGenerator.PropertyChanges`1</Target>
-    <Left>baseline/netstandard2.0/Avalonia.FreeDesktop.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.FreeDesktop.dll</Right>
-  </Suppression>
-</Suppressions>

+ 0 - 16
api/Avalonia.Skia.nupkg.xml

@@ -1,16 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- https://learn.microsoft.com/en-us/dotnet/fundamentals/package-validation/diagnostic-ids -->
-<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
-  <Suppression>
-    <DiagnosticId>CP0002</DiagnosticId>
-    <Target>M:Avalonia.Skia.SkiaSharpExtensions.ToSKFilterQuality(Avalonia.Media.Imaging.BitmapInterpolationMode)</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Skia.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Skia.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0006</DiagnosticId>
-    <Target>M:Avalonia.Skia.ISkiaGpuWithPlatformGraphicsContext.TryGetGrContext</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Skia.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Skia.dll</Right>
-  </Suppression>
-</Suppressions>

+ 0 - 16
api/Avalonia.Themes.Fluent.nupkg.xml

@@ -1,16 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- https://learn.microsoft.com/en-us/dotnet/fundamentals/package-validation/diagnostic-ids -->
-<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
-  <Suppression>
-    <DiagnosticId>CP0007</DiagnosticId>
-    <Target>T:Avalonia.Themes.Fluent.ColorPaletteResources</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Themes.Fluent.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Themes.Fluent.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0008</DiagnosticId>
-    <Target>T:Avalonia.Themes.Fluent.ColorPaletteResources</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Themes.Fluent.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Themes.Fluent.dll</Right>
-  </Suppression>
-</Suppressions>

+ 0 - 214
api/Avalonia.Win32.nupkg.xml

@@ -1,214 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- https://learn.microsoft.com/en-us/dotnet/fundamentals/package-validation/diagnostic-ids -->
-<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Win32.Interop.Automation.DockPosition</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Win32.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Win32.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Win32.Interop.Automation.IDockProvider</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Win32.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Win32.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Win32.Interop.Automation.IExpandCollapseProvider</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Win32.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Win32.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Win32.Interop.Automation.IGridItemProvider</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Win32.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Win32.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Win32.Interop.Automation.IGridProvider</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Win32.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Win32.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Win32.Interop.Automation.IInvokeProvider</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Win32.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Win32.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Win32.Interop.Automation.IMultipleViewProvider</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Win32.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Win32.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Win32.Interop.Automation.IRangeValueProvider</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Win32.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Win32.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Win32.Interop.Automation.IRawElementProviderAdviseEvents</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Win32.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Win32.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Win32.Interop.Automation.IRawElementProviderFragment</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Win32.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Win32.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Win32.Interop.Automation.IRawElementProviderFragmentRoot</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Win32.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Win32.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Win32.Interop.Automation.IRawElementProviderSimple</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Win32.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Win32.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Win32.Interop.Automation.IRawElementProviderSimple2</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Win32.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Win32.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Win32.Interop.Automation.IScrollItemProvider</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Win32.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Win32.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Win32.Interop.Automation.IScrollProvider</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Win32.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Win32.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Win32.Interop.Automation.ISelectionItemProvider</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Win32.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Win32.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Win32.Interop.Automation.ISelectionProvider</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Win32.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Win32.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Win32.Interop.Automation.ISynchronizedInputProvider</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Win32.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Win32.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Win32.Interop.Automation.ITableItemProvider</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Win32.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Win32.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Win32.Interop.Automation.ITableProvider</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Win32.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Win32.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Win32.Interop.Automation.ITextProvider</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Win32.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Win32.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Win32.Interop.Automation.ITextRangeProvider</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Win32.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Win32.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Win32.Interop.Automation.IToggleProvider</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Win32.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Win32.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Win32.Interop.Automation.ITransformProvider</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Win32.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Win32.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Win32.Interop.Automation.IValueProvider</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Win32.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Win32.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Win32.Interop.Automation.IWindowProvider</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Win32.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Win32.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Win32.Interop.Automation.NavigateDirection</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Win32.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Win32.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Win32.Interop.Automation.ProviderOptions</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Win32.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Win32.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Win32.Interop.Automation.RowOrColumnMajor</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Win32.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Win32.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Win32.Interop.Automation.SupportedTextSelection</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Win32.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Win32.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Win32.Interop.Automation.SynchronizedInputType</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Win32.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Win32.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Win32.Interop.Automation.TextPatternRangeEndpoint</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Win32.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Win32.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Win32.Interop.Automation.TextUnit</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Win32.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Win32.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Win32.Interop.Automation.WindowInteractionState</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Win32.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Win32.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Win32.Interop.Automation.WindowVisualState</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Win32.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Win32.dll</Right>
-  </Suppression>
-</Suppressions>

+ 0 - 16
api/Avalonia.iOS.nupkg.xml

@@ -1,16 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- https://learn.microsoft.com/en-us/dotnet/fundamentals/package-validation/diagnostic-ids -->
-<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
-  <Suppression>
-    <DiagnosticId>CP0002</DiagnosticId>
-    <Target>M:Avalonia.iOS.UIViewControlHandle.get_HandleDescriptor</Target>
-    <Left>baseline/net8.0-tvos17.0/Avalonia.iOS.dll</Left>
-    <Right>target/net8.0-tvos17.0/Avalonia.iOS.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0007</DiagnosticId>
-    <Target>T:Avalonia.iOS.UIViewControlHandle</Target>
-    <Left>baseline/net8.0-tvos17.0/Avalonia.iOS.dll</Left>
-    <Right>target/net8.0-tvos17.0/Avalonia.iOS.dll</Right>
-  </Suppression>
-</Suppressions>

+ 0 - 268
api/Avalonia.nupkg.xml

@@ -1,268 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- https://learn.microsoft.com/en-us/dotnet/fundamentals/package-validation/diagnostic-ids -->
-<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Controls.PseudolassesExtensions</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Data.Core.CastTypePropertyPathElement</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Data.Core.ChildTraversalPropertyPathElement</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Data.Core.EnsureTypePropertyPathElement</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Data.Core.IPropertyPathElement</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Data.Core.PropertyPath</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Data.Core.PropertyPathBuilder</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Data.Core.PropertyPropertyPathElement</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Utilities.CharacterReader</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Utilities.IdentifierParser</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Utilities.KeywordParser</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0001</DiagnosticId>
-    <Target>T:Avalonia.Utilities.StyleClassParser</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0002</DiagnosticId>
-    <Target>M:Avalonia.Diagnostics.AppliedStyle.get_HasActivator</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0002</DiagnosticId>
-    <Target>M:Avalonia.Diagnostics.AppliedStyle.get_IsActive</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0002</DiagnosticId>
-    <Target>M:Avalonia.Diagnostics.AppliedStyle.get_Style</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0002</DiagnosticId>
-    <Target>M:Avalonia.Diagnostics.StyledElementExtensions.GetStyleDiagnostics(Avalonia.StyledElement)</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0002</DiagnosticId>
-    <Target>M:Avalonia.Diagnostics.StyleDiagnostics.#ctor(System.Collections.Generic.IReadOnlyList{Avalonia.Diagnostics.AppliedStyle})</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0002</DiagnosticId>
-    <Target>M:Avalonia.Diagnostics.StyleDiagnostics.get_AppliedStyles</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0002</DiagnosticId>
-    <Target>M:Avalonia.Threading.DispatcherPriorityAwaitable.get_IsCompleted</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0002</DiagnosticId>
-    <Target>M:Avalonia.Threading.DispatcherPriorityAwaitable.GetAwaiter</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0002</DiagnosticId>
-    <Target>M:Avalonia.Threading.DispatcherPriorityAwaitable.GetResult</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0002</DiagnosticId>
-    <Target>M:Avalonia.Threading.DispatcherPriorityAwaitable.OnCompleted(System.Action)</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0002</DiagnosticId>
-    <Target>M:Avalonia.Threading.DispatcherPriorityAwaitable`1.GetAwaiter</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0002</DiagnosticId>
-    <Target>M:Avalonia.Threading.DispatcherPriorityAwaitable`1.GetResult</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0002</DiagnosticId>
-    <Target>M:Avalonia.Controls.Primitives.IPopupHost.ConfigurePosition(Avalonia.Visual,Avalonia.Controls.PlacementMode,Avalonia.Point,Avalonia.Controls.Primitives.PopupPositioning.PopupAnchor,Avalonia.Controls.Primitives.PopupPositioning.PopupGravity,Avalonia.Controls.Primitives.PopupPositioning.PopupPositionerConstraintAdjustment,System.Nullable{Avalonia.Rect})</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Controls.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Controls.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0002</DiagnosticId>
-    <Target>M:Avalonia.Controls.Screens.#ctor(Avalonia.Platform.IScreenImpl)</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Controls.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Controls.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0006</DiagnosticId>
-    <Target>M:Avalonia.Input.Platform.IClipboard.FlushAsync</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0006</DiagnosticId>
-    <Target>M:Avalonia.Input.Platform.IClipboard.TryGetInProcessDataObjectAsync</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0006</DiagnosticId>
-    <Target>M:Avalonia.Platform.Storage.IStorageFolder.GetFileAsync(System.String)</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0006</DiagnosticId>
-    <Target>M:Avalonia.Platform.Storage.IStorageFolder.GetFolderAsync(System.String)</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0006</DiagnosticId>
-    <Target>M:Avalonia.Controls.Notifications.IManagedNotificationManager.Close(System.Object)</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Controls.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Controls.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0006</DiagnosticId>
-    <Target>M:Avalonia.Controls.Notifications.INotificationManager.Close(Avalonia.Controls.Notifications.INotification)</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Controls.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Controls.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0006</DiagnosticId>
-    <Target>M:Avalonia.Controls.Notifications.INotificationManager.CloseAll</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Controls.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Controls.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0006</DiagnosticId>
-    <Target>M:Avalonia.Controls.Primitives.IPopupHost.ConfigurePosition(Avalonia.Controls.Primitives.PopupPositioning.PopupPositionRequest)</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Controls.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Controls.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0006</DiagnosticId>
-    <Target>M:Avalonia.Controls.Primitives.IPopupHost.TakeFocus</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Controls.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Controls.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0006</DiagnosticId>
-    <Target>P:Avalonia.Controls.Platform.IInsetsManager.DisplayEdgeToEdgePreference</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Controls.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Controls.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0006</DiagnosticId>
-    <Target>P:Avalonia.Controls.Platform.IInsetsManager.DisplaysEdgeToEdge</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Controls.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Controls.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0007</DiagnosticId>
-    <Target>T:Avalonia.Threading.DispatcherPriorityAwaitable</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0007</DiagnosticId>
-    <Target>T:Avalonia.Threading.DispatcherPriorityAwaitable`1</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0008</DiagnosticId>
-    <Target>T:Avalonia.Threading.DispatcherPriorityAwaitable</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0008</DiagnosticId>
-    <Target>T:Avalonia.Threading.DispatcherPriorityAwaitable`1</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0009</DiagnosticId>
-    <Target>T:Avalonia.Diagnostics.StyleDiagnostics</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0009</DiagnosticId>
-    <Target>T:Avalonia.Controls.Screens</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Controls.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Controls.dll</Right>
-  </Suppression>
-  <Suppression>
-    <DiagnosticId>CP0012</DiagnosticId>
-    <Target>M:Avalonia.Controls.Button.OnAccessKey(Avalonia.Interactivity.RoutedEventArgs)</Target>
-    <Left>baseline/netstandard2.0/Avalonia.Controls.dll</Left>
-    <Right>target/netstandard2.0/Avalonia.Controls.dll</Right>
-  </Suppression>
-</Suppressions>

+ 0 - 1
build/SharedVersion.props

@@ -3,7 +3,6 @@
   <PropertyGroup>
   <PropertyGroup>
     <Product>Avalonia</Product>
     <Product>Avalonia</Product>
     <Version>12.0.999</Version>
     <Version>12.0.999</Version>
-    <ApiCompatVersion>11.1.0</ApiCompatVersion>
     <Authors>Avalonia Team</Authors>
     <Authors>Avalonia Team</Authors>
     <Copyright>Copyright 2013-$([System.DateTime]::Now.ToString(`yyyy`)) &#169; The AvaloniaUI Project</Copyright>
     <Copyright>Copyright 2013-$([System.DateTime]::Now.ToString(`yyyy`)) &#169; The AvaloniaUI Project</Copyright>
     <PackageProjectUrl>https://avaloniaui.net/?utm_source=nuget&amp;utm_medium=referral&amp;utm_content=project_homepage_link</PackageProjectUrl>
     <PackageProjectUrl>https://avaloniaui.net/?utm_source=nuget&amp;utm_medium=referral&amp;utm_content=project_homepage_link</PackageProjectUrl>

+ 384 - 233
nukebuild/ApiDiffHelper.cs

@@ -1,313 +1,464 @@
+#nullable enable
+
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
-using System.Diagnostics;
+using System.Collections.Immutable;
 using System.IO;
 using System.IO;
 using System.IO.Compression;
 using System.IO.Compression;
 using System.Linq;
 using System.Linq;
-using System.Net;
-using System.Net.Http;
-using System.Text.RegularExpressions;
+using System.Security.Cryptography;
+using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
+using NuGet.Common;
+using NuGet.Configuration;
+using NuGet.Frameworks;
+using NuGet.Packaging;
+using NuGet.Protocol;
+using NuGet.Protocol.Core.Types;
+using NuGet.Versioning;
+using Nuke.Common.IO;
 using Nuke.Common.Tooling;
 using Nuke.Common.Tooling;
-using Serilog;
 using static Serilog.Log;
 using static Serilog.Log;
 
 
 public static class ApiDiffHelper
 public static class ApiDiffHelper
 {
 {
-    static readonly HttpClient s_httpClient = new();
-
-    public static async Task GetDiff(
-        Tool apiDiffTool, string outputFolder,
-        string packagePath, string baselineVersion)
+    const string NightlyFeedUri = "https://nuget-feed-nightly.avaloniaui.net/v3/index.json";
+    const string MainPackageName = "Avalonia";
+    const string FolderLib = "lib";
+
+    public static void ValidatePackage(
+        Tool apiCompatTool,
+        PackageDiffInfo packageDiff,
+        AbsolutePath suppressionFilesFolderPath,
+        bool updateSuppressionFile)
     {
     {
-        await using var baselineStream = await DownloadBaselinePackage(packagePath, baselineVersion);
-        if (baselineStream == null)
-            return;
+        Information("Validating API for package {Id}", packageDiff.PackageId);
 
 
-        if (!Directory.Exists(outputFolder))
-        {
-            Directory.CreateDirectory(outputFolder!);
-        }
+        Directory.CreateDirectory(suppressionFilesFolderPath);
 
 
-        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 suppressionArgs = "";
+
+        var suppressionFile = suppressionFilesFolderPath / (packageDiff.PackageId + ".nupkg.xml");
+        if (suppressionFile.FileExists())
+            suppressionArgs += $""" --suppression-file="{suppressionFile}" --permit-unnecessary-suppressions """;
 
 
-            var pairs = new List<(string baseline, string target)>();
+        if (updateSuppressionFile)
+            suppressionArgs += $""" --suppression-output-file="{suppressionFile}" --generate-suppression-file --preserve-unnecessary-suppressions """;
 
 
-            var packageId = GetPackageId(packagePath);
+        var allErrors = new List<string>();
 
 
-            // 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)
+        Parallel.ForEach(
+            packageDiff.Frameworks,
+            framework =>
             {
             {
-                var baselineDllPath = await ExtractDll("baseline", baselineDll, tempFolder);
+                var args = $""" -l="{framework.BaselineFolderPath}" -r="{framework.CurrentFolderPath}" {suppressionArgs}""";
 
 
-                var targetTfm = baselineDll.target;
-                var targetDll = targetDlls.FirstOrDefault(e =>
-                    e.target.StartsWith(targetTfm) && e.entry.Name == baselineDll.entry.Name);
-                if (targetDll is null)
-                {
-                    if (s_tfmRedirects.FirstOrDefault(t => baselineDll.target.StartsWith(t.oldTfm) && (t.package is null || packageId == t.package)).newTfm is {} newTfm)
-                    {
-                        targetTfm = newTfm;
-                        targetDll = targetDlls.FirstOrDefault(e =>
-                            e.target.StartsWith(targetTfm) && e.entry.Name == baselineDll.entry.Name);
-                    }
-                }
+                var localErrors = GetErrors(apiCompatTool(args));
 
 
-                if (targetDll?.entry is null)
+                if (localErrors.Length > 0)
                 {
                 {
-                    throw new InvalidOperationException($"Some assemblies are missing in the new package {packageId}: {baselineDll.entry.Name} for {baselineDll.target}");
+                    lock (allErrors)
+                        allErrors.AddRange(localErrors);
                 }
                 }
+            });
 
 
-                var targetDllPath = await ExtractDll("target", targetDll, tempFolder);
+        ThrowOnErrors(allErrors, packageDiff.PackageId, "ValidateApiDiff");
+    }
 
 
-                pairs.Add((baselineDllPath, targetDllPath));
-            }
+    public static void GenerateMarkdownDiff(
+        Tool apiDiffTool,
+        PackageDiffInfo packageDiff,
+        AbsolutePath rootOutputFolderPath,
+        string baselineDisplay,
+        string currentDisplay)
+    {
+        Information("Creating markdown diff for package {Id}", packageDiff.PackageId);
 
 
-            await Task.WhenAll(pairs.Select(p => Task.Run(() =>
-            {
-                var baselineApi = p.baseline + Random.Shared.Next() + ".api.cs";
-                var targetApi = p.target + Random.Shared.Next() + ".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())
+        var packageOutputFolderPath = rootOutputFolderPath / packageDiff.PackageId;
+        Directory.CreateDirectory(packageOutputFolderPath);
+
+        // Not specifying -eattrs incorrectly tries to load AttributesToExclude.txt, create an empty file instead.
+        // See https://github.com/dotnet/sdk/issues/49719
+        var excludedAttributesFilePath = (AbsolutePath)Path.Join(Path.GetTempPath(), Guid.NewGuid().ToString());
+        File.WriteAllBytes(excludedAttributesFilePath!, []);
+
+        try
+        {
+            var allErrors = new List<string>();
+
+            // The API diff tool is unbelievably slow, process in parallel.
+            Parallel.ForEach(
+                packageDiff.Frameworks,
+                framework =>
                 {
                 {
-                    gitProcess.StartInfo = new ProcessStartInfo
+                    var frameworkOutputFolderPath = packageOutputFolderPath / framework.Framework.GetShortFolderName();
+                    var args = $""" -b="{framework.BaselineFolderPath}" -bfn="{baselineDisplay}" -a="{framework.CurrentFolderPath}" -afn="{currentDisplay}" -o="{frameworkOutputFolderPath}" -eattrs="{excludedAttributesFilePath}" """;
+
+                    var localErrors = GetErrors(apiDiffTool(args));
+
+                    if (localErrors.Length > 0)
                     {
                     {
-                        CreateNoWindow = true,
-                        RedirectStandardError = false,
-                        RedirectStandardOutput = false,
-                        FileName = "git",
-                        Arguments = args,
-                        WorkingDirectory = tempFolder
-                    };
-                    gitProcess.Start();
-                    gitProcess.WaitForExit();
-                }
+                        lock (allErrors)
+                            allErrors.AddRange(localErrors);
+                    }
+                });
 
 
-                var resultFile = new FileInfo(Path.Combine(tempFolder, resultDiff));
-                if (resultFile.Length > 0)
-                {
-                    resultFile.CopyTo(Path.Combine(outputFolder, Path.GetFileName(resultDiff)), true);
-                }
-            })));
+            ThrowOnErrors(allErrors, packageDiff.PackageId, "OutputApiDiff");
+
+            MergeFrameworkMarkdownDiffFiles(
+                rootOutputFolderPath,
+                packageOutputFolderPath,
+                [..packageDiff.Frameworks.Select(info => info.Framework)]);
+
+            Directory.Delete(packageOutputFolderPath, true);
+        }
+        finally
+        {
+            File.Delete(excludedAttributesFilePath);
         }
         }
     }
     }
 
 
-    private static readonly (string package, string oldTfm, string newTfm)[] s_tfmRedirects = new[]
-    {
-        // We use StartsWith below comparing these tfm, as we ignore platform versions (like, net6.0-ios16.1).
-        ("Avalonia.Android", "net6.0-android", "net8.0-android"),
-        ("Avalonia.iOS", "net6.0-ios", "net8.0-ios"),
-        // Browser was changed from net7.0 to net8.0-browser. 
-        ("Avalonia.Browser", "net7.0", "net8.0-browser"),
-        ("Avalonia.Browser.Blazor", "net7.0", "net8.0-browser"),
-        // Designer was moved from netcoreapp to netstandard.
-        ("Avalonia", "netcoreapp2.0", "netstandard2.0"),
-        ("Avalonia", "net461", "netstandard2.0")
-    };
-
-    public static async Task ValidatePackage(
-        Tool apiCompatTool, string packagePath, string baselineVersion,
-        string suppressionFilesFolder, bool updateSuppressionFile)
+    static void MergeFrameworkMarkdownDiffFiles(
+        AbsolutePath rootOutputFolderPath,
+        AbsolutePath packageOutputFolderPath,
+        ImmutableArray<NuGetFramework> frameworks)
     {
     {
-        if (!Directory.Exists(suppressionFilesFolder))
+        // At this point, the hierarchy looks like:
+        //   markdown/
+        //   ├─ net8.0/
+        //   │  ├─ api_diff_Avalonia.md
+        //   │  ├─ api_diff_Avalonia.Controls.md
+        //   ├─ netstandard2.0/
+        //   │  ├─ api_diff_Avalonia.md
+        //   │  ├─ api_diff_Avalonia.Controls.md
+        //
+        // We want one file per assembly: merge all files with the same name.
+        // However, it's very likely that the diff is the same for several frameworks: in this case, keep only one file.
+
+        var assemblyGroups = frameworks
+            .SelectMany(GetFrameworkDiffFiles, (framework, filePath) => (framework, filePath))
+            .GroupBy(x => x.filePath.Name)
+            .OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase);
+
+        foreach (var assemblyGroup in assemblyGroups)
         {
         {
-            Directory.CreateDirectory(suppressionFilesFolder!);
+            using var writer = File.CreateText(rootOutputFolderPath / assemblyGroup.Key.Replace("api_diff_", ""));
+            var addSeparator = false;
+
+            foreach (var similarDiffGroup in assemblyGroup.GroupBy(x => HashFile(x.filePath), ByteArrayEqualityComparer.Instance))
+            {
+                if (addSeparator)
+                    writer.WriteLine();
+
+                using var reader = File.OpenText(similarDiffGroup.First().filePath);
+                var firstLine = reader.ReadLine();
+
+                writer.Write(firstLine);
+                writer.WriteLine(" (" + string.Join(", ", similarDiffGroup.Select(x => x.framework.GetShortFolderName())) + ")");
+
+                while (reader.ReadLine() is { } line)
+                    writer.WriteLine(line);
+
+                addSeparator = true;
+            }
+        }
+
+        AbsolutePath[] GetFrameworkDiffFiles(NuGetFramework framework)
+        {
+            var frameworkFolderPath = packageOutputFolderPath / framework.GetShortFolderName();
+            if (!frameworkFolderPath.DirectoryExists())
+                return [];
+
+            return Directory.GetFiles(frameworkFolderPath, "*.md")
+                .Where(filePath => Path.GetFileName(filePath) != "api_diff.md")
+                .Select(filePath => (AbsolutePath)filePath)
+                .ToArray();
         }
         }
 
 
-        await using var baselineStream = await DownloadBaselinePackage(packagePath, baselineVersion);
-        if (baselineStream == null) 
+        static byte[] HashFile(AbsolutePath filePath)
+        {
+            using var stream = File.OpenRead(filePath);
+            return SHA256.HashData(stream);
+        }
+    }
+
+    public static void MergePackageMarkdownDiffFiles(
+        AbsolutePath rootOutputFolderPath,
+        string baselineDisplay,
+        string currentDisplay)
+    {
+        const string mergedFileName = "_diff.md";
+
+        var filePaths = Directory.EnumerateFiles(rootOutputFolderPath, "*.md")
+            .Where(filePath => Path.GetFileName(filePath) != mergedFileName)
+            .Order(StringComparer.OrdinalIgnoreCase)
+            .ToArray();
+
+        using var writer = File.CreateText(rootOutputFolderPath / mergedFileName);
+
+        writer.WriteLine($"# API diff between {baselineDisplay} and {currentDisplay}");
+
+        if (filePaths.Length == 0)
+        {
+            writer.WriteLine();
+            writer.WriteLine("No changes.");
             return;
             return;
+        }
 
 
-        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))
+        foreach (var filePath in filePaths)
         {
         {
-            var targetDlls = GetDlls(target);
-            var baselineDlls = GetDlls(baseline);
+            writer.WriteLine();
+
+            using var reader = File.OpenText(filePath);
+
+            while (reader.ReadLine() is { } line)
+            {
+                if (line.StartsWith('#'))
+                    writer.Write('#');
+
+                writer.WriteLine(line);
+            }
+        }
+    }
 
 
-            var left = new List<string>();
-            var right = new List<string>();
+    static string[] GetErrors(IEnumerable<Output> outputs)
+        => outputs
+            .Where(output => output.Type == OutputType.Err)
+            .Select(output => output.Text)
+            .ToArray();
 
 
-            var packageId = GetPackageId(packagePath);
-            var suppressionFile = Path.Combine(suppressionFilesFolder, packageId + ".nupkg.xml");
+    static void ThrowOnErrors(List<string> errors, string packageId, string taskName)
+    {
+        if (errors.Count > 0)
+        {
+            throw new AggregateException(
+                $"{taskName} task has failed for \"{packageId}\" package",
+                errors.Select(error => new Exception(error)));
+        }
+    }
 
 
-            // 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)
+    public static async Task<GlobalDiffInfo> DownloadAndExtractPackagesAsync(
+        IEnumerable<AbsolutePath> currentPackagePaths,
+        NuGetVersion currentVersion,
+        bool isReleaseBranch,
+        AbsolutePath outputFolderPath,
+        NuGetVersion? forcedBaselineVersion)
+    {
+        var downloadContext = await CreateNuGetDownloadContextAsync();
+        var baselineVersion = forcedBaselineVersion ??
+                              await GetBaselineVersionAsync(downloadContext, currentVersion, isReleaseBranch);
+
+        Information("API baseline version is {Baseline} for current version {Current}", baselineVersion, currentVersion);
+
+        var memoryStream = new MemoryStream();
+        var packageDiffs = ImmutableArray.CreateBuilder<PackageDiffInfo>();
+
+        foreach (var packagePath in currentPackagePaths)
+        {
+            string packageId;
+            AbsolutePath currentFolderPath;
+            AbsolutePath baselineFolderPath;
+            Dictionary<NuGetFramework, string> currentFolderNames;
+            Dictionary<NuGetFramework, string> baselineFolderNames;
+
+            // Extract current package
+            using (var currentArchive = new ZipArchive(File.OpenRead(packagePath), ZipArchiveMode.Read, leaveOpen: false))
             {
             {
-                var baselineDllPath = await ExtractDll("baseline", baselineDll, tempFolder);
+                using var packageReader = new PackageArchiveReader(currentArchive);
+                packageId = packageReader.NuspecReader.GetId();
+                currentFolderPath = outputFolderPath / "current" / packageId;
+                currentFolderNames = ExtractDiffableAssembliesFromPackage(currentArchive, currentFolderPath);
+            }
 
 
-                var targetTfm = baselineDll.target;
-                var targetDll = targetDlls.FirstOrDefault(e =>
-                    e.target.StartsWith(targetTfm) && e.entry.Name == baselineDll.entry.Name);
-                if (targetDll?.entry is null)
-                {
-                    if (s_tfmRedirects.FirstOrDefault(t => baselineDll.target.StartsWith(t.oldTfm) && (t.package is null || packageId == t.package)).newTfm is {} newTfm)
-                    {
-                        targetTfm = newTfm;
-                        targetDll = targetDlls.FirstOrDefault(e =>
-                            e.target.StartsWith(targetTfm) && e.entry.Name == baselineDll.entry.Name);
-                    }
-                }
-                if (targetDll?.entry is null && targetDlls.Count == 1)
-                {
-                    targetDll = targetDlls.First();
-                    Warning(
-                        $"Some assemblies are missing in the new package {packageId}: {baselineDll.entry.Name} for {baselineDll.target}." +
-                        $"Resolved: {targetDll.target} ({targetDll.entry.Name})");
-                }
+            // Download baseline package
+            memoryStream.Position = 0L;
+            memoryStream.SetLength(0L);
+            await DownloadBaselinePackageAsync(memoryStream, downloadContext, packageId, baselineVersion);
+            memoryStream.Position = 0L;
 
 
-                if (targetDll?.entry is null)
-                {
-                    if (packageId == "Avalonia"
-                        && baselineDll.target is "net461" or "netcoreapp2.0")
-                    {
-                        // In 11.1 we have removed net461 and netcoreapp2.0 targets from Avalonia package.
-                        continue;
-                    }
-                    
-                    var actualTargets = string.Join(", ",
-                        targetDlls.Select(d => $"{d.target} ({d.entry.Name})"));
-                    throw new InvalidOperationException(
-                        $"Some assemblies are missing in the new package {packageId}: {baselineDll.entry.Name} for {baselineDll.target}."
-                        + $"\r\nActual targets: {actualTargets}.");
-                }
+            // Extract baseline package
+            using (var baselineArchive = new ZipArchive(memoryStream, ZipArchiveMode.Read, leaveOpen: true))
+            {
+                baselineFolderPath = outputFolderPath / "baseline" / packageId;
+                baselineFolderNames = ExtractDiffableAssembliesFromPackage(baselineArchive, baselineFolderPath);
+            }
+
+            if (currentFolderNames.Count == 0 && baselineFolderNames.Count == 0)
+                continue;
 
 
-                var targetDllPath = await ExtractDll("target", targetDll, tempFolder);
+            var frameworkDiffs = new List<FrameworkDiffInfo>();
 
 
-                left.Add(baselineDllPath);
-                right.Add(targetDllPath);
+            // Handle frameworks that exist only in the current package.
+            foreach (var framework in currentFolderNames.Keys.Except(baselineFolderNames.Keys))
+            {
+                var folderName = currentFolderNames[framework];
+                Directory.CreateDirectory(baselineFolderPath / folderName);
+                baselineFolderNames.Add(framework, folderName);
             }
             }
 
 
-            if (left.Any())
+            // Handle frameworks that exist only for the baseline package.
+            foreach (var framework in baselineFolderNames.Keys.Except(currentFolderNames.Keys))
             {
             {
-                var args = $""" -l={string.Join(',', left)} -r="{string.Join(',', right)}" """;
-                if (File.Exists(suppressionFile))
-                {
-                    args += $""" --suppression-file="{suppressionFile}" """;
-                }
+                var folderName = baselineFolderNames[framework];
+                Directory.CreateDirectory(currentFolderPath / folderName);
+                currentFolderNames.Add(framework, folderName);
+            }
 
 
-                if (updateSuppressionFile)
-                {
-                    args += $""" --suppression-output-file="{suppressionFile}" --generate-suppression-file=true """;
-                }
+            foreach (var (framework, currentFolderName) in currentFolderNames)
+            {
+                var baselineFolderName = baselineFolderNames[framework];
 
 
-                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)));
-                }
+                frameworkDiffs.Add(new FrameworkDiffInfo(
+                    framework,
+                    baselineFolderPath / FolderLib / baselineFolderName,
+                    currentFolderPath / FolderLib / currentFolderName));
             }
             }
+
+            packageDiffs.Add(new PackageDiffInfo(packageId, [..frameworkDiffs]));
         }
         }
+
+        return new GlobalDiffInfo(baselineVersion, currentVersion, packageDiffs.DrainToImmutable());
     }
     }
 
 
-    record DllEntry(string target, ZipArchiveEntry entry);
-    
-    static IReadOnlyCollection<DllEntry> GetDlls(ZipArchive archive)
+    static async Task<NuGetDownloadContext> CreateNuGetDownloadContextAsync()
     {
     {
-        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.FullName.Contains("analyzers\\")
-                && !e.Name.Contains("Avalonia.Build.Tasks"))
-            .Select(e => (
-                entry: e,
-                isRef: e.FullName.Contains("ref/") || e.FullName.Contains("ref\\"),
-                target: Path.GetDirectoryName(e.FullName)!.Split(new [] { '/', '\\' }).Last())
-            )
-            .GroupBy(e => (e.target, e.entry.Name))
-            .Select(g => g.MaxBy(e => e.isRef))
-            .Select(e => new DllEntry(e.target, e.entry))
-            .ToArray();
+        var packageSource = new PackageSource(NightlyFeedUri) { ProtocolVersion = 3 };
+        var repository = Repository.Factory.GetCoreV3(packageSource);
+        var findPackageByIdResource = await repository.GetResourceAsync<FindPackageByIdResource>();
+        return new NuGetDownloadContext(packageSource, findPackageByIdResource);
     }
     }
 
 
-    static async Task<Stream> DownloadBaselinePackage(string packagePath, string baselineVersion)
+    /// <summary>
+    /// Finds the baseline version to diff against.
+    /// On release branches, use the latest stable version.
+    /// On the main branch and on PRs, use the latest nightly version.
+    /// This method assumes all packages share the same version.
+    /// </summary>
+    static async Task<NuGetVersion> GetBaselineVersionAsync(
+        NuGetDownloadContext context,
+        NuGetVersion currentVersion,
+        bool isReleaseBranch)
     {
     {
-        if (baselineVersion is null)
+        var versions = await context.FindPackageByIdResource.GetAllVersionsAsync(
+            MainPackageName,
+            context.CacheContext,
+            NullLogger.Instance,
+            CancellationToken.None);
+
+        versions = versions.Where(v => v < currentVersion);
+
+        if (isReleaseBranch)
+            versions = versions.Where(v => !v.IsPrerelease);
+
+        return versions.OrderDescending().FirstOrDefault()
+           ?? throw new InvalidOperationException(
+               $"Could not find a version less than {currentVersion} for package {MainPackageName} in source {context.PackageSource.Source}");
+    }
+
+    static async Task DownloadBaselinePackageAsync(
+        Stream destinationStream,
+        NuGetDownloadContext context,
+        string packageId,
+        NuGetVersion version)
+    {
+        Information("Downloading {Id} {Version} baseline package", packageId, version);
+
+        var downloaded = await context.FindPackageByIdResource.CopyNupkgToStreamAsync(
+            packageId,
+            version,
+            destinationStream,
+            context.CacheContext,
+            NullLogger.Instance,
+            CancellationToken.None);
+
+        if (!downloaded)
         {
         {
             throw new InvalidOperationException(
             throw new InvalidOperationException(
-                "Build \"api-baseline\" parameter must be set when running Nuke CreatePackages");
+                $"Could not download version {version} for package {packageId} in source {context.PackageSource.Source}");
         }
         }
+    }
 
 
-        /*
-         Gets package name from versions like:
-         Avalonia.0.10.0-preview1
-         Avalonia.11.0.999-cibuild0037534-beta
-         Avalonia.11.0.0
-         */
-        var packageId = GetPackageId(packagePath);
-        Information("Downloading {0} {1} baseline package", packageId, baselineVersion);
+    static Dictionary<NuGetFramework, string> ExtractDiffableAssembliesFromPackage(
+        ZipArchive packageArchive,
+        AbsolutePath destinationFolderPath)
+    {
+        var folderByFramework = new Dictionary<NuGetFramework, string>();
 
 
-        try
-        {
-            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 (HttpRequestException e) when (e.StatusCode == HttpStatusCode.NotFound)
+        foreach (var entry in packageArchive.Entries)
         {
         {
-            return null;
+            if (TryGetFrameworkFolderName(entry.FullName) is not { } folderName)
+                continue;
+
+            // Ignore platform versions: assume that e.g. net8.0-android34 and net8.0-android35 are the same for diff purposes.
+            var framework = WithoutPlatformVersion(NuGetFramework.ParseFolder(folderName));
+
+            if (folderByFramework.TryGetValue(framework, out var existingFolderName))
+            {
+                if (existingFolderName != folderName)
+                {
+                    throw new InvalidOperationException(
+                        $"Found two similar frameworks with different platform versions: {existingFolderName} and {folderName}");
+                }
+            }
+            else
+                folderByFramework.Add(framework, folderName);
+
+            var targetFilePath = destinationFolderPath / entry.FullName;
+            Directory.CreateDirectory(targetFilePath.Parent);
+            entry.ExtractToFile(targetFilePath, overwrite: true);
         }
         }
-        catch (Exception ex)
+
+        return folderByFramework;
+
+        static string? TryGetFrameworkFolderName(string entryPath)
         {
         {
-            throw new InvalidOperationException($"Downloading baseline package for {packageId} {baselineVersion} failed.\r" + ex.Message, ex);
+            if (!entryPath.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
+                return null;
+
+            var segments = entryPath.Split('/');
+            if (segments is not [FolderLib, var name, ..])
+                return null;
+
+            return name;
         }
         }
+
+        // e.g. net8.0-android34.0 to net8.0-android
+        static NuGetFramework WithoutPlatformVersion(NuGetFramework value)
+            => value.HasPlatform && value.PlatformVersion != FrameworkConstants.EmptyVersion ?
+                new NuGetFramework(value.Framework, value.Version, value.Platform, FrameworkConstants.EmptyVersion) :
+                value;
     }
     }
 
 
-    static async Task<string> ExtractDll(string basePath, DllEntry dllEntry, string targetFolder)
+    public sealed class GlobalDiffInfo(
+        NuGetVersion baselineVersion,
+        NuGetVersion currentVersion,
+        ImmutableArray<PackageDiffInfo> packages)
     {
     {
-        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);
-        }
+        public NuGetVersion BaselineVersion { get; } = baselineVersion;
+        public NuGetVersion CurrentVersion { get; } = currentVersion;
+        public ImmutableArray<PackageDiffInfo> Packages { get; } = packages;
+    }
 
 
-        return dllPath;
+    public sealed class PackageDiffInfo(string packageId, ImmutableArray<FrameworkDiffInfo> frameworks)
+    {
+        public string PackageId { get; } = packageId;
+        public ImmutableArray<FrameworkDiffInfo> Frameworks { get; } = frameworks;
     }
     }
 
 
-    static void GenerateApiListing(Tool apiDiffTool, string inputFile, string outputFile, string workingDif)
+    public sealed class FrameworkDiffInfo(
+        NuGetFramework framework,
+        AbsolutePath baselineFolderPath,
+        AbsolutePath currentFolderPath)
     {
     {
-        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)));
-        }
+        public NuGetFramework Framework { get; } = framework;
+        public AbsolutePath BaselineFolderPath { get; } = baselineFolderPath;
+        public AbsolutePath CurrentFolderPath { get; } = currentFolderPath;
     }
     }
 
 
-    static string GetPackageId(string packagePath)
+    sealed class NuGetDownloadContext(PackageSource packageSource, FindPackageByIdResource findPackageByIdResource)
     {
     {
-        return Regex.Replace(
-            Path.GetFileNameWithoutExtension(packagePath),
-            """(\.\d+\.\d+\.\d+(?:-.+)?)$""", "");
+        public SourceCacheContext CacheContext { get; } = new();
+        public PackageSource PackageSource { get; } = packageSource;
+        public FindPackageByIdResource FindPackageByIdResource { get; } = findPackageByIdResource;
     }
     }
 }
 }

+ 48 - 15
nukebuild/Build.cs

@@ -20,6 +20,7 @@ using static Nuke.Common.Tools.VSWhere.VSWhereTasks;
 using static Serilog.Log;
 using static Serilog.Log;
 using MicroCom.CodeGenerator;
 using MicroCom.CodeGenerator;
 using NuGet.Configuration;
 using NuGet.Configuration;
+using NuGet.Versioning;
 using Nuke.Common.CI.AzurePipelines;
 using Nuke.Common.CI.AzurePipelines;
 using Nuke.Common.IO;
 using Nuke.Common.IO;
 
 
@@ -35,11 +36,15 @@ partial class Build : NukeBuild
 {
 {
     BuildParameters Parameters { get; set; }
     BuildParameters Parameters { get; set; }
 
 
-    [PackageExecutable("Microsoft.DotNet.ApiCompat.Tool", "Microsoft.DotNet.ApiCompat.Tool.dll", Framework = "net6.0")]
+#nullable enable
+    ApiDiffHelper.GlobalDiffInfo? GlobalDiff { get; set; }
+#nullable restore
+
+    [PackageExecutable("Microsoft.DotNet.ApiCompat.Tool", "Microsoft.DotNet.ApiCompat.Tool.dll", Framework = "net8.0")]
     Tool ApiCompatTool;
     Tool ApiCompatTool;
     
     
-    [PackageExecutable("Microsoft.DotNet.GenAPI.Tool", "Microsoft.DotNet.GenAPI.Tool.dll", Framework = "net8.0")]
-    Tool ApiGenTool;
+    [PackageExecutable("Microsoft.DotNet.ApiDiff.Tool", "Microsoft.DotNet.ApiDiff.Tool.dll", Framework = "net8.0")]
+    Tool ApiDiffTool;
 
 
     [PackageExecutable("dotnet-ilrepack", "ILRepackTool.dll", Framework = "net8.0")]
     [PackageExecutable("dotnet-ilrepack", "ILRepackTool.dll", Framework = "net8.0")]
     Tool IlRepackTool;
     Tool IlRepackTool;
@@ -321,25 +326,53 @@ partial class Build : NukeBuild
                 Parameters.NugetRoot / $"Avalonia.{Parameters.Version}.nupkg",
                 Parameters.NugetRoot / $"Avalonia.{Parameters.Version}.nupkg",
                 Parameters.NugetRoot / $"Avalonia.{Parameters.Version}.snupkg");
                 Parameters.NugetRoot / $"Avalonia.{Parameters.Version}.snupkg");
         });
         });
-    
-    Target ValidateApiDiff => _ => _
+
+    Target DownloadApiBaselinePackages => _ => _
         .DependsOn(CreateNugetPackages)
         .DependsOn(CreateNugetPackages)
         .Executes(async () =>
         .Executes(async () =>
         {
         {
-            await Task.WhenAll(
-                Directory.GetFiles(Parameters.NugetRoot, "*.nupkg").Select(nugetPackage => ApiDiffHelper.ValidatePackage(
-                    ApiCompatTool, nugetPackage, Parameters.ApiValidationBaseline,
-                    Parameters.ApiValidationSuppressionFiles, Parameters.UpdateApiValidationSuppression)));
+            GlobalDiff = await ApiDiffHelper.DownloadAndExtractPackagesAsync(
+                Directory.EnumerateFiles(Parameters.NugetRoot, "*.nupkg").Select(path => (AbsolutePath)path),
+                NuGetVersion.Parse(Parameters.Version),
+                Parameters.IsReleaseBranch,
+                Parameters.ArtifactsDir / "api-diff" / "assemblies",
+                Parameters.ForceApiValidationBaseline is { } forcedBaseline ? NuGetVersion.Parse(forcedBaseline) : null);
+        });
+
+    Target ValidateApiDiff => _ => _
+        .DependsOn(DownloadApiBaselinePackages)
+        .Executes(() =>
+        {
+            var globalDiff = GlobalDiff!;
+
+            Parallel.ForEach(
+                globalDiff.Packages,
+                packageDiff => ApiDiffHelper.ValidatePackage(
+                    ApiCompatTool,
+                    packageDiff,
+                    Parameters.ApiValidationSuppressionFiles,
+                    Parameters.UpdateApiValidationSuppression));
         });
         });
     
     
     Target OutputApiDiff => _ => _
     Target OutputApiDiff => _ => _
-        .DependsOn(CreateNugetPackages)
-        .Executes(async () =>
+        .DependsOn(DownloadApiBaselinePackages)
+        .Executes(() =>
         {
         {
-            await Task.WhenAll(
-                Directory.GetFiles(Parameters.NugetRoot, "*.nupkg").Select(nugetPackage => ApiDiffHelper.GetDiff(
-                    ApiGenTool, RootDirectory / "api" / "diff",
-                    nugetPackage, Parameters.ApiValidationBaseline)));
+            var globalDiff = GlobalDiff!;
+            var outputFolderPath = Parameters.ArtifactsDir / "api-diff" / "markdown";
+            var baselineDisplay = globalDiff.BaselineVersion.ToString();
+            var currentDisplay = globalDiff.CurrentVersion.ToString();
+
+            Parallel.ForEach(
+                globalDiff.Packages,
+                packageDiff => ApiDiffHelper.GenerateMarkdownDiff(
+                    ApiDiffTool,
+                    packageDiff,
+                    outputFolderPath,
+                    baselineDisplay,
+                    currentDisplay));
+
+            ApiDiffHelper.MergePackageMarkdownDiffFiles(outputFolderPath, baselineDisplay, currentDisplay);
         });
         });
     
     
     Target RunTests => _ => _
     Target RunTests => _ => _

+ 18 - 18
nukebuild/BuildParameters.cs

@@ -1,3 +1,5 @@
+#nullable enable
+
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
@@ -12,25 +14,25 @@ using static Nuke.Common.IO.PathConstruction;
 public partial class Build
 public partial class Build
 {
 {
     [Parameter(Name = "configuration")]
     [Parameter(Name = "configuration")]
-    public string Configuration { get; set; }
+    public string? Configuration { get; set; }
 
 
     [Parameter(Name = "skip-tests")]
     [Parameter(Name = "skip-tests")]
     public bool SkipTests { get; set; }
     public bool SkipTests { get; set; }
 
 
     [Parameter(Name = "force-nuget-version")]
     [Parameter(Name = "force-nuget-version")]
-    public string ForceNugetVersion { get; set; }
+    public string? ForceNugetVersion { get; set; }
 
 
     [Parameter(Name = "skip-previewer")]
     [Parameter(Name = "skip-previewer")]
     public bool SkipPreviewer { get; set; }
     public bool SkipPreviewer { get; set; }
 
 
-    [Parameter(Name = "api-baseline")]
-    public string ApiValidationBaseline { get; set; }
+    [Parameter(Name = "force-api-baseline")]
+    public string? ForceApiValidationBaseline { get; set; }
 
 
     [Parameter(Name = "update-api-suppression")]
     [Parameter(Name = "update-api-suppression")]
     public bool? UpdateApiValidationSuppression { get; set; }
     public bool? UpdateApiValidationSuppression { get; set; }
 
 
     [Parameter(Name = "version-output-dir")]
     [Parameter(Name = "version-output-dir")]
-    public AbsolutePath VersionOutputDir { get; set; }
+    public AbsolutePath? VersionOutputDir { get; set; }
 
 
     public class BuildParameters
     public class BuildParameters
     {
     {
@@ -39,8 +41,8 @@ public partial class Build
         public bool SkipPreviewer {get;}
         public bool SkipPreviewer {get;}
         public string MainRepo { get; }
         public string MainRepo { get; }
         public string MasterBranch { get; }
         public string MasterBranch { get; }
-        public string RepositoryName { get; }
-        public string RepositoryBranch { get; }
+        public string? RepositoryName { get; }
+        public string? RepositoryBranch { get; }
         public string ReleaseConfiguration { get; }
         public string ReleaseConfiguration { get; }
         public Regex ReleaseBranchRegex { get; }
         public Regex ReleaseBranchRegex { get; }
         public string MSBuildSolution { get; }
         public string MSBuildSolution { get; }
@@ -70,10 +72,10 @@ public partial class Build
         public string FileZipSuffix { get; }
         public string FileZipSuffix { get; }
         public AbsolutePath ZipCoreArtifacts { get; }
         public AbsolutePath ZipCoreArtifacts { get; }
         public AbsolutePath ZipNuGetArtifacts { get; }
         public AbsolutePath ZipNuGetArtifacts { get; }
-        public string ApiValidationBaseline { get; }
+        public string? ForceApiValidationBaseline { get; }
         public bool UpdateApiValidationSuppression { get; }
         public bool UpdateApiValidationSuppression { get; }
         public AbsolutePath ApiValidationSuppressionFiles { get; }
         public AbsolutePath ApiValidationSuppressionFiles { get; }
-        public AbsolutePath VersionOutputDir { get; }
+        public AbsolutePath? VersionOutputDir { get; }
 
 
         public BuildParameters(Build b, bool isPackingToLocalCache)
         public BuildParameters(Build b, bool isPackingToLocalCache)
         {
         {
@@ -115,10 +117,9 @@ public partial class Build
             IsNuGetRelease = IsMainRepo && IsReleasable && IsReleaseBranch;
             IsNuGetRelease = IsMainRepo && IsReleasable && IsReleaseBranch;
 
 
             // VERSION
             // VERSION
-            var (propsVersion, propsApiCompatVersion) = GetVersion();
-            Version = b.ForceNugetVersion ?? propsVersion;
+            Version = b.ForceNugetVersion ?? GetVersion();
 
 
-            ApiValidationBaseline = b.ApiValidationBaseline ?? propsApiCompatVersion;
+            ForceApiValidationBaseline = b.ForceApiValidationBaseline;
             UpdateApiValidationSuppression = b.UpdateApiValidationSuppression ?? IsLocalBuild;
             UpdateApiValidationSuppression = b.UpdateApiValidationSuppression ?? IsLocalBuild;
             
             
             if (IsRunningOnAzure)
             if (IsRunningOnAzure)
@@ -126,7 +127,9 @@ public partial class Build
                 if (!IsNuGetRelease)
                 if (!IsNuGetRelease)
                 {
                 {
                     // Use AssemblyVersion with Build as version
                     // Use AssemblyVersion with Build as version
-                    Version += "-cibuild" + int.Parse(Environment.GetEnvironmentVariable("BUILD_BUILDID")).ToString("0000000") + "-alpha";
+                    var buildId = Environment.GetEnvironmentVariable("BUILD_BUILDID") ??
+                                  throw new InvalidOperationException("Missing environment variable BUILD_BUILDID");
+                    Version += "-cibuild" + int.Parse(buildId).ToString("0000000") + "-alpha";
                 }
                 }
 
 
                 PublishTestResults = true;
                 PublishTestResults = true;
@@ -157,13 +160,10 @@ public partial class Build
             VersionOutputDir = b.VersionOutputDir;
             VersionOutputDir = b.VersionOutputDir;
         }
         }
 
 
-        (string Version, string ApiCompatVersion) GetVersion()
+        string GetVersion()
         {
         {
             var xdoc = XDocument.Load(RootDirectory / "build/SharedVersion.props");
             var xdoc = XDocument.Load(RootDirectory / "build/SharedVersion.props");
-            return (
-                xdoc.Descendants().First(x => x.Name.LocalName == "Version").Value,
-                xdoc.Descendants().First(x => x.Name.LocalName == "ApiCompatVersion").Value
-            );
+            return xdoc.Descendants().First(x => x.Name.LocalName == "Version").Value;
         }
         }
     }
     }
 
 

+ 25 - 0
nukebuild/ByteArrayEqualityComparer.cs

@@ -0,0 +1,25 @@
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+
+public sealed class ByteArrayEqualityComparer : IEqualityComparer<byte[]>
+{
+    public static ByteArrayEqualityComparer Instance { get; } = new();
+
+    public bool Equals(byte[]? x, byte[]? y) {
+        if (ReferenceEquals(x, y))
+            return true;
+        if (x is null || y is null)
+            return false;
+
+        return x.AsSpan().SequenceEqual(y.AsSpan());
+    }
+
+    public int GetHashCode(byte[]? obj)
+    {
+        var hashCode = new HashCode();
+        hashCode.AddBytes(obj.AsSpan());
+        return hashCode.ToHashCode();
+    }
+}

+ 5 - 4
nukebuild/_build.csproj

@@ -9,8 +9,8 @@
     <TargetFramework>$(AvsCurrentTargetFramework)</TargetFramework>
     <TargetFramework>$(AvsCurrentTargetFramework)</TargetFramework>
     <!-- See https://github.com/nuke-build/nuke/issues/818 -->
     <!-- See https://github.com/nuke-build/nuke/issues/818 -->
     <EnableUnsafeBinaryFormatterSerialization>true</EnableUnsafeBinaryFormatterSerialization>
     <EnableUnsafeBinaryFormatterSerialization>true</EnableUnsafeBinaryFormatterSerialization>
-    <!-- Necessary for Microsoft.DotNet.GenAPI.Tool -->
-    <RestoreAdditionalProjectSources>https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet8-transport/nuget/v3/index.json</RestoreAdditionalProjectSources>
+    <!-- Necessary for Microsoft.DotNet.ApiDiff.Tool -->
+    <RestoreAdditionalProjectSources>https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet10-transport/nuget/v3/index.json</RestoreAdditionalProjectSources>
   </PropertyGroup>
   </PropertyGroup>
 
 
   <Import Project="..\build\JetBrains.dotMemoryUnit.props" />
   <Import Project="..\build\JetBrains.dotMemoryUnit.props" />
@@ -21,13 +21,14 @@
     <!-- Keep in sync with Avalonia.Build.Tasks -->
     <!-- Keep in sync with Avalonia.Build.Tasks -->
     <PackageReference Include="Mono.Cecil" Version="0.11.5" />
     <PackageReference Include="Mono.Cecil" Version="0.11.5" />
     <PackageReference Include="Microsoft.Build.Framework" Version="17.3.2" PrivateAssets="All" />
     <PackageReference Include="Microsoft.Build.Framework" Version="17.3.2" PrivateAssets="All" />
+    <PackageReference Include="NuGet.Protocol" Version="6.14.0" />
     <PackageReference Include="xunit.runner.console" Version="2.4.2">
     <PackageReference Include="xunit.runner.console" Version="2.4.2">
       <PrivateAssets>all</PrivateAssets>
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>
     </PackageReference>
 
 
-    <PackageDownload Include="Microsoft.DotNet.ApiCompat.Tool" Version="[8.0.200]" />
-    <PackageDownload Include="Microsoft.DotNet.GenAPI.Tool" Version="[8.0.300-preview.24115.44]" />
+    <PackageDownload Include="Microsoft.DotNet.ApiCompat.Tool" Version="[10.0.100-preview.7.25380.108]" />
+    <PackageDownload Include="Microsoft.DotNet.ApiDiff.Tool" Version="[10.0.100-rc.1.25414.111]" />
     <PackageDownload Include="dotnet-ilrepack" Version="[1.0.0]" />
     <PackageDownload Include="dotnet-ilrepack" Version="[1.0.0]" />
   </ItemGroup>
   </ItemGroup>