dependabot-nuget 0.266.0 → 0.267.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4669e522f1ce196d5657096bdf738e6ab353c5a697ea1b0acb1f0c87f9815a44
4
- data.tar.gz: 33657445578fcd52b0370490b3c1f4b2fe165ad9f00e22d831d6a216b89458f5
3
+ metadata.gz: 19fd9d2635d3d1f5737870ebe8bcd155fb5e90557c7eec2b6832233bb0573b29
4
+ data.tar.gz: d1bd4ff99ae3fd8319a796cf04ba3a859c084cd465013cd5a1c8b106969a8183
5
5
  SHA512:
6
- metadata.gz: 0ca40a2a42e55b376edc0694253f81a54eb30d75748c3a5572e3a754b64aab96fe86b34f753d0f4566bdf66816da3318aed0eba31fc195581cb2072f89c0d8cb
7
- data.tar.gz: 115640e60d1b2a59edb5b6956c9e8d3c35105dafa6ce00d60aa5de52561da96670606af7862d1a24ab1f24bc4fd91651b4856eeb4b7fc545ca230cc72a529493
6
+ metadata.gz: 4b1c5c0ed9ca379e3c209c21dbc047e7364a9c3f7eda956ebd11a6f0f44b5a3a3e5a7fe9493ccafe82ac98bad132ab2323a56817c2f234c135814282cd229c30
7
+ data.tar.gz: c0f297752cb28d6c32df1261ab10199a5f39e17af23f3c3e46ed76aafd59785e79cd43c81e1a164b8fe92b9f7763f50e3d2c4a8d34c9bb59a4976d0e6eb44be4
@@ -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 isUpdateNecessary = IsUpdateNecessary(dependencyInfo, projectsWithDependency);
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
- updatedDependencies = updatedVersion is not null
103
- ? await FindUpdatedDependenciesAsync(
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(), ".dependabot", "packages");
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 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
  }
@@ -5,4 +5,5 @@ public enum ErrorType
5
5
  // TODO: add `Unknown` option to track all other failure types
6
6
  None,
7
7
  AuthenticationFailure,
8
+ MissingFile,
8
9
  }
@@ -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
+ }
@@ -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
  }
@@ -146,6 +146,7 @@ internal static class PackagesConfigUpdater
146
146
  }
147
147
 
148
148
  MSBuildHelper.ThrowOnUnauthenticatedFeed(fullOutput);
149
+ MSBuildHelper.ThrowOnMissingFile(fullOutput);
149
150
  throw new Exception(fullOutput);
150
151
  }
151
152
  }
@@ -73,6 +73,14 @@ public class UpdaterWorker
73
73
  ErrorDetails = "(" + string.Join("|", NuGetContext.GetPackageSourceUrls(workspacePath)) + ")",
74
74
  };
75
75
  }
76
+ catch (MissingFileException ex)
77
+ {
78
+ result = new()
79
+ {
80
+ ErrorType = ErrorType.MissingFile,
81
+ ErrorDetails = ex.FilePath,
82
+ };
83
+ }
76
84
 
77
85
  _processedProjectPaths.Clear();
78
86
  if (resultOutputPath is { })
@@ -620,6 +620,16 @@ internal static partial class MSBuildHelper
620
620
  }
621
621
  }
622
622
 
623
+ internal static void ThrowOnMissingFile(string output)
624
+ {
625
+ var missingFilePattern = new Regex(@"The imported project \""(?<FilePath>.*)\"" was not found");
626
+ var match = missingFilePattern.Match(output);
627
+ if (match.Success)
628
+ {
629
+ throw new MissingFileException(match.Groups["FilePath"].Value);
630
+ }
631
+ }
632
+
623
633
  internal static bool TryGetGlobalJsonPath(string repoRootPath, string workspacePath, [NotNullWhen(returnValue: true)] out string? globalJsonPath)
624
634
  {
625
635
  globalJsonPath = PathHelper.GetFileInDirectoryOrParent(workspacePath, repoRootPath, "global.json", caseSensitive: false);
@@ -390,6 +390,8 @@ public partial class AnalyzeWorkerTests : AnalyzeWorkerTestBase
390
390
  </m:properties>
391
391
  </entry>
392
392
  """));
393
+ case "/api/v2/package/Some.Package/1.0.0":
394
+ return (200, MockNuGetPackage.CreateSimplePackage("Some.Package", "1.0.0", "net8.0").GetZipStream().ReadAllBytes());
393
395
  case "/api/v2/package/Some.Package/1.2.3":
394
396
  return (200, MockNuGetPackage.CreateSimplePackage("Some.Package", "1.2.3", "net8.0").GetZipStream().ReadAllBytes());
395
397
  case "/api/v2/FindPackagesById()?id='Microsoft.WindowsDesktop.App.Ref'&semVerLevel=2.0.0":
@@ -32,6 +32,7 @@ public class RequirementTests
32
32
  [InlineData("2.0", "~> 1.0", false)]
33
33
  [InlineData("1", "~> 1", true)]
34
34
  [InlineData("2", "~> 1", false)]
35
+ [InlineData("5.3.8", "< 6, > 5.2.4", true)]
35
36
  public void IsSatisfiedBy(string versionString, string requirementString, bool expected)
36
37
  {
37
38
  var version = NuGetVersion.Parse(versionString);
@@ -1,3 +1,4 @@
1
+ using System.Collections.Immutable;
1
2
  using System.IO.Compression;
2
3
  using System.Security.Cryptography;
3
4
  using System.Text;
@@ -95,7 +96,7 @@ namespace NuGetUpdater.Core.Test
95
96
  /// Creates a mock NuGet package with a single assembly in the appropriate `lib/` directory. The assembly will
96
97
  /// contain the appropriate `AssemblyVersion` attribute and nothing else.
97
98
  /// </summary>
98
- public static MockNuGetPackage CreatePackageWithAssembly(string id, string version, string targetFramework, string assemblyVersion, (string? TargetFramework, (string Id, string Version)[] Packages)[]? dependencyGroups = null)
99
+ public static MockNuGetPackage CreatePackageWithAssembly(string id, string version, string targetFramework, string assemblyVersion, ImmutableArray<byte>? assemblyPublicKey = null, (string? TargetFramework, (string Id, string Version)[] Packages)[]? dependencyGroups = null)
99
100
  {
100
101
  return new(
101
102
  id,
@@ -104,7 +105,7 @@ namespace NuGetUpdater.Core.Test
104
105
  DependencyGroups: dependencyGroups,
105
106
  Files:
106
107
  [
107
- ($"lib/{targetFramework}/{id}.dll", CreateAssembly(id, assemblyVersion))
108
+ ($"lib/{targetFramework}/{id}.dll", CreateAssembly(id, assemblyVersion, assemblyPublicKey))
108
109
  ]
109
110
  );
110
111
  }
@@ -130,19 +131,14 @@ namespace NuGetUpdater.Core.Test
130
131
  );
131
132
  }
132
133
 
133
- public static MockNuGetPackage CreateDotNetToolPackage(string id, string version, string targetFramework)
134
+ public static MockNuGetPackage CreateDotNetToolPackage(string id, string version, string targetFramework, XElement[]? additionalMetadata = null)
134
135
  {
136
+ var packageMetadata = new XElement("packageTypes", new XElement("packageType", new XAttribute("name", "DotnetTool")));
137
+ var allMetadata = new[] { packageMetadata }.Concat(additionalMetadata ?? []).ToArray();
135
138
  return new(
136
139
  id,
137
140
  version,
138
- AdditionalMetadata:
139
- [
140
- new XElement("packageTypes",
141
- new XElement("packageType",
142
- new XAttribute("name", "DotnetTool")
143
- )
144
- )
145
- ],
141
+ AdditionalMetadata: allMetadata,
146
142
  Files:
147
143
  [
148
144
  ($"tools/{targetFramework}/any/DotnetToolSettings.xml", Encoding.UTF8.GetBytes($"""
@@ -157,8 +153,10 @@ namespace NuGetUpdater.Core.Test
157
153
  );
158
154
  }
159
155
 
160
- public static MockNuGetPackage CreateMSBuildSdkPackage(string id, string version, string? sdkPropsContent = null, string? sdkTargetsContent = null)
156
+ public static MockNuGetPackage CreateMSBuildSdkPackage(string id, string version, string? sdkPropsContent = null, string? sdkTargetsContent = null, XElement[]? additionalMetadata = null)
161
157
  {
158
+ var packageMetadata = new XElement("packageTypes", new XElement("packageType", new XAttribute("name", "MSBuildSdk")));
159
+ var allMetadata = new[] { packageMetadata }.Concat(additionalMetadata ?? []).ToArray();
162
160
  sdkPropsContent ??= """
163
161
  <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
164
162
  </Project>
@@ -170,14 +168,7 @@ namespace NuGetUpdater.Core.Test
170
168
  return new(
171
169
  id,
172
170
  version,
173
- AdditionalMetadata:
174
- [
175
- new XElement("packageTypes",
176
- new XElement("packageType",
177
- new XAttribute("name", "MSBuildSdk")
178
- )
179
- )
180
- ],
171
+ AdditionalMetadata: additionalMetadata,
181
172
  Files:
182
173
  [
183
174
  ("Sdk/Sdk.props", Encoding.UTF8.GetBytes(sdkPropsContent)),
@@ -271,9 +262,13 @@ namespace NuGetUpdater.Core.Test
271
262
  return _stream;
272
263
  }
273
264
 
274
- private static byte[] CreateAssembly(string assemblyName, string assemblyVersion)
265
+ private static byte[] CreateAssembly(string assemblyName, string assemblyVersion, ImmutableArray<byte>? assemblyPublicKey = null)
275
266
  {
276
267
  CSharpCompilationOptions compilationOptions = new(OutputKind.DynamicallyLinkedLibrary);
268
+ if (assemblyPublicKey is not null)
269
+ {
270
+ compilationOptions = compilationOptions.WithCryptoPublicKey(assemblyPublicKey.Value);
271
+ }
277
272
  CSharpCompilation compilation = CSharpCompilation.Create(assemblyName, options: compilationOptions)
278
273
  .AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location))
279
274
  .AddSyntaxTrees(CSharpSyntaxTree.ParseText($"[assembly: System.Reflection.AssemblyVersionAttribute(\"{assemblyVersion}\")]"));
@@ -1,3 +1,6 @@
1
+ using System.Collections.Immutable;
2
+ using System.Text.Json;
3
+
1
4
  using NuGetUpdater.Core.Updater;
2
5
 
3
6
  using Xunit;
@@ -1013,6 +1016,307 @@ public partial class UpdateWorkerTests
1013
1016
  );
1014
1017
  }
1015
1018
 
1019
+ [Fact]
1020
+ public async Task UpdateBindingRedirect_DuplicateRedirectsForTheSameAssemblyAreRemoved()
1021
+ {
1022
+ await TestUpdateForProject("Some.Package", "7.0.1", "13.0.1",
1023
+ packages:
1024
+ [
1025
+ MockNuGetPackage.CreatePackageWithAssembly("Some.Package", "7.0.1", "net45", "7.0.0.0"),
1026
+ MockNuGetPackage.CreatePackageWithAssembly("Some.Package", "13.0.1", "net45", "13.0.0.0"),
1027
+ ],
1028
+ projectContents: """
1029
+ <Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
1030
+ <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
1031
+ <PropertyGroup>
1032
+ <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
1033
+ </PropertyGroup>
1034
+ <ItemGroup>
1035
+ <None Include="packages.config" />
1036
+ </ItemGroup>
1037
+ <ItemGroup>
1038
+ <None Include="app.config" />
1039
+ </ItemGroup>
1040
+ <ItemGroup>
1041
+ <Reference Include="Some.Package, Version=7.0.0.0, Culture=neutral, PublicKeyToken=null">
1042
+ <HintPath>packages\Some.Package.7.0.1\lib\net45\Some.Package.dll</HintPath>
1043
+ <Private>True</Private>
1044
+ </Reference>
1045
+ </ItemGroup>
1046
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
1047
+ </Project>
1048
+ """,
1049
+ packagesConfigContents: """
1050
+ <packages>
1051
+ <package id="Some.Package" version="7.0.1" targetFramework="net45" />
1052
+ </packages>
1053
+ """,
1054
+ additionalFiles:
1055
+ [
1056
+ ("app.config", """
1057
+ <configuration>
1058
+ <runtime>
1059
+ <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
1060
+ <dependentAssembly>
1061
+ <assemblyIdentity name="Some.Package" publicKeyToken="null" culture="neutral" />
1062
+ <bindingRedirect oldVersion="0.0.0.0-7.0.0.0" newVersion="7.0.0.0" />
1063
+ </dependentAssembly>
1064
+ <dependentAssembly>
1065
+ <assemblyIdentity name="Some.Package" publicKeyToken="null" culture="neutral" />
1066
+ <bindingRedirect oldVersion="0.0.0.0-7.0.0.0" newVersion="7.0.0.0" />
1067
+ </dependentAssembly>
1068
+ </assemblyBinding>
1069
+ </runtime>
1070
+ </configuration>
1071
+ """)
1072
+ ],
1073
+ expectedProjectContents: """
1074
+ <Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
1075
+ <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
1076
+ <PropertyGroup>
1077
+ <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
1078
+ </PropertyGroup>
1079
+ <ItemGroup>
1080
+ <None Include="packages.config" />
1081
+ </ItemGroup>
1082
+ <ItemGroup>
1083
+ <None Include="app.config" />
1084
+ </ItemGroup>
1085
+ <ItemGroup>
1086
+ <Reference Include="Some.Package, Version=13.0.0.0, Culture=neutral, PublicKeyToken=null">
1087
+ <HintPath>packages\Some.Package.13.0.1\lib\net45\Some.Package.dll</HintPath>
1088
+ <Private>True</Private>
1089
+ </Reference>
1090
+ </ItemGroup>
1091
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
1092
+ </Project>
1093
+ """,
1094
+ expectedPackagesConfigContents: """
1095
+ <?xml version="1.0" encoding="utf-8"?>
1096
+ <packages>
1097
+ <package id="Some.Package" version="13.0.1" targetFramework="net45" />
1098
+ </packages>
1099
+ """,
1100
+ additionalFilesExpected:
1101
+ [
1102
+ ("app.config", """
1103
+ <configuration>
1104
+ <runtime>
1105
+ <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
1106
+ <dependentAssembly>
1107
+ <assemblyIdentity name="Some.Package" publicKeyToken="null" culture="neutral" />
1108
+ <bindingRedirect oldVersion="0.0.0.0-13.0.0.0" newVersion="13.0.0.0" />
1109
+ </dependentAssembly>
1110
+ </assemblyBinding>
1111
+ </runtime>
1112
+ </configuration>
1113
+ """)
1114
+ ]
1115
+ );
1116
+ }
1117
+
1118
+ [Fact]
1119
+ public async Task UpdateBindingRedirect_ExistingRedirectForAssemblyPublicKeyTokenDiffersByCase()
1120
+ {
1121
+ // Generated using "sn -k keypair.snk && sn -p keypair.snk public.snk" then converting public.snk to base64
1122
+ // https://learn.microsoft.com/en-us/dotnet/standard/assembly/create-public-private-key-pair
1123
+ var assemblyStrongNamePublicKey = Convert.FromBase64String(
1124
+ "ACQAAASAAACUAAAABgIAAAAkAABSU0ExAAQAAAEAAQAJJW4hmKpxa9pU0JPDvJ9KqjvfQuMUovGtFjkZ9b0i1KQ/7kqEOjW3Va0eGpU7Kz0qHp14iYQ3SsMzBZU3mZ2Ezeqg+dCVuDk7o2lp++4m1FstHsebtXBetyOzWkneo+3iKSzOQ7bOXj2s5M9umqRPk+yj0ZBILf+HvfAd07iIuQ=="
1125
+ ).ToImmutableArray();
1126
+
1127
+ await TestUpdateForProject("Some.Package", "7.0.1", "13.0.1",
1128
+ packages:
1129
+ [
1130
+ MockNuGetPackage.CreatePackageWithAssembly("Some.Package", "7.0.1", "net45", "7.0.0.0", assemblyPublicKey: assemblyStrongNamePublicKey),
1131
+ MockNuGetPackage.CreatePackageWithAssembly("Some.Package", "13.0.1", "net45", "13.0.0.0", assemblyPublicKey: assemblyStrongNamePublicKey),
1132
+ ],
1133
+ projectContents: """
1134
+ <Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
1135
+ <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
1136
+ <PropertyGroup>
1137
+ <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
1138
+ </PropertyGroup>
1139
+ <ItemGroup>
1140
+ <None Include="packages.config" />
1141
+ </ItemGroup>
1142
+ <ItemGroup>
1143
+ <None Include="app.config" />
1144
+ </ItemGroup>
1145
+ <ItemGroup>
1146
+ <Reference Include="Some.Package, Version=7.0.0.0, Culture=neutral, PublicKeyToken=13523fc3be375af1">
1147
+ <HintPath>packages\Some.Package.7.0.1\lib\net45\Some.Package.dll</HintPath>
1148
+ <Private>True</Private>
1149
+ </Reference>
1150
+ </ItemGroup>
1151
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
1152
+ </Project>
1153
+ """,
1154
+ packagesConfigContents: """
1155
+ <packages>
1156
+ <package id="Some.Package" version="7.0.1" targetFramework="net45" />
1157
+ </packages>
1158
+ """,
1159
+ additionalFiles:
1160
+ [
1161
+ ("app.config", """
1162
+ <configuration>
1163
+ <runtime>
1164
+ <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
1165
+ <dependentAssembly>
1166
+ <assemblyIdentity name="Some.Package" publicKeyToken="13523FC3BE375AF1" culture="neutral" />
1167
+ <bindingRedirect oldVersion="0.0.0.0-7.0.0.0" newVersion="7.0.0.0" />
1168
+ </dependentAssembly>
1169
+ </assemblyBinding>
1170
+ </runtime>
1171
+ </configuration>
1172
+ """)
1173
+ ],
1174
+ expectedProjectContents: """
1175
+ <Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
1176
+ <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
1177
+ <PropertyGroup>
1178
+ <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
1179
+ </PropertyGroup>
1180
+ <ItemGroup>
1181
+ <None Include="packages.config" />
1182
+ </ItemGroup>
1183
+ <ItemGroup>
1184
+ <None Include="app.config" />
1185
+ </ItemGroup>
1186
+ <ItemGroup>
1187
+ <Reference Include="Some.Package, Version=13.0.0.0, Culture=neutral, PublicKeyToken=13523fc3be375af1">
1188
+ <HintPath>packages\Some.Package.13.0.1\lib\net45\Some.Package.dll</HintPath>
1189
+ <Private>True</Private>
1190
+ </Reference>
1191
+ </ItemGroup>
1192
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
1193
+ </Project>
1194
+ """,
1195
+ expectedPackagesConfigContents: """
1196
+ <?xml version="1.0" encoding="utf-8"?>
1197
+ <packages>
1198
+ <package id="Some.Package" version="13.0.1" targetFramework="net45" />
1199
+ </packages>
1200
+ """,
1201
+ additionalFilesExpected:
1202
+ [
1203
+ ("app.config", """
1204
+ <configuration>
1205
+ <runtime>
1206
+ <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
1207
+ <dependentAssembly>
1208
+ <assemblyIdentity name="Some.Package" publicKeyToken="13523FC3BE375AF1" culture="neutral" />
1209
+ <bindingRedirect oldVersion="0.0.0.0-13.0.0.0" newVersion="13.0.0.0" />
1210
+ </dependentAssembly>
1211
+ </assemblyBinding>
1212
+ </runtime>
1213
+ </configuration>
1214
+ """)
1215
+ ]
1216
+ );
1217
+ }
1218
+
1219
+ [Fact]
1220
+ public async Task UpdateBindingRedirect_ConfigXmlDeclarationNodeIsPreserved()
1221
+ {
1222
+ await TestUpdateForProject("Some.Package", "7.0.1", "13.0.1",
1223
+ packages:
1224
+ [
1225
+ MockNuGetPackage.CreatePackageWithAssembly("Some.Package", "7.0.1", "net45", "7.0.0.0"),
1226
+ MockNuGetPackage.CreatePackageWithAssembly("Some.Package", "13.0.1", "net45", "13.0.0.0"),
1227
+ ],
1228
+ projectContents: """
1229
+ <Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
1230
+ <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
1231
+ <PropertyGroup>
1232
+ <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
1233
+ </PropertyGroup>
1234
+ <ItemGroup>
1235
+ <None Include="packages.config" />
1236
+ </ItemGroup>
1237
+ <ItemGroup>
1238
+ <None Include="app.config" />
1239
+ </ItemGroup>
1240
+ <ItemGroup>
1241
+ <Reference Include="Some.Package, Version=7.0.0.0, Culture=neutral, PublicKeyToken=null">
1242
+ <HintPath>packages\Some.Package.7.0.1\lib\net45\Some.Package.dll</HintPath>
1243
+ <Private>True</Private>
1244
+ </Reference>
1245
+ </ItemGroup>
1246
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
1247
+ </Project>
1248
+ """,
1249
+ packagesConfigContents: """
1250
+ <packages>
1251
+ <package id="Some.Package" version="7.0.1" targetFramework="net45" />
1252
+ </packages>
1253
+ """,
1254
+ additionalFiles:
1255
+ [
1256
+ ("app.config", """
1257
+ <?xml version="1.0" encoding="utf-8"?>
1258
+ <configuration>
1259
+ <runtime>
1260
+ <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
1261
+ <dependentAssembly>
1262
+ <assemblyIdentity name="Some.Package" publicKeyToken="null" culture="neutral" />
1263
+ <bindingRedirect oldVersion="0.0.0.0-7.0.0.0" newVersion="7.0.0.0" />
1264
+ </dependentAssembly>
1265
+ <dependentAssembly>
1266
+ <assemblyIdentity name="Some.Package" publicKeyToken="null" culture="neutral" />
1267
+ <bindingRedirect oldVersion="0.0.0.0-7.0.0.0" newVersion="7.0.0.0" />
1268
+ </dependentAssembly>
1269
+ </assemblyBinding>
1270
+ </runtime>
1271
+ </configuration>
1272
+ """)
1273
+ ],
1274
+ expectedProjectContents: """
1275
+ <Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
1276
+ <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
1277
+ <PropertyGroup>
1278
+ <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
1279
+ </PropertyGroup>
1280
+ <ItemGroup>
1281
+ <None Include="packages.config" />
1282
+ </ItemGroup>
1283
+ <ItemGroup>
1284
+ <None Include="app.config" />
1285
+ </ItemGroup>
1286
+ <ItemGroup>
1287
+ <Reference Include="Some.Package, Version=13.0.0.0, Culture=neutral, PublicKeyToken=null">
1288
+ <HintPath>packages\Some.Package.13.0.1\lib\net45\Some.Package.dll</HintPath>
1289
+ <Private>True</Private>
1290
+ </Reference>
1291
+ </ItemGroup>
1292
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
1293
+ </Project>
1294
+ """,
1295
+ expectedPackagesConfigContents: """
1296
+ <?xml version="1.0" encoding="utf-8"?>
1297
+ <packages>
1298
+ <package id="Some.Package" version="13.0.1" targetFramework="net45" />
1299
+ </packages>
1300
+ """,
1301
+ additionalFilesExpected:
1302
+ [
1303
+ ("app.config", """
1304
+ <?xml version="1.0" encoding="utf-8"?>
1305
+ <configuration>
1306
+ <runtime>
1307
+ <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
1308
+ <dependentAssembly>
1309
+ <assemblyIdentity name="Some.Package" publicKeyToken="null" culture="neutral" />
1310
+ <bindingRedirect oldVersion="0.0.0.0-13.0.0.0" newVersion="13.0.0.0" />
1311
+ </dependentAssembly>
1312
+ </assemblyBinding>
1313
+ </runtime>
1314
+ </configuration>
1315
+ """)
1316
+ ]
1317
+ );
1318
+ }
1319
+
1016
1320
  [Fact]
1017
1321
  public async Task PackagesConfigUpdateCanHappenEvenWithMismatchedVersionNumbers()
1018
1322
  {
@@ -1422,6 +1726,62 @@ public partial class UpdateWorkerTests
1422
1726
  """);
1423
1727
  }
1424
1728
 
1729
+ [Fact]
1730
+ public async Task MissingTargetsAreReported()
1731
+ {
1732
+ using var temporaryDirectory = await TemporaryDirectory.CreateWithContentsAsync(
1733
+ [
1734
+ ("project.csproj", """
1735
+ <Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
1736
+ <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
1737
+ <Import Project="this.file.does.not.exist.targets" />
1738
+ <PropertyGroup>
1739
+ <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
1740
+ </PropertyGroup>
1741
+ <ItemGroup>
1742
+ <None Include="packages.config" />
1743
+ </ItemGroup>
1744
+ <ItemGroup>
1745
+ <Reference Include="Some.Package, Version=1.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed">
1746
+ <HintPath>packages\Some.Package.1.0.0\lib\net45\Some.Package.dll</HintPath>
1747
+ <Private>True</Private>
1748
+ </Reference>
1749
+ </ItemGroup>
1750
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
1751
+ </Project>
1752
+ """),
1753
+ ("packages.config", """
1754
+ <packages>
1755
+ <package id="Some.Package" version="1.0.0" targetFramework="net45" />
1756
+ </packages>
1757
+ """),
1758
+ ("NuGet.Config", """
1759
+ <configuration>
1760
+ <packageSources>
1761
+ <clear />
1762
+ <add key="private_feed" value="packages" />
1763
+ </packageSources>
1764
+ </configuration>
1765
+ """)
1766
+ ]
1767
+ );
1768
+ MockNuGetPackage[] packages =
1769
+ [
1770
+ MockNuGetPackage.CreateSimplePackage("Some.Package", "1.0.0", "net45"),
1771
+ MockNuGetPackage.CreateSimplePackage("Some.Package", "1.1.0", "net45"),
1772
+ ];
1773
+ await MockNuGetPackagesInDirectory(packages, Path.Combine(temporaryDirectory.DirectoryPath, "packages"));
1774
+ var resultOutputPath = Path.Combine(temporaryDirectory.DirectoryPath, "result.json");
1775
+
1776
+ var worker = new UpdaterWorker(new Logger(verbose: true));
1777
+ await worker.RunAsync(temporaryDirectory.DirectoryPath, "project.csproj", "Some.Package", "1.0.0", "1.1.0", isTransitive: false, resultOutputPath: resultOutputPath);
1778
+
1779
+ var resultContents = await File.ReadAllTextAsync(resultOutputPath);
1780
+ var result = JsonSerializer.Deserialize<UpdateOperationResult>(resultContents, UpdaterWorker.SerializerOptions)!;
1781
+ Assert.Equal(ErrorType.MissingFile, result.ErrorType);
1782
+ Assert.Equal(Path.Combine(temporaryDirectory.DirectoryPath, "this.file.does.not.exist.targets"), result.ErrorDetails);
1783
+ }
1784
+
1425
1785
  [Fact]
1426
1786
  public async Task ReportsPrivateSourceAuthenticationFailure()
1427
1787
  {
@@ -51,20 +51,22 @@ module Dependabot
51
51
  @fetched_files = T.let({}, T::Hash[String, T::Array[Dependabot::DependencyFile]])
52
52
  @nuget_config_files = T.let(nil, T.nilable(T::Array[Dependabot::DependencyFile]))
53
53
  @packages_config_files = T.let(nil, T.nilable(T::Array[Dependabot::DependencyFile]))
54
+ @assembly_binding_redirect_config_files = T.let(nil, T.nilable(T::Array[Dependabot::DependencyFile]))
54
55
  end
55
56
 
56
57
  sig { override.returns(T::Array[DependencyFile]) }
57
58
  def fetch_files
58
- fetched_files = []
59
- fetched_files += project_files
60
- fetched_files += directory_build_files
61
- fetched_files += imported_property_files
62
-
63
- fetched_files += packages_config_files
64
- fetched_files += nuget_config_files
65
- fetched_files << global_json if global_json
66
- fetched_files << dotnet_tools_json if dotnet_tools_json
67
- fetched_files << packages_props if packages_props
59
+ fetched_files = [
60
+ *project_files,
61
+ *directory_build_files,
62
+ *imported_property_files,
63
+ *packages_config_files,
64
+ *assembly_binding_redirect_config_files,
65
+ *nuget_config_files,
66
+ global_json,
67
+ dotnet_tools_json,
68
+ packages_props
69
+ ].compact
68
70
 
69
71
  # dedup files based on their absolute path
70
72
  fetched_files = fetched_files.uniq do |fetched_file|
@@ -128,6 +130,23 @@ module Dependabot
128
130
  end
129
131
  end
130
132
 
133
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
134
+ def assembly_binding_redirect_config_files
135
+ return @assembly_binding_redirect_config_files if @assembly_binding_redirect_config_files
136
+
137
+ candidate_paths =
138
+ [*project_files.map { |f| File.dirname(f.name) }, "."].uniq
139
+
140
+ # Assembly binding redirects can appear in any app/web.config file for a .NET Framework project
141
+ # https://learn.microsoft.com/en-us/dotnet/framework/configure-apps/redirect-assembly-versions#specify-assembly-binding-in-configuration-files
142
+ @assembly_binding_redirect_config_files =
143
+ candidate_paths.filter_map do |dir|
144
+ file = repo_contents(dir: dir)
145
+ .find { |f| f.name.match?(/^(app|web)\.config$/i) }
146
+ fetch_file_from_host(File.join(dir, file.name)) if file
147
+ end
148
+ end
149
+
131
150
  # rubocop:disable Metrics/PerceivedComplexity
132
151
  sig { returns(T.nilable(T::Array[T.untyped])) }
133
152
  def sln_file_names
@@ -21,6 +21,8 @@ module Dependabot
21
21
  [
22
22
  %r{^[^/]*\.([a-z]{2})?proj$},
23
23
  /^packages\.config$/i,
24
+ /^app\.config$/i,
25
+ /^web\.config$/i,
24
26
  /^global\.json$/i,
25
27
  /^dotnet-tools\.json$/i,
26
28
  /^Directory\.Build\.props$/i,
@@ -261,6 +261,8 @@ module Dependabot
261
261
  # no issue
262
262
  when "AuthenticationFailure"
263
263
  raise PrivateSourceAuthenticationFailure, error_details
264
+ when "MissingFile"
265
+ raise DependencyFileNotFound, error_details
264
266
  else
265
267
  raise "Unexpected error type from native tool: #{error_type}: #{error_details}"
266
268
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dependabot-nuget
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.266.0
4
+ version: 0.267.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dependabot
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-07-18 00:00:00.000000000 Z
11
+ date: 2024-07-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dependabot-common
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: 0.266.0
19
+ version: 0.267.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: 0.266.0
26
+ version: 0.267.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rubyzip
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -383,6 +383,7 @@ files:
383
383
  - helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/CompatabilityChecker.cs
384
384
  - helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/FrameworkCompatibilityService.cs
385
385
  - helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/SupportedFrameworks.cs
386
+ - helpers/lib/NuGetUpdater/NuGetUpdater.Core/MissingFileException.cs
386
387
  - helpers/lib/NuGetUpdater/NuGetUpdater.Core/NativeResult.cs
387
388
  - helpers/lib/NuGetUpdater/NuGetUpdater.Core/NuGetUpdater.Core.csproj
388
389
  - helpers/lib/NuGetUpdater/NuGetUpdater.Core/Property.cs
@@ -460,7 +461,7 @@ licenses:
460
461
  - MIT
461
462
  metadata:
462
463
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
463
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.266.0
464
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.267.0
464
465
  post_install_message:
465
466
  rdoc_options: []
466
467
  require_paths: