dependabot-nuget 0.265.0 → 0.267.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+ }