Skip to content

Commit 5bb4480

Browse files
committed
Merged PR 713643: The Nuget resolver should always take the smaller moniker in the case where other compound ones are present.
The nuget resolver sorts the content of a package in order. The assumption is that any non-compound moniker (e.g. 'net6.0') will come before any compound one (e.g. 'net6.0-android31.0). However, the comparison was made over strings across the whole relative path. This means that lib\net6.0-android31.0\Microsoft.Identity.Client.dll < lib\net6.0\Microsoft.Identity.Client.xml because 'lib\net6.0-' is less than 'lib/net6.0/' (the character '-' is less than the character '/'). Fix this by making a hierarchical relative path comparison, where each atom in the path is compared separately. This was reported before for package Microsoft.Identity.Client, but the workaound (pin a qualifier to net5.0/netstandard.2.0) was not actually working for other scenarios that also required bumping the package version. Related work items: #2047084
1 parent afadd4e commit 5bb4480

File tree

3 files changed

+115
-1
lines changed

3 files changed

+115
-1
lines changed

Public/Src/FrontEnd/Nuget/NugetAnalyzedPackage.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ public sealed class NugetAnalyzedPackage
129129

130130
private readonly Dictionary<string, INugetPackage> m_packagesOnConfig;
131131
private readonly bool m_doNotEnforceDependencyVersions;
132+
private readonly NugetRelativePathComparer m_nugetRelativePathComparer;
132133

133134
/// <nodoc/>
134135
private NugetAnalyzedPackage(
@@ -151,6 +152,7 @@ private NugetAnalyzedPackage(
151152
m_dependencies = new List<INugetPackage>();
152153
DependenciesPerFramework = new MultiValueDictionary<PathAtom, INugetPackage>();
153154
CredentialProviderPath = credentialProviderPath;
155+
m_nugetRelativePathComparer = new NugetRelativePathComparer(m_context.StringTable);
154156
}
155157

156158
/// <summary>
@@ -189,7 +191,7 @@ private void ParseManagedSemantics()
189191
var magicNugetMarker = PathAtom.Create(stringTable, "_._");
190192
var dllExtension = PathAtom.Create(stringTable, ".dll");
191193

192-
foreach (var relativePath in PackageOnDisk.Contents.OrderBy(path => path.ToString(stringTable)))
194+
foreach (var relativePath in PackageOnDisk.Contents.OrderBy(path => path, m_nugetRelativePathComparer))
193195
{
194196
// This is a dll. Check if it is in a lib folder or ref folder.
195197

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Diagnostics.ContractsLight;
7+
using BuildXL.Utilities.Core;
8+
9+
namespace BuildXL.FrontEnd.Nuget
10+
{
11+
/// <summary>
12+
/// Compares two relative paths in hierarchical order, starting with the atom closer to the root. Each atom is compared as a string, case insensitive.
13+
/// </summary>
14+
/// <remarks>
15+
/// For example, consider these two paths:
16+
///
17+
/// 1- lib/net6.0-android31/Microsoft.Identity.Client.dll
18+
/// 2- lib/net6.0/Microsoft.Identity.Client.dll
19+
///
20+
/// A regular string-based comparison would determine 1 &lt; 2, because the prefix string 'lib/net6.0-' is lexicographically smaller than 'lib/net6.0/'. On the other hand
21+
/// this comparer will determine that 2 &lt; 1, because the second atom on both paths is the first one that differs (starting from the root) and the string 'net6.0' is less than the string 'net6.0-android31'.
22+
/// </remarks>
23+
internal class NugetRelativePathComparer : IComparer<RelativePath>
24+
{
25+
private readonly StringTable m_stringTable;
26+
27+
/// <nodoc/>
28+
public NugetRelativePathComparer(StringTable stringTable)
29+
{
30+
Contract.Requires(stringTable != null);
31+
m_stringTable = stringTable;
32+
}
33+
34+
/// <inheritdoc/>
35+
public int Compare(RelativePath left, RelativePath right)
36+
{
37+
Contract.Requires(left.IsValid);
38+
Contract.Requires(right.IsValid);
39+
40+
var leftAtoms = left.GetAtoms();
41+
var rightAtoms = right.GetAtoms();
42+
// Let's go in order, starting from the atom closer to the root
43+
for(var i = 0; i < Math.Min(leftAtoms.Length, rightAtoms.Length); i++)
44+
{
45+
// Each pair is compared as strings - case insensitive (even on Linux, nuget is case insensitive across the board, so path differing in casing should be
46+
// understood as the same path
47+
var comparison = StringComparer.OrdinalIgnoreCase.Compare(leftAtoms[i].ToString(m_stringTable), rightAtoms[i].ToString(m_stringTable));
48+
49+
// If the pair of atoms are different, that determines the comparison of the whole path
50+
if (comparison != 0)
51+
{
52+
return comparison;
53+
}
54+
}
55+
56+
// If all atoms are the same up to the minimum length that is present on both sides,
57+
// the one with less atoms is smaller
58+
return leftAtoms.Length - rightAtoms.Length;
59+
60+
}
61+
}
62+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using BuildXL.FrontEnd.Nuget;
5+
using BuildXL.FrontEnd.Sdk;
6+
using BuildXL.Utilities.Core;
7+
using Xunit;
8+
9+
namespace Test.BuildXL.FrontEnd.Nuget
10+
{
11+
public class NugetRelativePathComparerTests
12+
{
13+
private readonly FrontEndContext m_context;
14+
15+
public NugetRelativePathComparerTests()
16+
{
17+
m_context = FrontEndContext.CreateInstanceForTesting();
18+
}
19+
20+
[Theory]
21+
[InlineData("same", "same", 0)]
22+
[InlineData("same/path","same/path", 0)]
23+
[InlineData("same/PATH", "same/path", 0)]
24+
[InlineData("short", "shortNot", -1)]
25+
[InlineData("prefix/short", "prefix/shortNot", -1)]
26+
[InlineData("short/but/longer/path", "shortNot/shorterPath", -1)]
27+
[InlineData("lib/net6.0/Microsoft.Identity.Client.dll", "lib/net6.0-android31.0/Microsoft.Identity.Client.dll", -1)]
28+
[InlineData("path/with/a/lot/of/atoms", "path/with/a/lot", 1)]
29+
public void ValidateComparison(string left, string right, int expectedResult)
30+
{
31+
var comparer = new NugetRelativePathComparer(m_context.StringTable);
32+
var leftPath = RelativePath.Create(m_context.StringTable, left);
33+
var rightPath = RelativePath.Create(m_context.StringTable, right);
34+
35+
var result = comparer.Compare(leftPath, rightPath);
36+
switch (expectedResult)
37+
{
38+
case -1:
39+
Assert.True(result < 0);
40+
break;
41+
case 0:
42+
Assert.True(result == 0);
43+
break;
44+
case 1:
45+
Assert.True(result > 0);
46+
break;
47+
}
48+
}
49+
}
50+
}

0 commit comments

Comments
 (0)