dependabot-nuget 0.265.0 → 0.267.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/UpdateCommand.cs +6 -6
  3. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Analyze.cs +173 -0
  4. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalysisResult.cs +1 -1
  5. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs +92 -79
  6. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/CompatabilityChecker.cs +21 -8
  7. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/NuGetContext.cs +36 -9
  8. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Requirement.cs +88 -45
  9. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs +33 -16
  10. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs +44 -25
  11. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs +1 -1
  12. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/ErrorType.cs +9 -0
  13. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/ProjectBuildFile.cs +2 -2
  14. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/MissingFileException.cs +11 -0
  15. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/NativeResult.cs +8 -0
  16. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectManager.cs +45 -42
  17. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackagesConfigUpdater.cs +19 -1
  18. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs +2 -1
  19. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdateOperationResult.cs +5 -0
  20. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdaterWorker.cs +70 -22
  21. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +29 -1
  22. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTestBase.cs +6 -2
  23. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs +450 -0
  24. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/CompatibilityCheckerTests.cs +23 -0
  25. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/RequirementTests.cs +1 -0
  26. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs +2 -0
  27. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs +148 -0
  28. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs +1 -1
  29. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/MockNuGetPackage.cs +17 -22
  30. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/TestHttpServer.cs +81 -0
  31. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTestBase.cs +27 -7
  32. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.Mixed.cs +32 -0
  33. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.PackagesConfig.cs +447 -2
  34. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.Sdk.cs +88 -0
  35. data/lib/dependabot/nuget/analysis/dependency_analysis.rb +3 -0
  36. data/lib/dependabot/nuget/file_fetcher.rb +30 -11
  37. data/lib/dependabot/nuget/file_updater.rb +2 -0
  38. data/lib/dependabot/nuget/metadata_finder.rb +160 -2
  39. data/lib/dependabot/nuget/native_discovery/native_workspace_discovery.rb +3 -0
  40. data/lib/dependabot/nuget/native_helpers.rb +36 -3
  41. data/lib/dependabot/nuget/native_update_checker/native_update_checker.rb +1 -0
  42. data/lib/dependabot/nuget/nuget_config_credential_helpers.rb +3 -0
  43. metadata +12 -7
@@ -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 Requirement(string op, NuGetVersion version)
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
- private static readonly Dictionary<string, Version> BumpMap = [];
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
- if (BumpMap.TryGetValue(version.OriginalVersion!, out var bumpedVersion))
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
- return bumpedVersion;
111
+ throw new ArgumentException($"`{requirement}` is a invalid requirement string", nameof(requirement));
85
112
  }
86
113
 
87
- var versionParts = version.OriginalVersion! // Get the original string this version was created from
88
- .Split('-')[0] // Get the version part without pre-release
89
- .Split('.') // Split into Major.Minor.Patch.Revision if present
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
- if (versionParts.Length > 1)
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
- versionParts = versionParts[..^1]; // Remove the last part
133
+ throw new ArgumentException("At least two parts are required", nameof(parts));
96
134
  }
97
135
 
98
- versionParts[^1]++; // Increment the new last part
136
+ Parts = parts.ToImmutableArray();
137
+ }
99
138
 
100
- bumpedVersion = NuGetVersion.Parse(string.Join('.', versionParts)).Version;
101
- BumpMap[version.OriginalVersion!] = bumpedVersion;
139
+ public override string ToString()
140
+ {
141
+ return string.Join(", ", Parts);
142
+ }
102
143
 
103
- return bumpedVersion;
144
+ public override bool IsSatisfiedBy(NuGetVersion version)
145
+ {
146
+ return Parts.All(part => part.IsSatisfiedBy(version));
104
147
  }
105
148
  }
@@ -66,15 +66,24 @@ internal static class VersionFinder
66
66
  continue;
67
67
  }
68
68
 
69
- var existsInFeed = await feed.Exists(
70
- packageId,
71
- includePrerelease,
72
- includeUnlisted: false,
73
- nugetContext.SourceCacheContext,
74
- NullLogger.Instance,
75
- cancellationToken);
76
- if (!existsInFeed)
69
+ try
70
+ {
71
+ // a non-compliant v2 API returning 404 can cause this to throw
72
+ var existsInFeed = await feed.Exists(
73
+ packageId,
74
+ includePrerelease,
75
+ includeUnlisted: false,
76
+ nugetContext.SourceCacheContext,
77
+ NullLogger.Instance,
78
+ cancellationToken);
79
+ if (!existsInFeed)
80
+ {
81
+ continue;
82
+ }
83
+ }
84
+ catch (FatalProtocolException)
77
85
  {
86
+ // if anything goes wrong here, the package source obviously doesn't contain the requested package
78
87
  continue;
79
88
  }
80
89
 
@@ -162,15 +171,23 @@ internal static class VersionFinder
162
171
  continue;
163
172
  }
164
173
 
165
- var existsInFeed = await feed.Exists(
166
- new PackageIdentity(packageId, version),
167
- includeUnlisted: false,
168
- nugetContext.SourceCacheContext,
169
- NullLogger.Instance,
170
- cancellationToken);
171
- if (existsInFeed)
174
+ try
175
+ {
176
+ // a non-compliant v2 API returning 404 can cause this to throw
177
+ var existsInFeed = await feed.Exists(
178
+ new PackageIdentity(packageId, version),
179
+ includeUnlisted: false,
180
+ nugetContext.SourceCacheContext,
181
+ NullLogger.Instance,
182
+ cancellationToken);
183
+ if (existsInFeed)
184
+ {
185
+ return true;
186
+ }
187
+ }
188
+ catch (FatalProtocolException)
172
189
  {
173
- return true;
190
+ // if anything goes wrong here, the package source obviously doesn't contain the requested package
174
191
  }
175
192
  }
176
193
 
@@ -1,4 +1,5 @@
1
1
  using System.Collections.Immutable;
2
+ using System.Net;
2
3
  using System.Text.Json;
3
4
  using System.Text.Json.Serialization;
4
5
 
@@ -6,6 +7,7 @@ using Microsoft.Build.Construction;
6
7
  using Microsoft.Build.Definition;
7
8
  using Microsoft.Build.Evaluation;
8
9
 
10
+ using NuGetUpdater.Core.Analyze;
9
11
  using NuGetUpdater.Core.Utilities;
10
12
 
11
13
  namespace NuGetUpdater.Core.Discover;
@@ -47,43 +49,60 @@ public partial class DiscoveryWorker
47
49
  DirectoryPackagesPropsDiscoveryResult? directoryPackagesPropsDiscovery = null;
48
50
 
49
51
  ImmutableArray<ProjectDiscoveryResult> projectResults = [];
52
+ WorkspaceDiscoveryResult result;
50
53
 
51
- if (Directory.Exists(workspacePath))
54
+ try
52
55
  {
53
- _logger.Log($"Discovering build files in workspace [{workspacePath}].");
56
+ if (Directory.Exists(workspacePath))
57
+ {
58
+ _logger.Log($"Discovering build files in workspace [{workspacePath}].");
54
59
 
55
- dotNetToolsJsonDiscovery = DotNetToolsJsonDiscovery.Discover(repoRootPath, workspacePath, _logger);
56
- globalJsonDiscovery = GlobalJsonDiscovery.Discover(repoRootPath, workspacePath, _logger);
60
+ dotNetToolsJsonDiscovery = DotNetToolsJsonDiscovery.Discover(repoRootPath, workspacePath, _logger);
61
+ globalJsonDiscovery = GlobalJsonDiscovery.Discover(repoRootPath, workspacePath, _logger);
57
62
 
58
- if (globalJsonDiscovery is not null)
59
- {
60
- await TryRestoreMSBuildSdksAsync(repoRootPath, workspacePath, globalJsonDiscovery.Dependencies, _logger);
61
- }
63
+ if (globalJsonDiscovery is not null)
64
+ {
65
+ await TryRestoreMSBuildSdksAsync(repoRootPath, workspacePath, globalJsonDiscovery.Dependencies, _logger);
66
+ }
62
67
 
63
- projectResults = await RunForDirectoryAsnyc(repoRootPath, workspacePath);
68
+ // this next line should throw or something
69
+ projectResults = await RunForDirectoryAsnyc(repoRootPath, workspacePath);
64
70
 
65
- directoryPackagesPropsDiscovery = DirectoryPackagesPropsDiscovery.Discover(repoRootPath, workspacePath, projectResults, _logger);
71
+ directoryPackagesPropsDiscovery = DirectoryPackagesPropsDiscovery.Discover(repoRootPath, workspacePath, projectResults, _logger);
66
72
 
67
- if (directoryPackagesPropsDiscovery is not null)
73
+ if (directoryPackagesPropsDiscovery is not null)
74
+ {
75
+ projectResults = projectResults.Remove(projectResults.First(p => p.FilePath.Equals(directoryPackagesPropsDiscovery.FilePath, StringComparison.OrdinalIgnoreCase)));
76
+ }
77
+ }
78
+ else
68
79
  {
69
- projectResults = projectResults.Remove(projectResults.First(p => p.FilePath.Equals(directoryPackagesPropsDiscovery.FilePath, StringComparison.OrdinalIgnoreCase)));
80
+ _logger.Log($"Workspace path [{workspacePath}] does not exist.");
70
81
  }
82
+
83
+ result = new WorkspaceDiscoveryResult
84
+ {
85
+ Path = initialWorkspacePath,
86
+ DotNetToolsJson = dotNetToolsJsonDiscovery,
87
+ GlobalJson = globalJsonDiscovery,
88
+ DirectoryPackagesProps = directoryPackagesPropsDiscovery,
89
+ Projects = projectResults.OrderBy(p => p.FilePath).ToImmutableArray(),
90
+ };
71
91
  }
72
- else
92
+ catch (HttpRequestException ex)
93
+ when (ex.StatusCode == HttpStatusCode.Unauthorized || ex.StatusCode == HttpStatusCode.Forbidden)
73
94
  {
74
- _logger.Log($"Workspace path [{workspacePath}] does not exist.");
95
+ // TODO: consolidate this error handling between AnalyzeWorker, DiscoveryWorker, and UpdateWorker
96
+ result = new WorkspaceDiscoveryResult
97
+ {
98
+ ErrorType = ErrorType.AuthenticationFailure,
99
+ ErrorDetails = "(" + string.Join("|", NuGetContext.GetPackageSourceUrls(workspacePath)) + ")",
100
+ Path = initialWorkspacePath,
101
+ Projects = [],
102
+ };
75
103
  }
76
104
 
77
- var result = new WorkspaceDiscoveryResult
78
- {
79
- Path = initialWorkspacePath,
80
- DotNetToolsJson = dotNetToolsJsonDiscovery,
81
- GlobalJson = globalJsonDiscovery,
82
- DirectoryPackagesProps = directoryPackagesPropsDiscovery,
83
- Projects = projectResults.OrderBy(p => p.FilePath).ToImmutableArray(),
84
- };
85
-
86
- await WriteResults(repoRootPath, outputPath, result);
105
+ await WriteResultsAsync(repoRootPath, outputPath, result);
87
106
 
88
107
  _logger.Log("Discovery complete.");
89
108
 
@@ -293,7 +312,7 @@ public partial class DiscoveryWorker
293
312
  return [.. results.Values];
294
313
  }
295
314
 
296
- private static async Task WriteResults(string repoRootPath, string outputPath, WorkspaceDiscoveryResult result)
315
+ internal static async Task WriteResultsAsync(string repoRootPath, string outputPath, WorkspaceDiscoveryResult result)
297
316
  {
298
317
  var resultPath = Path.IsPathRooted(outputPath)
299
318
  ? outputPath
@@ -2,7 +2,7 @@ using System.Collections.Immutable;
2
2
 
3
3
  namespace NuGetUpdater.Core.Discover;
4
4
 
5
- public sealed record WorkspaceDiscoveryResult
5
+ public sealed record WorkspaceDiscoveryResult : NativeResult
6
6
  {
7
7
  public required string Path { get; init; }
8
8
  public bool IsSuccess { get; init; } = true;
@@ -0,0 +1,9 @@
1
+ namespace NuGetUpdater.Core;
2
+
3
+ public enum ErrorType
4
+ {
5
+ // TODO: add `Unknown` option to track all other failure types
6
+ None,
7
+ AuthenticationFailure,
8
+ MissingFile,
9
+ }
@@ -93,11 +93,11 @@ internal sealed class ProjectBuildFile : XmlBuildFile
93
93
  {
94
94
  var isUpdate = false;
95
95
 
96
- var name = element.GetAttributeOrSubElementValue("Include", StringComparison.OrdinalIgnoreCase);
96
+ var name = element.GetAttributeOrSubElementValue("Include", StringComparison.OrdinalIgnoreCase)?.Trim();
97
97
  if (name is null)
98
98
  {
99
99
  isUpdate = true;
100
- name = element.GetAttributeOrSubElementValue("Update", StringComparison.OrdinalIgnoreCase);
100
+ name = element.GetAttributeOrSubElementValue("Update", StringComparison.OrdinalIgnoreCase)?.Trim();
101
101
  }
102
102
 
103
103
  if (name is null || name.StartsWith("@("))
@@ -0,0 +1,11 @@
1
+ namespace NuGetUpdater.Core;
2
+
3
+ internal class MissingFileException : Exception
4
+ {
5
+ public string FilePath { get; }
6
+
7
+ public MissingFileException(string filePath)
8
+ {
9
+ FilePath = filePath;
10
+ }
11
+ }
@@ -0,0 +1,8 @@
1
+ namespace NuGetUpdater.Core;
2
+
3
+ public record NativeResult
4
+ {
5
+ // TODO: nullable not required, `ErrorType.None` is the default anyway
6
+ public ErrorType? ErrorType { get; init; }
7
+ public string? ErrorDetails { get; init; }
8
+ }
@@ -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
- // Look to see if we already have this in the list of bindings already in config.
217
- if (currentBindings.TryGetValue((bindingRedirect.Name, bindingRedirect.PublicKeyToken), out var existingBinding))
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
- UpdateBindingRedirectElement(existingBinding, bindingRedirect);
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 document.ToString();
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 Dictionary<(string Name, string PublicKeyToken), XElement> GetAssemblyBindings(XElement runtime)
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
- return assemblyElementPairs.ToDictionary(p => (p.Binding.Name, p.Binding.PublicKeyToken), p => p.Element);
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
  }
@@ -1,3 +1,4 @@
1
+ using System.Diagnostics;
1
2
  using System.Text;
2
3
  using System.Xml.Linq;
3
4
  using System.Xml.XPath;
@@ -102,9 +103,9 @@ internal static class PackagesConfigUpdater
102
103
  Console.SetError(writer);
103
104
 
104
105
  var currentDir = Environment.CurrentDirectory;
106
+ var existingSpawnedProcesses = GetLikelyNuGetSpawnedProcesses();
105
107
  try
106
108
  {
107
-
108
109
  Environment.CurrentDirectory = projectDirectory;
109
110
  var retryingAfterRestore = false;
110
111
 
@@ -144,6 +145,8 @@ internal static class PackagesConfigUpdater
144
145
  goto doRestore;
145
146
  }
146
147
 
148
+ MSBuildHelper.ThrowOnUnauthenticatedFeed(fullOutput);
149
+ MSBuildHelper.ThrowOnMissingFile(fullOutput);
147
150
  throw new Exception(fullOutput);
148
151
  }
149
152
  }
@@ -157,9 +160,24 @@ internal static class PackagesConfigUpdater
157
160
  Environment.CurrentDirectory = currentDir;
158
161
  Console.SetOut(originalOut);
159
162
  Console.SetError(originalError);
163
+
164
+ // NuGet.exe can spawn processes that hold on to the temporary directory, so we need to kill them
165
+ var currentSpawnedProcesses = GetLikelyNuGetSpawnedProcesses();
166
+ var deltaSpawnedProcesses = currentSpawnedProcesses.Except(existingSpawnedProcesses).ToArray();
167
+ foreach (var credProvider in deltaSpawnedProcesses)
168
+ {
169
+ logger.Log($"Ending spawned credential provider process");
170
+ credProvider.Kill();
171
+ }
160
172
  }
161
173
  }
162
174
 
175
+ private static Process[] GetLikelyNuGetSpawnedProcesses()
176
+ {
177
+ var processes = Process.GetProcesses().Where(p => p.ProcessName.StartsWith("CredentialProvider", StringComparison.OrdinalIgnoreCase) == true).ToArray();
178
+ return processes;
179
+ }
180
+
163
181
  internal static string? GetPathToPackagesDirectory(ProjectBuildFile projectBuildFile, string dependencyName, string dependencyVersion, string packagesConfigPath)
164
182
  {
165
183
  // the packages directory can be found from the hint path of the matching dependency, e.g., when given "Newtonsoft.Json", "7.0.1", and a project like this:
@@ -228,6 +228,7 @@ internal static class SdkPackageUpdater
228
228
 
229
229
  // see https://learn.microsoft.com/nuget/consume-packages/install-use-packages-dotnet-cli
230
230
  var (exitCode, stdout, stderr) = await ProcessEx.RunAsync("dotnet", $"add {projectPath} package {dependencyName} --version {newDependencyVersion}", workingDirectory: Path.GetDirectoryName(projectPath));
231
+ MSBuildHelper.ThrowOnUnauthenticatedFeed(stdout);
231
232
  if (exitCode != 0)
232
233
  {
233
234
  logger.Log($" Transitive dependency [{dependencyName}/{newDependencyVersion}] was not added.\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}");
@@ -594,7 +595,7 @@ internal static class SdkPackageUpdater
594
595
  string packageName)
595
596
  => buildFile.PackageItemNodes.Where(e =>
596
597
  string.Equals(
597
- e.GetAttributeOrSubElementValue("Include", StringComparison.OrdinalIgnoreCase) ?? e.GetAttributeOrSubElementValue("Update", StringComparison.OrdinalIgnoreCase),
598
+ (e.GetAttributeOrSubElementValue("Include", StringComparison.OrdinalIgnoreCase) ?? e.GetAttributeOrSubElementValue("Update", StringComparison.OrdinalIgnoreCase))?.Trim(),
598
599
  packageName,
599
600
  StringComparison.OrdinalIgnoreCase) &&
600
601
  (e.GetAttributeOrSubElementValue("Version", StringComparison.OrdinalIgnoreCase) ?? e.GetAttributeOrSubElementValue("VersionOverride", StringComparison.OrdinalIgnoreCase)) is not null);
@@ -0,0 +1,5 @@
1
+ namespace NuGetUpdater.Core.Updater;
2
+
3
+ public record UpdateOperationResult : NativeResult
4
+ {
5
+ }