From 66724d47858ec1405644678aaa86ff43d2e8db88 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Wed, 20 Aug 2025 04:40:50 +0200 Subject: [PATCH] Improved API diff (#19490) * Improved API diff * Merge API diff files * Ignore platform version for API diff * Start with fresh suppression files --- .nuke/build.schema.json | 8 +- NuGet.Config | 3 +- api/Avalonia.Android.nupkg.xml | 22 - api/Avalonia.Browser.nupkg.xml | 22 - api/Avalonia.FreeDesktop.nupkg.xml | 10 - api/Avalonia.Skia.nupkg.xml | 16 - api/Avalonia.Themes.Fluent.nupkg.xml | 16 - api/Avalonia.Win32.nupkg.xml | 214 --------- api/Avalonia.iOS.nupkg.xml | 16 - api/Avalonia.nupkg.xml | 268 ----------- build/SharedVersion.props | 1 - nukebuild/ApiDiffHelper.cs | 617 +++++++++++++++---------- nukebuild/Build.cs | 63 ++- nukebuild/BuildParameters.cs | 36 +- nukebuild/ByteArrayEqualityComparer.cs | 25 + nukebuild/_build.csproj | 9 +- 16 files changed, 486 insertions(+), 860 deletions(-) delete mode 100644 api/Avalonia.Android.nupkg.xml delete mode 100644 api/Avalonia.Browser.nupkg.xml delete mode 100644 api/Avalonia.FreeDesktop.nupkg.xml delete mode 100644 api/Avalonia.Skia.nupkg.xml delete mode 100644 api/Avalonia.Themes.Fluent.nupkg.xml delete mode 100644 api/Avalonia.Win32.nupkg.xml delete mode 100644 api/Avalonia.iOS.nupkg.xml delete mode 100644 api/Avalonia.nupkg.xml create mode 100644 nukebuild/ByteArrayEqualityComparer.cs diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index 44e1bb4696..a9bcc2bd4b 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -6,9 +6,6 @@ "build": { "type": "object", "properties": { - "api-baseline": { - "type": "string" - }, "configuration": { "type": "string" }, @@ -16,6 +13,9 @@ "type": "boolean", "description": "Indicates to continue a previously failed build attempt" }, + "force-api-baseline": { + "type": "string" + }, "force-nuget-version": { "type": "string" }, @@ -83,6 +83,7 @@ "CompileNative", "CreateIntermediateNugetPackages", "CreateNugetPackages", + "DownloadApiBaselinePackages", "GenerateCppHeaders", "OutputApiDiff", "OutputVersion", @@ -121,6 +122,7 @@ "CompileNative", "CreateIntermediateNugetPackages", "CreateNugetPackages", + "DownloadApiBaselinePackages", "GenerateCppHeaders", "OutputApiDiff", "OutputVersion", diff --git a/NuGet.Config b/NuGet.Config index 2042fea360..99d827d465 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -3,7 +3,6 @@ - - + diff --git a/api/Avalonia.Android.nupkg.xml b/api/Avalonia.Android.nupkg.xml deleted file mode 100644 index deed9db4de..0000000000 --- a/api/Avalonia.Android.nupkg.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CP0001 - T:Avalonia.Android.AvaloniaMainActivity`1 - baseline/net8.0-android34.0/Avalonia.Android.dll - target/net8.0-android34.0/Avalonia.Android.dll - - - CP0002 - M:Avalonia.Android.AndroidViewControlHandle.get_HandleDescriptor - baseline/net8.0-android34.0/Avalonia.Android.dll - target/net8.0-android34.0/Avalonia.Android.dll - - - CP0007 - T:Avalonia.Android.AndroidViewControlHandle - baseline/net8.0-android34.0/Avalonia.Android.dll - target/net8.0-android34.0/Avalonia.Android.dll - - \ No newline at end of file diff --git a/api/Avalonia.Browser.nupkg.xml b/api/Avalonia.Browser.nupkg.xml deleted file mode 100644 index 0fb414ed14..0000000000 --- a/api/Avalonia.Browser.nupkg.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CP0002 - M:Avalonia.Browser.JSObjectControlHandle.get_Handle - baseline/net8.0-browser1.0/Avalonia.Browser.dll - target/net8.0-browser1.0/Avalonia.Browser.dll - - - CP0002 - M:Avalonia.Browser.JSObjectControlHandle.get_HandleDescriptor - baseline/net8.0-browser1.0/Avalonia.Browser.dll - target/net8.0-browser1.0/Avalonia.Browser.dll - - - CP0007 - T:Avalonia.Browser.JSObjectControlHandle - baseline/net8.0-browser1.0/Avalonia.Browser.dll - target/net8.0-browser1.0/Avalonia.Browser.dll - - \ No newline at end of file diff --git a/api/Avalonia.FreeDesktop.nupkg.xml b/api/Avalonia.FreeDesktop.nupkg.xml deleted file mode 100644 index f5fcb60bc8..0000000000 --- a/api/Avalonia.FreeDesktop.nupkg.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - CP0001 - T:Tmds.DBus.SourceGenerator.PropertyChanges`1 - baseline/netstandard2.0/Avalonia.FreeDesktop.dll - target/netstandard2.0/Avalonia.FreeDesktop.dll - - \ No newline at end of file diff --git a/api/Avalonia.Skia.nupkg.xml b/api/Avalonia.Skia.nupkg.xml deleted file mode 100644 index b275cbff58..0000000000 --- a/api/Avalonia.Skia.nupkg.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - CP0002 - M:Avalonia.Skia.SkiaSharpExtensions.ToSKFilterQuality(Avalonia.Media.Imaging.BitmapInterpolationMode) - baseline/netstandard2.0/Avalonia.Skia.dll - target/netstandard2.0/Avalonia.Skia.dll - - - CP0006 - M:Avalonia.Skia.ISkiaGpuWithPlatformGraphicsContext.TryGetGrContext - baseline/netstandard2.0/Avalonia.Skia.dll - target/netstandard2.0/Avalonia.Skia.dll - - \ No newline at end of file diff --git a/api/Avalonia.Themes.Fluent.nupkg.xml b/api/Avalonia.Themes.Fluent.nupkg.xml deleted file mode 100644 index 717b64f81e..0000000000 --- a/api/Avalonia.Themes.Fluent.nupkg.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - CP0007 - T:Avalonia.Themes.Fluent.ColorPaletteResources - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - - CP0008 - T:Avalonia.Themes.Fluent.ColorPaletteResources - baseline/netstandard2.0/Avalonia.Themes.Fluent.dll - target/netstandard2.0/Avalonia.Themes.Fluent.dll - - \ No newline at end of file diff --git a/api/Avalonia.Win32.nupkg.xml b/api/Avalonia.Win32.nupkg.xml deleted file mode 100644 index 3ce897deda..0000000000 --- a/api/Avalonia.Win32.nupkg.xml +++ /dev/null @@ -1,214 +0,0 @@ - - - - - CP0001 - T:Avalonia.Win32.Interop.Automation.DockPosition - baseline/netstandard2.0/Avalonia.Win32.dll - target/netstandard2.0/Avalonia.Win32.dll - - - CP0001 - T:Avalonia.Win32.Interop.Automation.IDockProvider - baseline/netstandard2.0/Avalonia.Win32.dll - target/netstandard2.0/Avalonia.Win32.dll - - - CP0001 - T:Avalonia.Win32.Interop.Automation.IExpandCollapseProvider - baseline/netstandard2.0/Avalonia.Win32.dll - target/netstandard2.0/Avalonia.Win32.dll - - - CP0001 - T:Avalonia.Win32.Interop.Automation.IGridItemProvider - baseline/netstandard2.0/Avalonia.Win32.dll - target/netstandard2.0/Avalonia.Win32.dll - - - CP0001 - T:Avalonia.Win32.Interop.Automation.IGridProvider - baseline/netstandard2.0/Avalonia.Win32.dll - target/netstandard2.0/Avalonia.Win32.dll - - - CP0001 - T:Avalonia.Win32.Interop.Automation.IInvokeProvider - baseline/netstandard2.0/Avalonia.Win32.dll - target/netstandard2.0/Avalonia.Win32.dll - - - CP0001 - T:Avalonia.Win32.Interop.Automation.IMultipleViewProvider - baseline/netstandard2.0/Avalonia.Win32.dll - target/netstandard2.0/Avalonia.Win32.dll - - - CP0001 - T:Avalonia.Win32.Interop.Automation.IRangeValueProvider - baseline/netstandard2.0/Avalonia.Win32.dll - target/netstandard2.0/Avalonia.Win32.dll - - - CP0001 - T:Avalonia.Win32.Interop.Automation.IRawElementProviderAdviseEvents - baseline/netstandard2.0/Avalonia.Win32.dll - target/netstandard2.0/Avalonia.Win32.dll - - - CP0001 - T:Avalonia.Win32.Interop.Automation.IRawElementProviderFragment - baseline/netstandard2.0/Avalonia.Win32.dll - target/netstandard2.0/Avalonia.Win32.dll - - - CP0001 - T:Avalonia.Win32.Interop.Automation.IRawElementProviderFragmentRoot - baseline/netstandard2.0/Avalonia.Win32.dll - target/netstandard2.0/Avalonia.Win32.dll - - - CP0001 - T:Avalonia.Win32.Interop.Automation.IRawElementProviderSimple - baseline/netstandard2.0/Avalonia.Win32.dll - target/netstandard2.0/Avalonia.Win32.dll - - - CP0001 - T:Avalonia.Win32.Interop.Automation.IRawElementProviderSimple2 - baseline/netstandard2.0/Avalonia.Win32.dll - target/netstandard2.0/Avalonia.Win32.dll - - - CP0001 - T:Avalonia.Win32.Interop.Automation.IScrollItemProvider - baseline/netstandard2.0/Avalonia.Win32.dll - target/netstandard2.0/Avalonia.Win32.dll - - - CP0001 - T:Avalonia.Win32.Interop.Automation.IScrollProvider - baseline/netstandard2.0/Avalonia.Win32.dll - target/netstandard2.0/Avalonia.Win32.dll - - - CP0001 - T:Avalonia.Win32.Interop.Automation.ISelectionItemProvider - baseline/netstandard2.0/Avalonia.Win32.dll - target/netstandard2.0/Avalonia.Win32.dll - - - CP0001 - T:Avalonia.Win32.Interop.Automation.ISelectionProvider - baseline/netstandard2.0/Avalonia.Win32.dll - target/netstandard2.0/Avalonia.Win32.dll - - - CP0001 - T:Avalonia.Win32.Interop.Automation.ISynchronizedInputProvider - baseline/netstandard2.0/Avalonia.Win32.dll - target/netstandard2.0/Avalonia.Win32.dll - - - CP0001 - T:Avalonia.Win32.Interop.Automation.ITableItemProvider - baseline/netstandard2.0/Avalonia.Win32.dll - target/netstandard2.0/Avalonia.Win32.dll - - - CP0001 - T:Avalonia.Win32.Interop.Automation.ITableProvider - baseline/netstandard2.0/Avalonia.Win32.dll - target/netstandard2.0/Avalonia.Win32.dll - - - CP0001 - T:Avalonia.Win32.Interop.Automation.ITextProvider - baseline/netstandard2.0/Avalonia.Win32.dll - target/netstandard2.0/Avalonia.Win32.dll - - - CP0001 - T:Avalonia.Win32.Interop.Automation.ITextRangeProvider - baseline/netstandard2.0/Avalonia.Win32.dll - target/netstandard2.0/Avalonia.Win32.dll - - - CP0001 - T:Avalonia.Win32.Interop.Automation.IToggleProvider - baseline/netstandard2.0/Avalonia.Win32.dll - target/netstandard2.0/Avalonia.Win32.dll - - - CP0001 - T:Avalonia.Win32.Interop.Automation.ITransformProvider - baseline/netstandard2.0/Avalonia.Win32.dll - target/netstandard2.0/Avalonia.Win32.dll - - - CP0001 - T:Avalonia.Win32.Interop.Automation.IValueProvider - baseline/netstandard2.0/Avalonia.Win32.dll - target/netstandard2.0/Avalonia.Win32.dll - - - CP0001 - T:Avalonia.Win32.Interop.Automation.IWindowProvider - baseline/netstandard2.0/Avalonia.Win32.dll - target/netstandard2.0/Avalonia.Win32.dll - - - CP0001 - T:Avalonia.Win32.Interop.Automation.NavigateDirection - baseline/netstandard2.0/Avalonia.Win32.dll - target/netstandard2.0/Avalonia.Win32.dll - - - CP0001 - T:Avalonia.Win32.Interop.Automation.ProviderOptions - baseline/netstandard2.0/Avalonia.Win32.dll - target/netstandard2.0/Avalonia.Win32.dll - - - CP0001 - T:Avalonia.Win32.Interop.Automation.RowOrColumnMajor - baseline/netstandard2.0/Avalonia.Win32.dll - target/netstandard2.0/Avalonia.Win32.dll - - - CP0001 - T:Avalonia.Win32.Interop.Automation.SupportedTextSelection - baseline/netstandard2.0/Avalonia.Win32.dll - target/netstandard2.0/Avalonia.Win32.dll - - - CP0001 - T:Avalonia.Win32.Interop.Automation.SynchronizedInputType - baseline/netstandard2.0/Avalonia.Win32.dll - target/netstandard2.0/Avalonia.Win32.dll - - - CP0001 - T:Avalonia.Win32.Interop.Automation.TextPatternRangeEndpoint - baseline/netstandard2.0/Avalonia.Win32.dll - target/netstandard2.0/Avalonia.Win32.dll - - - CP0001 - T:Avalonia.Win32.Interop.Automation.TextUnit - baseline/netstandard2.0/Avalonia.Win32.dll - target/netstandard2.0/Avalonia.Win32.dll - - - CP0001 - T:Avalonia.Win32.Interop.Automation.WindowInteractionState - baseline/netstandard2.0/Avalonia.Win32.dll - target/netstandard2.0/Avalonia.Win32.dll - - - CP0001 - T:Avalonia.Win32.Interop.Automation.WindowVisualState - baseline/netstandard2.0/Avalonia.Win32.dll - target/netstandard2.0/Avalonia.Win32.dll - - \ No newline at end of file diff --git a/api/Avalonia.iOS.nupkg.xml b/api/Avalonia.iOS.nupkg.xml deleted file mode 100644 index 5f6e822d81..0000000000 --- a/api/Avalonia.iOS.nupkg.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - CP0002 - M:Avalonia.iOS.UIViewControlHandle.get_HandleDescriptor - baseline/net8.0-tvos17.0/Avalonia.iOS.dll - target/net8.0-tvos17.0/Avalonia.iOS.dll - - - CP0007 - T:Avalonia.iOS.UIViewControlHandle - baseline/net8.0-tvos17.0/Avalonia.iOS.dll - target/net8.0-tvos17.0/Avalonia.iOS.dll - - \ No newline at end of file diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml deleted file mode 100644 index 8d64cb2a82..0000000000 --- a/api/Avalonia.nupkg.xml +++ /dev/null @@ -1,268 +0,0 @@ - - - - - CP0001 - T:Avalonia.Controls.PseudolassesExtensions - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0001 - T:Avalonia.Data.Core.CastTypePropertyPathElement - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0001 - T:Avalonia.Data.Core.ChildTraversalPropertyPathElement - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0001 - T:Avalonia.Data.Core.EnsureTypePropertyPathElement - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0001 - T:Avalonia.Data.Core.IPropertyPathElement - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0001 - T:Avalonia.Data.Core.PropertyPath - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0001 - T:Avalonia.Data.Core.PropertyPathBuilder - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0001 - T:Avalonia.Data.Core.PropertyPropertyPathElement - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0001 - T:Avalonia.Utilities.CharacterReader - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0001 - T:Avalonia.Utilities.IdentifierParser - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0001 - T:Avalonia.Utilities.KeywordParser - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0001 - T:Avalonia.Utilities.StyleClassParser - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0002 - M:Avalonia.Diagnostics.AppliedStyle.get_HasActivator - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0002 - M:Avalonia.Diagnostics.AppliedStyle.get_IsActive - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0002 - M:Avalonia.Diagnostics.AppliedStyle.get_Style - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0002 - M:Avalonia.Diagnostics.StyledElementExtensions.GetStyleDiagnostics(Avalonia.StyledElement) - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0002 - M:Avalonia.Diagnostics.StyleDiagnostics.#ctor(System.Collections.Generic.IReadOnlyList{Avalonia.Diagnostics.AppliedStyle}) - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0002 - M:Avalonia.Diagnostics.StyleDiagnostics.get_AppliedStyles - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0002 - M:Avalonia.Threading.DispatcherPriorityAwaitable.get_IsCompleted - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0002 - M:Avalonia.Threading.DispatcherPriorityAwaitable.GetAwaiter - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0002 - M:Avalonia.Threading.DispatcherPriorityAwaitable.GetResult - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0002 - M:Avalonia.Threading.DispatcherPriorityAwaitable.OnCompleted(System.Action) - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0002 - M:Avalonia.Threading.DispatcherPriorityAwaitable`1.GetAwaiter - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0002 - M:Avalonia.Threading.DispatcherPriorityAwaitable`1.GetResult - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0002 - 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}) - baseline/netstandard2.0/Avalonia.Controls.dll - target/netstandard2.0/Avalonia.Controls.dll - - - CP0002 - M:Avalonia.Controls.Screens.#ctor(Avalonia.Platform.IScreenImpl) - baseline/netstandard2.0/Avalonia.Controls.dll - target/netstandard2.0/Avalonia.Controls.dll - - - CP0006 - M:Avalonia.Input.Platform.IClipboard.FlushAsync - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0006 - M:Avalonia.Input.Platform.IClipboard.TryGetInProcessDataObjectAsync - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0006 - M:Avalonia.Platform.Storage.IStorageFolder.GetFileAsync(System.String) - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0006 - M:Avalonia.Platform.Storage.IStorageFolder.GetFolderAsync(System.String) - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0006 - M:Avalonia.Controls.Notifications.IManagedNotificationManager.Close(System.Object) - baseline/netstandard2.0/Avalonia.Controls.dll - target/netstandard2.0/Avalonia.Controls.dll - - - CP0006 - M:Avalonia.Controls.Notifications.INotificationManager.Close(Avalonia.Controls.Notifications.INotification) - baseline/netstandard2.0/Avalonia.Controls.dll - target/netstandard2.0/Avalonia.Controls.dll - - - CP0006 - M:Avalonia.Controls.Notifications.INotificationManager.CloseAll - baseline/netstandard2.0/Avalonia.Controls.dll - target/netstandard2.0/Avalonia.Controls.dll - - - CP0006 - M:Avalonia.Controls.Primitives.IPopupHost.ConfigurePosition(Avalonia.Controls.Primitives.PopupPositioning.PopupPositionRequest) - baseline/netstandard2.0/Avalonia.Controls.dll - target/netstandard2.0/Avalonia.Controls.dll - - - CP0006 - M:Avalonia.Controls.Primitives.IPopupHost.TakeFocus - baseline/netstandard2.0/Avalonia.Controls.dll - target/netstandard2.0/Avalonia.Controls.dll - - - CP0006 - P:Avalonia.Controls.Platform.IInsetsManager.DisplayEdgeToEdgePreference - baseline/netstandard2.0/Avalonia.Controls.dll - target/netstandard2.0/Avalonia.Controls.dll - - - CP0006 - P:Avalonia.Controls.Platform.IInsetsManager.DisplaysEdgeToEdge - baseline/netstandard2.0/Avalonia.Controls.dll - target/netstandard2.0/Avalonia.Controls.dll - - - CP0007 - T:Avalonia.Threading.DispatcherPriorityAwaitable - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0007 - T:Avalonia.Threading.DispatcherPriorityAwaitable`1 - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0008 - T:Avalonia.Threading.DispatcherPriorityAwaitable - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0008 - T:Avalonia.Threading.DispatcherPriorityAwaitable`1 - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0009 - T:Avalonia.Diagnostics.StyleDiagnostics - baseline/netstandard2.0/Avalonia.Base.dll - target/netstandard2.0/Avalonia.Base.dll - - - CP0009 - T:Avalonia.Controls.Screens - baseline/netstandard2.0/Avalonia.Controls.dll - target/netstandard2.0/Avalonia.Controls.dll - - - CP0012 - M:Avalonia.Controls.Button.OnAccessKey(Avalonia.Interactivity.RoutedEventArgs) - baseline/netstandard2.0/Avalonia.Controls.dll - target/netstandard2.0/Avalonia.Controls.dll - - \ No newline at end of file diff --git a/build/SharedVersion.props b/build/SharedVersion.props index d18aa6447c..37d14a5647 100644 --- a/build/SharedVersion.props +++ b/build/SharedVersion.props @@ -3,7 +3,6 @@ Avalonia 12.0.999 - 11.1.0 Avalonia Team Copyright 2013-$([System.DateTime]::Now.ToString(`yyyy`)) © The AvaloniaUI Project https://avaloniaui.net/?utm_source=nuget&utm_medium=referral&utm_content=project_homepage_link diff --git a/nukebuild/ApiDiffHelper.cs b/nukebuild/ApiDiffHelper.cs index ac6be61ee3..1ef0995eff 100644 --- a/nukebuild/ApiDiffHelper.cs +++ b/nukebuild/ApiDiffHelper.cs @@ -1,313 +1,464 @@ +#nullable enable + using System; using System.Collections.Generic; -using System.Diagnostics; +using System.Collections.Immutable; using System.IO; using System.IO.Compression; 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 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 Serilog; using static Serilog.Log; 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(); - // 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(); + + // 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 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; + } - 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(); - var right = new List(); + static string[] GetErrors(IEnumerable 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 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 DownloadAndExtractPackagesAsync( + IEnumerable 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(); + + foreach (var packagePath in currentPackagePaths) + { + string packageId; + AbsolutePath currentFolderPath; + AbsolutePath baselineFolderPath; + Dictionary currentFolderNames; + Dictionary 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(); - 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 GetDlls(ZipArchive archive) + static async Task 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(); + return new NuGetDownloadContext(packageSource, findPackageByIdResource); } - static async Task DownloadBaselinePackage(string packagePath, string baselineVersion) + /// + /// 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. + /// + static async Task 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( - "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 ExtractDiffableAssembliesFromPackage( + ZipArchive packageArchive, + AbsolutePath destinationFolderPath) + { + var folderByFramework = new Dictionary(); - 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 ExtractDll(string basePath, DllEntry dllEntry, string targetFolder) + public sealed class GlobalDiffInfo( + NuGetVersion baselineVersion, + NuGetVersion currentVersion, + ImmutableArray 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 Packages { get; } = packages; + } - return dllPath; + public sealed class PackageDiffInfo(string packageId, ImmutableArray frameworks) + { + public string PackageId { get; } = packageId; + public ImmutableArray 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; } } diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index c6942e430c..0c962fa129 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -20,6 +20,7 @@ using static Nuke.Common.Tools.VSWhere.VSWhereTasks; using static Serilog.Log; using MicroCom.CodeGenerator; using NuGet.Configuration; +using NuGet.Versioning; using Nuke.Common.CI.AzurePipelines; using Nuke.Common.IO; @@ -35,11 +36,15 @@ partial class Build : NukeBuild { 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; - [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")] Tool IlRepackTool; @@ -321,25 +326,53 @@ partial class Build : NukeBuild Parameters.NugetRoot / $"Avalonia.{Parameters.Version}.nupkg", Parameters.NugetRoot / $"Avalonia.{Parameters.Version}.snupkg"); }); - - Target ValidateApiDiff => _ => _ + + Target DownloadApiBaselinePackages => _ => _ .DependsOn(CreateNugetPackages) .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 => _ => _ - .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 => _ => _ diff --git a/nukebuild/BuildParameters.cs b/nukebuild/BuildParameters.cs index 41e075a64c..ff28764569 100644 --- a/nukebuild/BuildParameters.cs +++ b/nukebuild/BuildParameters.cs @@ -1,3 +1,5 @@ +#nullable enable + using System; using System.Collections.Generic; using System.Linq; @@ -12,25 +14,25 @@ using static Nuke.Common.IO.PathConstruction; public partial class Build { [Parameter(Name = "configuration")] - public string Configuration { get; set; } + public string? Configuration { get; set; } [Parameter(Name = "skip-tests")] public bool SkipTests { get; set; } [Parameter(Name = "force-nuget-version")] - public string ForceNugetVersion { get; set; } + public string? ForceNugetVersion { get; set; } [Parameter(Name = "skip-previewer")] 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")] public bool? UpdateApiValidationSuppression { get; set; } [Parameter(Name = "version-output-dir")] - public AbsolutePath VersionOutputDir { get; set; } + public AbsolutePath? VersionOutputDir { get; set; } public class BuildParameters { @@ -39,8 +41,8 @@ public partial class Build public bool SkipPreviewer {get;} public string MainRepo { 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 Regex ReleaseBranchRegex { get; } public string MSBuildSolution { get; } @@ -70,10 +72,10 @@ public partial class Build public string FileZipSuffix { get; } public AbsolutePath ZipCoreArtifacts { get; } public AbsolutePath ZipNuGetArtifacts { get; } - public string ApiValidationBaseline { get; } + public string? ForceApiValidationBaseline { get; } public bool UpdateApiValidationSuppression { get; } public AbsolutePath ApiValidationSuppressionFiles { get; } - public AbsolutePath VersionOutputDir { get; } + public AbsolutePath? VersionOutputDir { get; } public BuildParameters(Build b, bool isPackingToLocalCache) { @@ -115,10 +117,9 @@ public partial class Build IsNuGetRelease = IsMainRepo && IsReleasable && IsReleaseBranch; // VERSION - var (propsVersion, propsApiCompatVersion) = GetVersion(); - Version = b.ForceNugetVersion ?? propsVersion; + Version = b.ForceNugetVersion ?? GetVersion(); - ApiValidationBaseline = b.ApiValidationBaseline ?? propsApiCompatVersion; + ForceApiValidationBaseline = b.ForceApiValidationBaseline; UpdateApiValidationSuppression = b.UpdateApiValidationSuppression ?? IsLocalBuild; if (IsRunningOnAzure) @@ -126,7 +127,9 @@ public partial class Build if (!IsNuGetRelease) { // 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; @@ -157,13 +160,10 @@ public partial class Build VersionOutputDir = b.VersionOutputDir; } - (string Version, string ApiCompatVersion) GetVersion() + string GetVersion() { 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; } } diff --git a/nukebuild/ByteArrayEqualityComparer.cs b/nukebuild/ByteArrayEqualityComparer.cs new file mode 100644 index 0000000000..f49a8f830e --- /dev/null +++ b/nukebuild/ByteArrayEqualityComparer.cs @@ -0,0 +1,25 @@ +#nullable enable + +using System; +using System.Collections.Generic; + +public sealed class ByteArrayEqualityComparer : IEqualityComparer +{ + 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(); + } +} diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index 2a74be30bf..7c52be978a 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -9,8 +9,8 @@ $(AvsCurrentTargetFramework) true - - https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet8-transport/nuget/v3/index.json + + https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet10-transport/nuget/v3/index.json @@ -21,13 +21,14 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + +