Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Create a tool and msbuild task to find differences in nuget packages #14460

Closed
wants to merge 3 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions Arcade.sln
Original file line number Diff line number Diff line change
@@ -147,6 +147,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.XliffTasks
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.XliffTasks.Tests", "src\Microsoft.DotNet.XliffTasks.Tests\Microsoft.DotNet.XliffTasks.Tests.csproj", "{6BA81447-C61D-4F91-BF0F-5B17AF4CFFAC}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PackageDiff", "PackageDiff", "{82AD0031-378A-47FC-9448-0D7EDD7BB1C9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PackageDiff", "src\PackageDiff\PackageDiff\PackageDiff.csproj", "{01760F15-01CD-463E-BB53-D71E1EA697B4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PackageDiff.Tasks", "src\PackageDiff\PackageDiff.Tasks\PackageDiff.Tasks.csproj", "{D9A0A3DD-6A14-425B-AF1D-975AB1E3E7B1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -921,6 +927,30 @@ Global
{6BA81447-C61D-4F91-BF0F-5B17AF4CFFAC}.Release|x64.Build.0 = Release|Any CPU
{6BA81447-C61D-4F91-BF0F-5B17AF4CFFAC}.Release|x86.ActiveCfg = Release|Any CPU
{6BA81447-C61D-4F91-BF0F-5B17AF4CFFAC}.Release|x86.Build.0 = Release|Any CPU
{01760F15-01CD-463E-BB53-D71E1EA697B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{01760F15-01CD-463E-BB53-D71E1EA697B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{01760F15-01CD-463E-BB53-D71E1EA697B4}.Debug|x64.ActiveCfg = Debug|Any CPU
{01760F15-01CD-463E-BB53-D71E1EA697B4}.Debug|x64.Build.0 = Debug|Any CPU
{01760F15-01CD-463E-BB53-D71E1EA697B4}.Debug|x86.ActiveCfg = Debug|Any CPU
{01760F15-01CD-463E-BB53-D71E1EA697B4}.Debug|x86.Build.0 = Debug|Any CPU
{01760F15-01CD-463E-BB53-D71E1EA697B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{01760F15-01CD-463E-BB53-D71E1EA697B4}.Release|Any CPU.Build.0 = Release|Any CPU
{01760F15-01CD-463E-BB53-D71E1EA697B4}.Release|x64.ActiveCfg = Release|Any CPU
{01760F15-01CD-463E-BB53-D71E1EA697B4}.Release|x64.Build.0 = Release|Any CPU
{01760F15-01CD-463E-BB53-D71E1EA697B4}.Release|x86.ActiveCfg = Release|Any CPU
{01760F15-01CD-463E-BB53-D71E1EA697B4}.Release|x86.Build.0 = Release|Any CPU
{D9A0A3DD-6A14-425B-AF1D-975AB1E3E7B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D9A0A3DD-6A14-425B-AF1D-975AB1E3E7B1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D9A0A3DD-6A14-425B-AF1D-975AB1E3E7B1}.Debug|x64.ActiveCfg = Debug|Any CPU
{D9A0A3DD-6A14-425B-AF1D-975AB1E3E7B1}.Debug|x64.Build.0 = Debug|Any CPU
{D9A0A3DD-6A14-425B-AF1D-975AB1E3E7B1}.Debug|x86.ActiveCfg = Debug|Any CPU
{D9A0A3DD-6A14-425B-AF1D-975AB1E3E7B1}.Debug|x86.Build.0 = Debug|Any CPU
{D9A0A3DD-6A14-425B-AF1D-975AB1E3E7B1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D9A0A3DD-6A14-425B-AF1D-975AB1E3E7B1}.Release|Any CPU.Build.0 = Release|Any CPU
{D9A0A3DD-6A14-425B-AF1D-975AB1E3E7B1}.Release|x64.ActiveCfg = Release|Any CPU
{D9A0A3DD-6A14-425B-AF1D-975AB1E3E7B1}.Release|x64.Build.0 = Release|Any CPU
{D9A0A3DD-6A14-425B-AF1D-975AB1E3E7B1}.Release|x86.ActiveCfg = Release|Any CPU
{D9A0A3DD-6A14-425B-AF1D-975AB1E3E7B1}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -958,6 +988,9 @@ Global
{14462553-E4E1-4F67-B954-4BF24B1DAAFE} = {3C542789-2576-48C8-9772-C9D7575F7E42}
{650B7526-7B8A-45B5-B14E-C16D828891B2} = {C53DD924-C212-49EA-9BC4-1827421361EF}
{6BA81447-C61D-4F91-BF0F-5B17AF4CFFAC} = {C53DD924-C212-49EA-9BC4-1827421361EF}
{82AD0031-378A-47FC-9448-0D7EDD7BB1C9} = {6DA9F58A-34D5-45A6-998E-5D2B8037C3FE}
{01760F15-01CD-463E-BB53-D71E1EA697B4} = {82AD0031-378A-47FC-9448-0D7EDD7BB1C9}
{D9A0A3DD-6A14-425B-AF1D-975AB1E3E7B1} = {82AD0031-378A-47FC-9448-0D7EDD7BB1C9}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {32B9C883-432E-4FC8-A1BF-090EB033DD5B}
4 changes: 4 additions & 0 deletions eng/Version.Details.xml
Original file line number Diff line number Diff line change
@@ -155,5 +155,9 @@
<Uri>https://github.com/dotnet/runtime</Uri>
<Sha>d099f075e45d2aa6007a22b71b45a08758559f80</Sha>
</Dependency>
<Dependency Name="Microsoft.NET.ILLink.Tasks" Version="">
<Uri>https://github.com/dotnet/runtime</Uri>
<Sha />
</Dependency>
</ToolsetDependencies>
</Dependencies>
2 changes: 2 additions & 0 deletions eng/Versions.props
Original file line number Diff line number Diff line change
@@ -82,5 +82,7 @@
<MicrosoftNetTestSdkVersion>17.5.0</MicrosoftNetTestSdkVersion>
<!-- xharness -->
<MicrosoftDotNetXHarnessCLIVersion>9.0.0-prerelease.24077.1</MicrosoftDotNetXHarnessCLIVersion>
<MicrosoftNETILLinkTasksVersion>
</MicrosoftNETILLinkTasksVersion>
</PropertyGroup>
</Project>
31 changes: 31 additions & 0 deletions src/PackageDiff/PackageDiff.Tasks/PackageDiff.Tasks.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>true</IsPackable>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<NoWarn>$(NoWarn);NU5128;NU5129;NU5100</NoWarn>
<BuildOutputTargetFolder>tasks</BuildOutputTargetFolder>
<GenerateDependencyFile>true</GenerateDependencyFile>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Build.Utilities.Core" PrivateAssets="all" ExcludeAssets="Runtime" />

<Content Include="**\*.props" Pack="true" PackagePath="%(RecursiveDir)%(Filename)%(Extension)" Publish="true"
CopyToOutputDirectory="PreserveNewest" TargetPath="%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>

<Target Name="AddToolToPackage" BeforeTargets="_GetPackageFiles">
<MSBuild Projects="..\PackageDiff\PackageDiff.csproj" Targets="Publish" Properties="TargetFramework=$(NetToolCurrent);Configuration=$(Configuration);PublishDir=$(OutputPath)\PackageDiff\" />
<ItemGroup>
<_DiffToolPublishContent Include="$(OutputPath)PackageDiff\**" />
<Content Include="@(_DiffToolPublishContent)" Pack="true" PackagePath="tools\%(RecursiveDir)%(Filename)%(Extension)" Publish="true"
CopyToOutputDirectory="PreserveNewest" TargetPath="tools\%(RecursiveDir)%(Filename)%(Extension)"/>
</ItemGroup>
</Target>

</Project>
28 changes: 28 additions & 0 deletions src/PackageDiff/PackageDiff.Tasks/PackageDiff.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Build.Framework;

public class PackageDiff: Microsoft.Build.Utilities.ToolTask
{
[Required]
public string BaselinePackage {get; set;} = "";

[Required]
public string TestPackage {get; set;} = "";

protected override string ToolName { get; } = $"PackageDiff" + (System.Environment.OSVersion.Platform == PlatformID.Unix ? "" : ".exe");

protected override MessageImportance StandardOutputLoggingImportance => MessageImportance.High;
protected override bool HandleTaskExecutionErrors() => true;

protected override string GenerateFullPathToTool()
{
return Path.Combine(Path.GetDirectoryName(typeof(PackageDiff).Assembly.Location)!, "..", "..", "tools", ToolName);
}

protected override string GenerateCommandLineCommands()
{
return $"\"{BaselinePackage}\" \"{TestPackage}\"";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project>

<PropertyGroup>
<_PackageDiffTasksAssemblyPath>$(MSBuildThisFileDirectory)\..\tasks\netstandard2.0\PackageDiff.Tasks.dll</_PackageDiffTasksAssemblyPath>
</PropertyGroup>

<UsingTask TaskName="PackageDiff" AssemblyFile="$(_PackageDiffTasksAssemblyPath)" />

</Project>
11 changes: 11 additions & 0 deletions src/PackageDiff/PackageDiff/PackageDiff.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<SelfContained>true</SelfContained>
<PublishTrimmed>true</PublishTrimmed>
</PropertyGroup>

</Project>
160 changes: 160 additions & 0 deletions src/PackageDiff/PackageDiff/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using System.Text;
using System.Threading.Tasks;

public class PackageDiff
{
public static async Task<int> Main(string[] args)
{
if (args.Length != 2)
{
Console.WriteLine("Usage: PackageDiff <path-or-url-of-package1> <path-or-url-of-package2>");
return 1;
}

ZipArchive package1 = await GetZipArchiveAsync(args[0]);
ZipArchive package2 = await GetZipArchiveAsync(args[1]);
var diff = GetDiffs(package1, package2);
if (diff is not "")
{
Console.WriteLine(diff);
return 1;
}
return 0;
}

public static async Task<ZipArchive> GetZipArchiveAsync(string arg)
{
if (File.Exists(arg))
{
return new ZipArchive(File.OpenRead(arg));
}
else if (Uri.TryCreate(arg, UriKind.RelativeOrAbsolute, out var uri))
{
var webClient = new HttpClient();
return new ZipArchive(await webClient.GetStreamAsync(uri));
}
else
{
throw new ArgumentException($"Invalid path or url to package1: {arg}");
}
}

public static string GetDiffs(ZipArchive package1, ZipArchive package2)
{
StringBuilder output = new();

if (TryGetDiff(package1.Entries.Select(entry => entry.FullName).ToList(), package2.Entries.Select(entry => entry.FullName).ToList(), out var fileDiffs))
{
output.AppendLine("File differences:");
output.AppendLine(string.Join(Environment.NewLine, fileDiffs.Select(d => " " + d)));
output.AppendLine();
}

if (TryGetDiff(package1.GetNuspec().Lines(), package2.GetNuspec().Lines(), out var editedDiff))
{
output.AppendLine("Nuspec differences:");
output.AppendLine(string.Join(Environment.NewLine, editedDiff.Select(d => " " + d)));
output.AppendLine();
}
var dlls1 = package1.Entries.Where(entry => entry.FullName.EndsWith(".dll")).ToImmutableDictionary(entry => entry.FullName, entry => entry);
var dlls2 = package2.Entries.Where(entry => entry.FullName.EndsWith(".dll")).ToImmutableDictionary(entry => entry.FullName, entry => entry);
foreach (var kvp in dlls1)
{
var dllPath = kvp.Key;
var dll1 = kvp.Value;
if (dlls2.TryGetValue(dllPath, out ZipArchiveEntry? dll2))
{
try
{
var version1 = new PEReader(dll1.Open().ReadToEnd().ToImmutableArray()).GetMetadataReader().GetAssemblyDefinition().Version.ToString();
var version2 = new PEReader(dll2.Open().ReadToEnd().ToImmutableArray()).GetMetadataReader().GetAssemblyDefinition().Version.ToString();
if (version1 != version2)
{
output.AppendLine($"Assembly {dllPath} has different versions: {version1} and {version2}");
}
}
catch (InvalidOperationException)
{ }
}
}
return output.ToString();
}

public static bool TryGetDiff(List<string> originalLines, List<string> modifiedLines, out List<string> formattedDiff)
{
// Edit distance algorithm: https://en.wikipedia.org/wiki/Longest_common_subsequence

int[,] dp = new int[originalLines.Count + 1, modifiedLines.Count + 1];

// Initialize first row and column
for (int i = 0; i <= originalLines.Count; i++)
{
dp[i, 0] = i;
}
for (int j = 0; j <= modifiedLines.Count; j++)
{
dp[0, j] = j;
}

// Compute edit distance
for (int i = 1; i <= originalLines.Count; i++)
{
for (int j = 1; j <= modifiedLines.Count; j++)
{
if (string.Compare(originalLines[i - 1], modifiedLines[j - 1]) == 0)
{
dp[i, j] = dp[i - 1, j - 1];
}
else
{
dp[i, j] = 1 + Math.Min(dp[i - 1, j], dp[i, j - 1]);
}
}
}

// Trace back the edits
int row = originalLines.Count;
int col = modifiedLines.Count;

formattedDiff = [];
while (row > 0 || col > 0)
{
if (row > 0 && col > 0 && string.Compare(originalLines[row - 1], modifiedLines[col - 1]) == 0)
{
formattedDiff.Add(" " + originalLines[row - 1]);
row--;
col--;
}
else if (col > 0 && (row == 0 || dp[row, col - 1] <= dp[row - 1, col]))
{
formattedDiff.Add("+ " + modifiedLines[col - 1]);
col--;
}
else if (row > 0 && (col == 0 || dp[row, col - 1] > dp[row - 1, col]))
{
formattedDiff.Add("- " + originalLines[row - 1]);
row--;
}
else
{
throw new Exception("Unreachable code");
}
}
formattedDiff.Reverse();
return dp[originalLines.Count, modifiedLines.Count] != 0;
}

}
60 changes: 60 additions & 0 deletions src/PackageDiff/PackageDiff/ZipExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;

static class ZipExtensions
{
public static List<string> Lines(this ZipArchiveEntry entry, Encoding? encoding = null)
{
return entry.ReadToString(encoding).Replace("\r\n", "\n").Split('\n').ToList();
}

public static string ReadToString(this ZipArchiveEntry entry, Encoding? encoding = null)
{
Stream stream = entry.Open();
byte[] buffer = stream.ReadToEnd();
// Remove UTF-8 BOM if present
int index = 0;
if (buffer[0] == 0xEF && buffer[1] == 0xBB && buffer[2] == 0xBF)
{
index = 3;
}
encoding ??= Encoding.UTF8;
string fileText = encoding.GetString(buffer, index, buffer.Length - index);
return fileText;
}

public static ZipArchiveEntry GetNuspec(this ZipArchive package)
{
return package.Entries.Where(entry => entry.FullName.EndsWith(".nuspec")).Single();
}

public static byte[] ReadToEnd(this Stream stream)
{
int bufferSize = 2048;
byte[] buffer = new byte[bufferSize];
int offset = 0;
while (true)
{
int bytesRead = stream.Read(buffer, offset, bufferSize - offset);
offset += bytesRead;
if (bytesRead == 0)
{
break;
}
if (offset == bufferSize)
{
Array.Resize(ref buffer, bufferSize * 2);
bufferSize *= 2;
}
}
Array.Resize(ref buffer, offset);
return buffer;
}
}