dependabot-nuget 0.266.0 → 0.268.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Analyze.cs +173 -0
- data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs +27 -10
- data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/NuGetContext.cs +12 -1
- data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Requirement.cs +88 -45
- data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/ErrorType.cs +1 -0
- data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/MissingFileException.cs +11 -0
- data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectManager.cs +45 -42
- data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackagesConfigUpdater.cs +1 -0
- data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs +16 -12
- data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdaterWorker.cs +8 -0
- data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +49 -14
- data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs +2 -0
- data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/RequirementTests.cs +1 -0
- data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/MockNuGetPackage.cs +16 -21
- data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.PackagesConfig.cs +360 -0
- data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.Sdk.cs +121 -0
- data/lib/dependabot/nuget/file_fetcher.rb +29 -10
- data/lib/dependabot/nuget/file_updater.rb +17 -9
- data/lib/dependabot/nuget/native_helpers.rb +2 -0
- metadata +6 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3dbd572d869f156e22c4020848387b9b54611ad236c08be6b3ca4cce3e5e7ebc
|
4
|
+
data.tar.gz: f9578fc6352ff07629255fdd1bb2032cda9b0109090507fc39eed9e1b369a68d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8c91cd7ef8d66d4aa48de704d70d41087ad8b6f867946f1cf59038a22f22d06febb5227e637d339fe93ec8fa5c086f8d410e16d00c3f70a4c8c348993de01c28
|
7
|
+
data.tar.gz: 161deea8269b8c3fe00013ef83da37e96ddedf9518e9463bdf952034a03ef00be0fac713495b4706ccbcb3d08d072aec700b7921dcbf01506e8e4d432c4fc634
|
@@ -134,6 +134,179 @@ public partial class EntryPointTests
|
|
134
134
|
);
|
135
135
|
}
|
136
136
|
|
137
|
+
[Fact]
|
138
|
+
public async Task DotNetToolsJsonCanBeAnalyzed()
|
139
|
+
{
|
140
|
+
var repoMetadata = XElement.Parse("""<repository type="git" url="https://nuget.example.com/some-global-tool" />""");
|
141
|
+
await RunAsync(path =>
|
142
|
+
[
|
143
|
+
"analyze",
|
144
|
+
"--repo-root",
|
145
|
+
path,
|
146
|
+
"--discovery-file-path",
|
147
|
+
Path.Join(path, "discovery.json"),
|
148
|
+
"--dependency-file-path",
|
149
|
+
Path.Join(path, "some-global-tool.json"),
|
150
|
+
"--analysis-folder-path",
|
151
|
+
Path.Join(path, AnalyzeWorker.AnalysisDirectoryName),
|
152
|
+
"--verbose",
|
153
|
+
],
|
154
|
+
packages:
|
155
|
+
[
|
156
|
+
MockNuGetPackage.CreateDotNetToolPackage("some-global-tool", "1.0.0", "net8.0", additionalMetadata: [repoMetadata]),
|
157
|
+
MockNuGetPackage.CreateDotNetToolPackage("some-global-tool", "1.1.0", "net8.0", additionalMetadata: [repoMetadata]),
|
158
|
+
],
|
159
|
+
dependencyName: "some-global-tool",
|
160
|
+
initialFiles:
|
161
|
+
[
|
162
|
+
(".config/dotnet-tools.json", """
|
163
|
+
{
|
164
|
+
"version": 1,
|
165
|
+
"isRoot": true,
|
166
|
+
"tools": {
|
167
|
+
"some-global-tool": {
|
168
|
+
"version": "1.0.0",
|
169
|
+
"commands": [
|
170
|
+
"some-global-tool"
|
171
|
+
]
|
172
|
+
}
|
173
|
+
}
|
174
|
+
}
|
175
|
+
"""),
|
176
|
+
("discovery.json", """
|
177
|
+
{
|
178
|
+
"Path": "",
|
179
|
+
"IsSuccess": true,
|
180
|
+
"Projects": [],
|
181
|
+
"DotNetToolsJson": {
|
182
|
+
"FilePath": ".config/dotnet-tools.json",
|
183
|
+
"IsSuccess": true,
|
184
|
+
"Dependencies": [
|
185
|
+
{
|
186
|
+
"Name": "some-global-tool",
|
187
|
+
"Version": "1.0.0",
|
188
|
+
"Type": "DotNetTool",
|
189
|
+
"EvaluationResult": null,
|
190
|
+
"TargetFrameworks": null,
|
191
|
+
"IsDevDependency": false,
|
192
|
+
"IsDirect": false,
|
193
|
+
"IsTransitive": false,
|
194
|
+
"IsOverride": false,
|
195
|
+
"IsUpdate": false,
|
196
|
+
"InfoUrl": null
|
197
|
+
}
|
198
|
+
]
|
199
|
+
}
|
200
|
+
}
|
201
|
+
"""),
|
202
|
+
("some-global-tool.json", """
|
203
|
+
{
|
204
|
+
"Name": "some-global-tool",
|
205
|
+
"Version": "1.0.0",
|
206
|
+
"IsVulnerable": false,
|
207
|
+
"IgnoredVersions": [],
|
208
|
+
"Vulnerabilities": []
|
209
|
+
}
|
210
|
+
"""),
|
211
|
+
],
|
212
|
+
expectedResult: new()
|
213
|
+
{
|
214
|
+
UpdatedVersion = "1.1.0",
|
215
|
+
CanUpdate = true,
|
216
|
+
VersionComesFromMultiDependencyProperty = false,
|
217
|
+
UpdatedDependencies =
|
218
|
+
[
|
219
|
+
new Dependency("some-global-tool", "1.1.0", DependencyType.DotNetTool, TargetFrameworks: null, IsDirect: true, InfoUrl: "https://nuget.example.com/some-global-tool")
|
220
|
+
],
|
221
|
+
}
|
222
|
+
);
|
223
|
+
}
|
224
|
+
|
225
|
+
[Fact]
|
226
|
+
public async Task GlobalJsonCanBeAnalyzed()
|
227
|
+
{
|
228
|
+
var repoMetadata = XElement.Parse("""<repository type="git" url="https://nuget.example.com/some.msbuild.sdk" />""");
|
229
|
+
await RunAsync(path =>
|
230
|
+
[
|
231
|
+
"analyze",
|
232
|
+
"--repo-root",
|
233
|
+
path,
|
234
|
+
"--discovery-file-path",
|
235
|
+
Path.Join(path, "discovery.json"),
|
236
|
+
"--dependency-file-path",
|
237
|
+
Path.Join(path, "Some.MSBuild.Sdk.json"),
|
238
|
+
"--analysis-folder-path",
|
239
|
+
Path.Join(path, AnalyzeWorker.AnalysisDirectoryName),
|
240
|
+
"--verbose",
|
241
|
+
],
|
242
|
+
packages:
|
243
|
+
[
|
244
|
+
MockNuGetPackage.CreateMSBuildSdkPackage("Some.MSBuild.Sdk", "1.0.0", "net8.0", additionalMetadata: [repoMetadata]),
|
245
|
+
MockNuGetPackage.CreateMSBuildSdkPackage("Some.MSBuild.Sdk", "1.1.0", "net8.0", additionalMetadata: [repoMetadata]),
|
246
|
+
],
|
247
|
+
dependencyName: "Some.MSBuild.Sdk",
|
248
|
+
initialFiles:
|
249
|
+
[
|
250
|
+
("global.json", """
|
251
|
+
{
|
252
|
+
"sdk": {
|
253
|
+
"version": "8.0.300",
|
254
|
+
"rollForward": "latestPatch"
|
255
|
+
},
|
256
|
+
"msbuild-sdks": {
|
257
|
+
"Some.MSBuild.Sdk": "1.0.0"
|
258
|
+
}
|
259
|
+
}
|
260
|
+
"""),
|
261
|
+
("discovery.json", """
|
262
|
+
{
|
263
|
+
"Path": "",
|
264
|
+
"IsSuccess": true,
|
265
|
+
"Projects": [],
|
266
|
+
"GlobalJson": {
|
267
|
+
"FilePath": "global.json",
|
268
|
+
"IsSuccess": true,
|
269
|
+
"Dependencies": [
|
270
|
+
{
|
271
|
+
"Name": "Some.MSBuild.Sdk",
|
272
|
+
"Version": "1.0.0",
|
273
|
+
"Type": "MSBuildSdk",
|
274
|
+
"EvaluationResult": null,
|
275
|
+
"TargetFrameworks": null,
|
276
|
+
"IsDevDependency": false,
|
277
|
+
"IsDirect": false,
|
278
|
+
"IsTransitive": false,
|
279
|
+
"IsOverride": false,
|
280
|
+
"IsUpdate": false,
|
281
|
+
"InfoUrl": null
|
282
|
+
}
|
283
|
+
]
|
284
|
+
}
|
285
|
+
}
|
286
|
+
"""),
|
287
|
+
("Some.MSBuild.Sdk.json", """
|
288
|
+
{
|
289
|
+
"Name": "Some.MSBuild.Sdk",
|
290
|
+
"Version": "1.0.0",
|
291
|
+
"IsVulnerable": false,
|
292
|
+
"IgnoredVersions": [],
|
293
|
+
"Vulnerabilities": []
|
294
|
+
}
|
295
|
+
"""),
|
296
|
+
],
|
297
|
+
expectedResult: new()
|
298
|
+
{
|
299
|
+
UpdatedVersion = "1.1.0",
|
300
|
+
CanUpdate = true,
|
301
|
+
VersionComesFromMultiDependencyProperty = false,
|
302
|
+
UpdatedDependencies =
|
303
|
+
[
|
304
|
+
new Dependency("Some.MSBuild.Sdk", "1.1.0", DependencyType.MSBuildSdk, TargetFrameworks: null, IsDirect: true, InfoUrl: "https://nuget.example.com/some.msbuild.sdk")
|
305
|
+
],
|
306
|
+
}
|
307
|
+
);
|
308
|
+
}
|
309
|
+
|
137
310
|
private static async Task RunAsync(Func<string, string[]> getArgs, string dependencyName, TestFile[] initialFiles, ExpectedAnalysisResult expectedResult, MockNuGetPackage[]? packages = null)
|
138
311
|
{
|
139
312
|
var actualResult = await RunAnalyzerAsync(dependencyName, initialFiles, async path =>
|
@@ -51,23 +51,21 @@ public partial class AnalyzeWorker
|
|
51
51
|
=> p.Dependencies.Where(d => !d.IsTransitive &&
|
52
52
|
d.EvaluationResult?.RootPropertyName is not null)
|
53
53
|
).ToImmutableArray();
|
54
|
+
var dotnetToolsHasDependency = discovery.DotNetToolsJson?.Dependencies.Any(d => d.Name.Equals(dependencyInfo.Name, StringComparison.OrdinalIgnoreCase)) == true;
|
55
|
+
var globalJsonHasDependency = discovery.GlobalJson?.Dependencies.Any(d => d.Name.Equals(dependencyInfo.Name, StringComparison.OrdinalIgnoreCase)) == true;
|
54
56
|
|
55
57
|
bool usesMultiDependencyProperty = false;
|
56
58
|
NuGetVersion? updatedVersion = null;
|
57
59
|
ImmutableArray<Dependency> updatedDependencies = [];
|
58
60
|
|
59
|
-
bool
|
61
|
+
bool isProjectUpdateNecessary = IsUpdateNecessary(dependencyInfo, projectsWithDependency);
|
62
|
+
var isUpdateNecessary = isProjectUpdateNecessary || dotnetToolsHasDependency || globalJsonHasDependency;
|
60
63
|
using var nugetContext = new NuGetContext(startingDirectory);
|
61
64
|
AnalysisResult result;
|
62
65
|
try
|
63
66
|
{
|
64
67
|
if (isUpdateNecessary)
|
65
68
|
{
|
66
|
-
if (!Directory.Exists(nugetContext.TempPackageDirectory))
|
67
|
-
{
|
68
|
-
Directory.CreateDirectory(nugetContext.TempPackageDirectory);
|
69
|
-
}
|
70
|
-
|
71
69
|
_logger.Log($" Determining multi-dependency property.");
|
72
70
|
var multiDependencies = DetermineMultiDependencyDetails(
|
73
71
|
discovery,
|
@@ -99,16 +97,35 @@ public partial class AnalyzeWorker
|
|
99
97
|
CancellationToken.None);
|
100
98
|
|
101
99
|
_logger.Log($" Finding updated peer dependencies.");
|
102
|
-
|
103
|
-
|
100
|
+
if (updatedVersion is null)
|
101
|
+
{
|
102
|
+
updatedDependencies = [];
|
103
|
+
}
|
104
|
+
else if (isProjectUpdateNecessary)
|
105
|
+
{
|
106
|
+
updatedDependencies = await FindUpdatedDependenciesAsync(
|
104
107
|
repoRoot,
|
105
108
|
discovery,
|
106
109
|
dependenciesToUpdate,
|
107
110
|
updatedVersion,
|
108
111
|
nugetContext,
|
109
112
|
_logger,
|
110
|
-
CancellationToken.None)
|
111
|
-
|
113
|
+
CancellationToken.None);
|
114
|
+
}
|
115
|
+
else if (dotnetToolsHasDependency)
|
116
|
+
{
|
117
|
+
var infoUrl = await nugetContext.GetPackageInfoUrlAsync(dependencyInfo.Name, updatedVersion.ToNormalizedString(), CancellationToken.None);
|
118
|
+
updatedDependencies = [new Dependency(dependencyInfo.Name, updatedVersion.ToNormalizedString(), DependencyType.DotNetTool, IsDirect: true, InfoUrl: infoUrl)];
|
119
|
+
}
|
120
|
+
else if (globalJsonHasDependency)
|
121
|
+
{
|
122
|
+
var infoUrl = await nugetContext.GetPackageInfoUrlAsync(dependencyInfo.Name, updatedVersion.ToNormalizedString(), CancellationToken.None);
|
123
|
+
updatedDependencies = [new Dependency(dependencyInfo.Name, updatedVersion.ToNormalizedString(), DependencyType.MSBuildSdk, IsDirect: true, InfoUrl: infoUrl)];
|
124
|
+
}
|
125
|
+
else
|
126
|
+
{
|
127
|
+
throw new InvalidOperationException("Unreachable.");
|
128
|
+
}
|
112
129
|
|
113
130
|
//TODO: At this point we should add the peer dependencies to a queue where
|
114
131
|
// we will analyze them one by one to see if they themselves are part of a
|
@@ -37,12 +37,23 @@ internal record NuGetContext : IDisposable
|
|
37
37
|
.Where(p => p.IsEnabled)
|
38
38
|
.ToImmutableArray();
|
39
39
|
Logger = logger ?? NullLogger.Instance;
|
40
|
-
TempPackageDirectory = Path.Combine(Path.GetTempPath(), ".
|
40
|
+
TempPackageDirectory = Path.Combine(Path.GetTempPath(), $"dependabot-packages_{Guid.NewGuid():d}");
|
41
|
+
Directory.CreateDirectory(TempPackageDirectory);
|
41
42
|
}
|
42
43
|
|
43
44
|
public void Dispose()
|
44
45
|
{
|
45
46
|
SourceCacheContext.Dispose();
|
47
|
+
if (Directory.Exists(TempPackageDirectory))
|
48
|
+
{
|
49
|
+
try
|
50
|
+
{
|
51
|
+
Directory.Delete(TempPackageDirectory, recursive: true);
|
52
|
+
}
|
53
|
+
catch
|
54
|
+
{
|
55
|
+
}
|
56
|
+
}
|
46
57
|
}
|
47
58
|
|
48
59
|
private readonly Dictionary<PackageIdentity, string?> _packageInfoUrlCache = new();
|
@@ -12,7 +12,59 @@ namespace NuGetUpdater.Core.Analyze;
|
|
12
12
|
/// See Gem::Version for a description on how versions and requirements work
|
13
13
|
/// together in RubyGems.
|
14
14
|
/// </remarks>
|
15
|
-
public class Requirement
|
15
|
+
public abstract class Requirement
|
16
|
+
{
|
17
|
+
public abstract bool IsSatisfiedBy(NuGetVersion version);
|
18
|
+
|
19
|
+
private static readonly Dictionary<string, Version> BumpMap = [];
|
20
|
+
/// <summary>
|
21
|
+
/// Return a new version object where the next to the last revision
|
22
|
+
/// number is one greater (e.g., 5.3.1 => 5.4).
|
23
|
+
/// </summary>
|
24
|
+
/// <remarks>
|
25
|
+
/// This logic intended to be similar to RubyGems Gem::Version#bump
|
26
|
+
/// </remarks>
|
27
|
+
public static Version Bump(NuGetVersion version)
|
28
|
+
{
|
29
|
+
if (BumpMap.TryGetValue(version.OriginalVersion!, out var bumpedVersion))
|
30
|
+
{
|
31
|
+
return bumpedVersion;
|
32
|
+
}
|
33
|
+
|
34
|
+
var versionParts = version.OriginalVersion! // Get the original string this version was created from
|
35
|
+
.Split('-')[0] // Get the version part without pre-release
|
36
|
+
.Split('.') // Split into Major.Minor.Patch.Revision if present
|
37
|
+
.Select(int.Parse)
|
38
|
+
.ToArray();
|
39
|
+
|
40
|
+
if (versionParts.Length > 1)
|
41
|
+
{
|
42
|
+
versionParts = versionParts[..^1]; // Remove the last part
|
43
|
+
}
|
44
|
+
|
45
|
+
versionParts[^1]++; // Increment the new last part
|
46
|
+
|
47
|
+
bumpedVersion = NuGetVersion.Parse(string.Join('.', versionParts)).Version;
|
48
|
+
BumpMap[version.OriginalVersion!] = bumpedVersion;
|
49
|
+
|
50
|
+
return bumpedVersion;
|
51
|
+
}
|
52
|
+
|
53
|
+
public static Requirement Parse(string requirement)
|
54
|
+
{
|
55
|
+
var specificParts = requirement.Split(',');
|
56
|
+
if (specificParts.Length == 1)
|
57
|
+
{
|
58
|
+
return IndividualRequirement.ParseIndividual(requirement);
|
59
|
+
}
|
60
|
+
|
61
|
+
var specificRequirements = specificParts.Select(IndividualRequirement.ParseIndividual).ToArray();
|
62
|
+
return new MultiPartRequirement(specificRequirements);
|
63
|
+
}
|
64
|
+
}
|
65
|
+
|
66
|
+
|
67
|
+
public class IndividualRequirement : Requirement
|
16
68
|
{
|
17
69
|
private static readonly ImmutableDictionary<string, Func<NuGetVersion, NuGetVersion, bool>> Operators = new Dictionary<string, Func<NuGetVersion, NuGetVersion, bool>>()
|
18
70
|
{
|
@@ -25,30 +77,10 @@ public class Requirement
|
|
25
77
|
["~>"] = (v, r) => v >= r && v.Version < Bump(r),
|
26
78
|
}.ToImmutableDictionary();
|
27
79
|
|
28
|
-
public static Requirement Parse(string requirement)
|
29
|
-
{
|
30
|
-
var splitIndex = requirement.LastIndexOfAny(['=', '>', '<']);
|
31
|
-
|
32
|
-
// Throw if the requirement is all operator and no version.
|
33
|
-
if (splitIndex == requirement.Length - 1)
|
34
|
-
{
|
35
|
-
throw new ArgumentException($"`{requirement}` is a invalid requirement string", nameof(requirement));
|
36
|
-
}
|
37
|
-
|
38
|
-
string[] parts = splitIndex == -1
|
39
|
-
? [requirement.Trim()]
|
40
|
-
: [requirement[..(splitIndex + 1)].Trim(), requirement[(splitIndex + 1)..].Trim()];
|
41
|
-
|
42
|
-
var op = parts.Length == 1 ? "=" : parts[0];
|
43
|
-
var version = NuGetVersion.Parse(parts[^1]);
|
44
|
-
|
45
|
-
return new Requirement(op, version);
|
46
|
-
}
|
47
|
-
|
48
80
|
public string Operator { get; }
|
49
81
|
public NuGetVersion Version { get; }
|
50
82
|
|
51
|
-
public
|
83
|
+
public IndividualRequirement(string op, NuGetVersion version)
|
52
84
|
{
|
53
85
|
if (!Operators.ContainsKey(op))
|
54
86
|
{
|
@@ -64,42 +96,53 @@ public class Requirement
|
|
64
96
|
return $"{Operator} {Version}";
|
65
97
|
}
|
66
98
|
|
67
|
-
public bool IsSatisfiedBy(NuGetVersion version)
|
99
|
+
public override bool IsSatisfiedBy(NuGetVersion version)
|
68
100
|
{
|
69
101
|
return Operators[Operator](version, Version);
|
70
102
|
}
|
71
103
|
|
72
|
-
|
73
|
-
/// <summary>
|
74
|
-
/// Return a new version object where the next to the last revision
|
75
|
-
/// number is one greater (e.g., 5.3.1 => 5.4).
|
76
|
-
/// </summary>
|
77
|
-
/// <remarks>
|
78
|
-
/// This logic intended to be similar to RubyGems Gem::Version#bump
|
79
|
-
/// </remarks>
|
80
|
-
public static Version Bump(NuGetVersion version)
|
104
|
+
public static IndividualRequirement ParseIndividual(string requirement)
|
81
105
|
{
|
82
|
-
|
106
|
+
var splitIndex = requirement.LastIndexOfAny(['=', '>', '<']);
|
107
|
+
|
108
|
+
// Throw if the requirement is all operator and no version.
|
109
|
+
if (splitIndex == requirement.Length - 1)
|
83
110
|
{
|
84
|
-
|
111
|
+
throw new ArgumentException($"`{requirement}` is a invalid requirement string", nameof(requirement));
|
85
112
|
}
|
86
113
|
|
87
|
-
|
88
|
-
.
|
89
|
-
.
|
90
|
-
.Select(int.Parse)
|
91
|
-
.ToArray();
|
114
|
+
string[] parts = splitIndex == -1
|
115
|
+
? [requirement.Trim()]
|
116
|
+
: [requirement[..(splitIndex + 1)].Trim(), requirement[(splitIndex + 1)..].Trim()];
|
92
117
|
|
93
|
-
|
118
|
+
var op = parts.Length == 1 ? "=" : parts[0];
|
119
|
+
var version = NuGetVersion.Parse(parts[^1]);
|
120
|
+
|
121
|
+
return new IndividualRequirement(op, version);
|
122
|
+
}
|
123
|
+
}
|
124
|
+
|
125
|
+
public class MultiPartRequirement : Requirement
|
126
|
+
{
|
127
|
+
public ImmutableArray<IndividualRequirement> Parts { get; }
|
128
|
+
|
129
|
+
public MultiPartRequirement(IndividualRequirement[] parts)
|
130
|
+
{
|
131
|
+
if (parts.Length <= 1)
|
94
132
|
{
|
95
|
-
|
133
|
+
throw new ArgumentException("At least two parts are required", nameof(parts));
|
96
134
|
}
|
97
135
|
|
98
|
-
|
136
|
+
Parts = parts.ToImmutableArray();
|
137
|
+
}
|
99
138
|
|
100
|
-
|
101
|
-
|
139
|
+
public override string ToString()
|
140
|
+
{
|
141
|
+
return string.Join(", ", Parts);
|
142
|
+
}
|
102
143
|
|
103
|
-
|
144
|
+
public override bool IsSatisfiedBy(NuGetVersion version)
|
145
|
+
{
|
146
|
+
return Parts.All(part => part.IsSatisfiedBy(version));
|
104
147
|
}
|
105
148
|
}
|
@@ -149,42 +149,6 @@ internal static class BindingRedirectManager
|
|
149
149
|
return (element.Name == "None" && string.Equals(path, "app.config", StringComparison.OrdinalIgnoreCase))
|
150
150
|
|| (element.Name == "Content" && string.Equals(path, "web.config", StringComparison.OrdinalIgnoreCase));
|
151
151
|
}
|
152
|
-
|
153
|
-
static string GetConfigFileName(XmlDocumentSyntax document)
|
154
|
-
{
|
155
|
-
var guidValue = document.Descendants()
|
156
|
-
.Where(static x => x.Name == "PropertyGroup")
|
157
|
-
.SelectMany(static x => x.Elements.Where(static x => x.Name == "ProjectGuid"))
|
158
|
-
.FirstOrDefault()
|
159
|
-
?.GetContentValue();
|
160
|
-
return guidValue switch
|
161
|
-
{
|
162
|
-
"{E24C65DC-7377-472B-9ABA-BC803B73C61A}" or "{349C5851-65DF-11DA-9384-00065B846F21}" => "Web.config",
|
163
|
-
_ => "App.config"
|
164
|
-
};
|
165
|
-
}
|
166
|
-
|
167
|
-
static string GenerateDefaultAppConfig(XmlDocumentSyntax document)
|
168
|
-
{
|
169
|
-
var frameworkVersion = GetFrameworkVersion(document);
|
170
|
-
return $"""
|
171
|
-
<?xml version="1.0" encoding="utf-8" ?>
|
172
|
-
<configuration>
|
173
|
-
<startup>
|
174
|
-
<supportedRuntime version="v4.0" sku=".NETFramework,Version={frameworkVersion}" />
|
175
|
-
</startup>
|
176
|
-
</configuration>
|
177
|
-
""";
|
178
|
-
}
|
179
|
-
|
180
|
-
static string? GetFrameworkVersion(XmlDocumentSyntax document)
|
181
|
-
{
|
182
|
-
return document.Descendants()
|
183
|
-
.Where(static x => x.Name == "PropertyGroup")
|
184
|
-
.SelectMany(static x => x.Elements.Where(static x => x.Name == "TargetFrameworkVersion"))
|
185
|
-
.FirstOrDefault()
|
186
|
-
?.GetContentValue();
|
187
|
-
}
|
188
152
|
}
|
189
153
|
|
190
154
|
private static string AddBindingRedirects(ConfigurationFile configFile, IEnumerable<Runtime_AssemblyBinding> bindingRedirects)
|
@@ -213,10 +177,24 @@ internal static class BindingRedirectManager
|
|
213
177
|
|
214
178
|
foreach (var bindingRedirect in bindingRedirects)
|
215
179
|
{
|
216
|
-
//
|
217
|
-
|
180
|
+
// If the binding redirect already exists in config, update it. Otherwise, add it.
|
181
|
+
var bindingAssemblyIdentity = new AssemblyIdentity(bindingRedirect.Name, bindingRedirect.PublicKeyToken);
|
182
|
+
if (currentBindings.Contains(bindingAssemblyIdentity))
|
218
183
|
{
|
219
|
-
|
184
|
+
// Check if there are multiple bindings in config for this assembly and remove all but the first one.
|
185
|
+
// Normally there should only be one binding per assembly identity unless the config is malformed, which we'll fix here like NuGet.exe would.
|
186
|
+
var existingBindings = currentBindings[bindingAssemblyIdentity];
|
187
|
+
if (existingBindings.Any())
|
188
|
+
{
|
189
|
+
// Remove all but the first element
|
190
|
+
foreach (var bindingElement in existingBindings.Skip(1))
|
191
|
+
{
|
192
|
+
RemoveElement(bindingElement);
|
193
|
+
}
|
194
|
+
|
195
|
+
// Update the first one with the new binding
|
196
|
+
UpdateBindingRedirectElement(existingBindings.First(), bindingRedirect);
|
197
|
+
}
|
220
198
|
}
|
221
199
|
else
|
222
200
|
{
|
@@ -228,7 +206,10 @@ internal static class BindingRedirectManager
|
|
228
206
|
}
|
229
207
|
}
|
230
208
|
|
231
|
-
return
|
209
|
+
return string.Concat(
|
210
|
+
document.Declaration?.ToString() ?? String.Empty, // Ensure the <?xml> declaration node is preserved, if present
|
211
|
+
document.ToString()
|
212
|
+
);
|
232
213
|
|
233
214
|
static XDocument GetConfiguration(string configFileContent)
|
234
215
|
{
|
@@ -278,7 +259,7 @@ internal static class BindingRedirectManager
|
|
278
259
|
}
|
279
260
|
}
|
280
261
|
|
281
|
-
static
|
262
|
+
static ILookup<AssemblyIdentity, XElement> GetAssemblyBindings(XElement runtime)
|
282
263
|
{
|
283
264
|
var dependencyAssemblyElements = runtime.Elements(AssemblyBindingName)
|
284
265
|
.Elements(DependentAssemblyName);
|
@@ -291,7 +272,12 @@ internal static class BindingRedirectManager
|
|
291
272
|
});
|
292
273
|
|
293
274
|
// Return a mapping from binding to element
|
294
|
-
|
275
|
+
// It is possible that multiple elements exist for the same assembly identity, so use a lookup (1:*) instead of a dictionary (1:1)
|
276
|
+
return assemblyElementPairs.ToLookup(
|
277
|
+
p => new AssemblyIdentity(p.Binding.Name, p.Binding.PublicKeyToken),
|
278
|
+
p => p.Element,
|
279
|
+
new AssemblyIdentityIgnoreCaseComparer()
|
280
|
+
);
|
295
281
|
}
|
296
282
|
|
297
283
|
static XElement GetAssemblyBindingElement(XElement runtime)
|
@@ -309,4 +295,21 @@ internal static class BindingRedirectManager
|
|
309
295
|
return assemblyBinding;
|
310
296
|
}
|
311
297
|
}
|
298
|
+
|
299
|
+
internal sealed record AssemblyIdentity(string Name, string PublicKeyToken);
|
300
|
+
|
301
|
+
// Case-insensitive comparer. This helps avoid creating duplicate binding redirects when there is a case form mismatch between assembly identities.
|
302
|
+
// Especially important for PublicKeyToken which is typically lowercase (using NuGet.exe), but can also be uppercase when using other tools (e.g. Visual Studio auto-resolve assembly conflicts feature).
|
303
|
+
internal sealed class AssemblyIdentityIgnoreCaseComparer : IEqualityComparer<AssemblyIdentity>
|
304
|
+
{
|
305
|
+
public bool Equals(AssemblyIdentity? x, AssemblyIdentity? y) =>
|
306
|
+
string.Equals(x?.Name, y?.Name, StringComparison.OrdinalIgnoreCase) &&
|
307
|
+
string.Equals(x?.PublicKeyToken, y?.PublicKeyToken, StringComparison.OrdinalIgnoreCase);
|
308
|
+
|
309
|
+
public int GetHashCode(AssemblyIdentity obj) =>
|
310
|
+
HashCode.Combine(
|
311
|
+
obj.Name?.ToLowerInvariant(),
|
312
|
+
obj.PublicKeyToken?.ToLowerInvariant()
|
313
|
+
);
|
314
|
+
}
|
312
315
|
}
|