dependabot-nuget 0.252.0 → 0.253.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (23) hide show
  1. checksums.yaml +4 -4
  2. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Update.cs +27 -9
  3. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs +1 -1
  4. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/SdkProjectDiscovery.cs +66 -65
  5. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs +44 -6
  6. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdaterWorker.cs +1 -1
  7. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +181 -93
  8. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/PathHelper.cs +18 -13
  9. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs +24 -6
  10. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.PackagesConfig.cs +18 -10
  11. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Project.cs +25 -0
  12. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/TestBase.cs +10 -0
  13. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/PackagesConfigUpdaterTests.cs +1 -8
  14. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTestBase.cs +1 -1
  15. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.DirsProj.cs +0 -5
  16. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.DotNetTools.cs +0 -5
  17. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.GlobalJson.cs +0 -5
  18. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.Mixed.cs +0 -5
  19. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.PackagesConfig.cs +0 -5
  20. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.Sdk.cs +30 -23
  21. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs +44 -9
  22. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/SdkPackageUpdaterHelperTests.cs +1 -1
  23. metadata +6 -19
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6658fe5d81acc16548fe2d8fa0aa6ad631e0699588a4595e6b8de2f382a163d8
4
- data.tar.gz: 3ced8b77f453319a715b76ccff47787a8798cb92366fe6a86d6894b855a984bf
3
+ metadata.gz: 272069533f6bea5533fb423db28c745b469884c904e393d9708091398a93f9a3
4
+ data.tar.gz: dd0c3fad1ab56f2bc32e509f89552068cc59b02cd21b1878ef1edcca84af45ba
5
5
  SHA512:
6
- metadata.gz: 958fba1507fdc26ec2e0bc3978702b9103543558552c79188e7cca93b85248f46c1d485686da353076fbab234ea150cc1bb7e11231a1765e87071cf068b2e95c
7
- data.tar.gz: 5262c02eeff331fbf8c1399ec2d8fc49bd8942f83f2c1d67a4c9f48a3c7515b0f52710cb5e9f1409c23b025719cbb68d6c43ea2db44f15fe9aa353d80a5b1bd8
6
+ metadata.gz: a66d28929da003117a1a5ef125be1acd2dd91a522f715fa1c263610a63e333e5b60ec3343836a1d19b6516527595b88425aed9f7df7a5c3f254579b1a12ec77c
7
+ data.tar.gz: 4fa773a0ff8f2c5e423fd505d111eaf7f84439f99111b68a87e77700290ae0ae97d52318ec9ddabc4d5c80e953bdaef761da96730c82baa8f5c0e36034798e3b
@@ -293,22 +293,30 @@ public partial class EntryPointTests
293
293
  );
294
294
  }
295
295
 
296
- [Fact]
297
- public async Task UpdaterDoesNotUseRepoGlobalJsonForMSBuildTasks()
296
+ [Theory]
297
+ [InlineData(null)]
298
+ [InlineData("src")]
299
+ public async Task UpdaterDoesNotUseRepoGlobalJsonForMSBuildTasks(string? workingDirectoryPath)
298
300
  {
299
301
  // This is a _very_ specific scenario where the `NuGetUpdater.Cli` tool might pick up a `global.json` from
300
302
  // the root of the repo under test and use it's `sdk` property when trying to locate MSBuild. To properly
301
303
  // test this, it must be tested in a new process where MSBuild has not been loaded yet and the runner tool
302
304
  // must be started with its working directory at the test repo's root.
303
305
  using var tempDir = new TemporaryDirectory();
304
- await File.WriteAllTextAsync(Path.Join(tempDir.DirectoryPath, "global.json"), """
306
+ var globalJsonPath = Path.Join(tempDir.DirectoryPath, "global.json");
307
+ var srcGlobalJsonPath = Path.Join(tempDir.DirectoryPath, "src", "global.json");
308
+ string globalJsonContent = """
305
309
  {
306
310
  "sdk": {
307
311
  "version": "99.99.99"
308
312
  }
309
313
  }
310
- """);
311
- await File.WriteAllTextAsync(Path.Join(tempDir.DirectoryPath, "project.csproj"), """
314
+ """;
315
+ await File.WriteAllTextAsync(globalJsonPath, globalJsonContent);
316
+ Directory.CreateDirectory(Path.Join(tempDir.DirectoryPath, "src"));
317
+ await File.WriteAllTextAsync(srcGlobalJsonPath, globalJsonContent);
318
+ var projectPath = Path.Join(tempDir.DirectoryPath, "src", "project.csproj");
319
+ await File.WriteAllTextAsync(projectPath, """
312
320
  <Project Sdk="Microsoft.NET.Sdk">
313
321
  <PropertyGroup>
314
322
  <TargetFramework>net8.0</TargetFramework>
@@ -325,7 +333,7 @@ public partial class EntryPointTests
325
333
  "--repo-root",
326
334
  tempDir.DirectoryPath,
327
335
  "--solution-or-project",
328
- Path.Join(tempDir.DirectoryPath, "project.csproj"),
336
+ projectPath,
329
337
  "--dependency",
330
338
  "Newtonsoft.Json",
331
339
  "--new-version",
@@ -336,15 +344,25 @@ public partial class EntryPointTests
336
344
  ]);
337
345
 
338
346
  // verify base run
339
- var (exitCode, output, error) = await ProcessEx.RunAsync(executableName, executableArgs, workingDirectory: tempDir.DirectoryPath);
347
+ var workingDirectory = tempDir.DirectoryPath;
348
+ if (workingDirectoryPath is not null)
349
+ {
350
+ workingDirectory = Path.Join(workingDirectory, workingDirectoryPath);
351
+ }
352
+
353
+ var (exitCode, output, error) = await ProcessEx.RunAsync(executableName, executableArgs, workingDirectory: workingDirectory);
340
354
  Assert.True(exitCode == 0, $"Error running update on unsupported SDK.\nSTDOUT:\n{output}\nSTDERR:\n{error}");
341
355
 
342
356
  // verify project update
343
- var updatedProjectContents = await File.ReadAllTextAsync(Path.Join(tempDir.DirectoryPath, "project.csproj"));
357
+ var updatedProjectContents = await File.ReadAllTextAsync(projectPath);
344
358
  Assert.Contains("13.0.1", updatedProjectContents);
345
359
 
346
360
  // verify `global.json` untouched
347
- var updatedGlobalJsonContents = await File.ReadAllTextAsync(Path.Join(tempDir.DirectoryPath, "global.json"));
361
+ var updatedGlobalJsonContents = await File.ReadAllTextAsync(globalJsonPath);
362
+ Assert.Contains("99.99.99", updatedGlobalJsonContents);
363
+
364
+ // verify `src/global.json` untouched
365
+ var updatedSrcGlobalJsonContents = await File.ReadAllTextAsync(srcGlobalJsonPath);
348
366
  Assert.Contains("99.99.99", updatedGlobalJsonContents);
349
367
  }
350
368
 
@@ -26,7 +26,7 @@ public partial class DiscoveryWorker
26
26
 
27
27
  public async Task RunAsync(string repoRootPath, string workspacePath, string outputPath)
28
28
  {
29
- MSBuildHelper.RegisterMSBuild();
29
+ MSBuildHelper.RegisterMSBuild(Environment.CurrentDirectory, repoRootPath);
30
30
 
31
31
  // When running under unit tests, the workspace path may not be rooted.
32
32
  if (!Path.IsPathRooted(workspacePath) || !Directory.Exists(workspacePath))
@@ -9,86 +9,87 @@ internal static class SdkProjectDiscovery
9
9
  public static async Task<ImmutableArray<ProjectDiscoveryResult>> DiscoverAsync(string repoRootPath, string workspacePath, string projectPath, Logger logger)
10
10
  {
11
11
  // Determine which targets and props files contribute to the build.
12
- var buildFiles = await MSBuildHelper.LoadBuildFilesAsync(repoRootPath, projectPath, includeSdkPropsAndTargets: true);
12
+ var (buildFiles, projectTargetFrameworks) = await MSBuildHelper.LoadBuildFilesAndTargetFrameworksAsync(repoRootPath, projectPath);
13
+ var tfms = projectTargetFrameworks.Order().ToImmutableArray();
13
14
 
14
15
  // Get all the dependencies which are directly referenced from the project file or indirectly referenced from
15
16
  // targets and props files.
16
17
  var topLevelDependencies = MSBuildHelper.GetTopLevelPackageDependencyInfos(buildFiles);
17
18
 
18
19
  var results = ImmutableArray.CreateBuilder<ProjectDiscoveryResult>();
19
- foreach (var buildFile in buildFiles)
20
+ if (tfms.Length > 0)
20
21
  {
21
- // Only include build files that exist beneath the RepoRootPath.
22
- if (buildFile.IsOutsideBasePath)
22
+ foreach (var buildFile in buildFiles)
23
23
  {
24
- continue;
25
- }
26
-
27
- // The build file dependencies have the correct DependencyType and the TopLevelDependencies have the evaluated version.
28
- // Combine them to have the set of dependencies that are directly referenced from the build file.
29
- var fileDependencies = BuildFile.GetDependencies(buildFile)
30
- .ToDictionary(d => d.Name, StringComparer.OrdinalIgnoreCase);
31
- var sdkDependencies = fileDependencies.Values
32
- .Where(d => d.Type == DependencyType.MSBuildSdk)
33
- .ToImmutableArray();
34
- var indirectDependencies = topLevelDependencies
35
- .Where(d => !fileDependencies.ContainsKey(d.Name))
36
- .ToImmutableArray();
37
- var directDependencies = topLevelDependencies
38
- .Where(d => fileDependencies.ContainsKey(d.Name))
39
- .Select(d =>
24
+ // Only include build files that exist beneath the RepoRootPath.
25
+ if (buildFile.IsOutsideBasePath)
40
26
  {
41
- var dependency = fileDependencies[d.Name];
42
- return d with
43
- {
44
- Type = dependency.Type,
45
- IsDirect = true
46
- };
47
- }).ToImmutableArray();
48
-
49
- if (buildFile.GetFileType() == ProjectBuildFileType.Project)
50
- {
51
- // Collect information that is specific to the project file.
52
- var tfms = MSBuildHelper.GetTargetFrameworkMonikers(buildFiles)
53
- .OrderBy(tfm => tfm)
54
- .ToImmutableArray();
55
- var properties = MSBuildHelper.GetProperties(buildFiles).Values
56
- .Where(p => !p.SourceFilePath.StartsWith(".."))
57
- .OrderBy(p => p.Name)
58
- .ToImmutableArray();
59
- var referencedProjectPaths = MSBuildHelper.GetProjectPathsFromProject(projectPath)
60
- .Select(path => Path.GetRelativePath(workspacePath, path))
61
- .OrderBy(p => p)
62
- .ToImmutableArray();
27
+ continue;
28
+ }
63
29
 
64
- // Get the complete set of dependencies including transitive dependencies.
65
- var dependencies = indirectDependencies.Concat(directDependencies).ToImmutableArray();
66
- dependencies = dependencies
67
- .Select(d => d with { TargetFrameworks = tfms })
30
+ // The build file dependencies have the correct DependencyType and the TopLevelDependencies have the evaluated version.
31
+ // Combine them to have the set of dependencies that are directly referenced from the build file.
32
+ var fileDependencies = BuildFile.GetDependencies(buildFile)
33
+ .ToDictionary(d => d.Name, StringComparer.OrdinalIgnoreCase);
34
+ var sdkDependencies = fileDependencies.Values
35
+ .Where(d => d.Type == DependencyType.MSBuildSdk)
68
36
  .ToImmutableArray();
69
- var transitiveDependencies = await GetTransitiveDependencies(repoRootPath, projectPath, tfms, dependencies, logger);
70
- ImmutableArray<Dependency> allDependencies = dependencies.Concat(transitiveDependencies).Concat(sdkDependencies)
71
- .OrderBy(d => d.Name)
37
+ var indirectDependencies = topLevelDependencies
38
+ .Where(d => !fileDependencies.ContainsKey(d.Name))
72
39
  .ToImmutableArray();
40
+ var directDependencies = topLevelDependencies
41
+ .Where(d => fileDependencies.ContainsKey(d.Name))
42
+ .Select(d =>
43
+ {
44
+ var dependency = fileDependencies[d.Name];
45
+ return d with
46
+ {
47
+ Type = dependency.Type,
48
+ IsDirect = true
49
+ };
50
+ }).ToImmutableArray();
73
51
 
74
- results.Add(new()
52
+ if (buildFile.GetFileType() == ProjectBuildFileType.Project)
75
53
  {
76
- FilePath = Path.GetRelativePath(workspacePath, buildFile.Path),
77
- Properties = properties,
78
- TargetFrameworks = tfms,
79
- ReferencedProjectPaths = referencedProjectPaths,
80
- Dependencies = allDependencies,
81
- });
82
- }
83
- else
84
- {
85
- results.Add(new()
86
- {
87
- FilePath = Path.GetRelativePath(workspacePath, buildFile.Path),
88
- Dependencies = directDependencies.Concat(sdkDependencies)
54
+ // Collect information that is specific to the project file.
55
+ var properties = MSBuildHelper.GetProperties(buildFiles).Values
56
+ .Where(p => !p.SourceFilePath.StartsWith(".."))
57
+ .OrderBy(p => p.Name)
58
+ .ToImmutableArray();
59
+ var referencedProjectPaths = MSBuildHelper.GetProjectPathsFromProject(projectPath)
60
+ .Select(path => Path.GetRelativePath(workspacePath, path))
61
+ .OrderBy(p => p)
62
+ .ToImmutableArray();
63
+
64
+ // Get the complete set of dependencies including transitive dependencies.
65
+ var dependencies = indirectDependencies.Concat(directDependencies).ToImmutableArray();
66
+ dependencies = dependencies
67
+ .Select(d => d with { TargetFrameworks = tfms })
68
+ .ToImmutableArray();
69
+ var transitiveDependencies = await GetTransitiveDependencies(repoRootPath, projectPath, tfms, dependencies, logger);
70
+ ImmutableArray<Dependency> allDependencies = dependencies.Concat(transitiveDependencies).Concat(sdkDependencies)
89
71
  .OrderBy(d => d.Name)
90
- .ToImmutableArray(),
91
- });
72
+ .ToImmutableArray();
73
+
74
+ results.Add(new()
75
+ {
76
+ FilePath = Path.GetRelativePath(workspacePath, buildFile.Path),
77
+ Properties = properties,
78
+ TargetFrameworks = tfms,
79
+ ReferencedProjectPaths = referencedProjectPaths,
80
+ Dependencies = allDependencies,
81
+ });
82
+ }
83
+ else
84
+ {
85
+ results.Add(new()
86
+ {
87
+ FilePath = Path.GetRelativePath(workspacePath, buildFile.Path),
88
+ Dependencies = directDependencies.Concat(sdkDependencies)
89
+ .OrderBy(d => d.Name)
90
+ .ToImmutableArray(),
91
+ });
92
+ }
92
93
  }
93
94
  }
94
95
 
@@ -20,8 +20,7 @@ internal static class SdkPackageUpdater
20
20
  // SDK-style project, modify the XML directly
21
21
  logger.Log(" Running for SDK-style project");
22
22
 
23
- var buildFiles = await MSBuildHelper.LoadBuildFilesAsync(repoRootPath, projectPath);
24
- var tfms = MSBuildHelper.GetTargetFrameworkMonikers(buildFiles);
23
+ (ImmutableArray<ProjectBuildFile> buildFiles, string[] tfms) = await MSBuildHelper.LoadBuildFilesAndTargetFrameworksAsync(repoRootPath, projectPath);
25
24
 
26
25
  // Get the set of all top-level dependencies in the current project
27
26
  var topLevelDependencies = MSBuildHelper.GetTopLevelPackageDependencyInfos(buildFiles).ToArray();
@@ -42,7 +41,7 @@ internal static class SdkPackageUpdater
42
41
  return;
43
42
  }
44
43
 
45
- UpdateTopLevelDepdendency(buildFiles, dependencyName, previousDependencyVersion, newDependencyVersion, peerDependencies, logger);
44
+ await UpdateTopLevelDepdendency(repoRootPath, buildFiles, tfms, dependencyName, previousDependencyVersion, newDependencyVersion, peerDependencies, logger);
46
45
  }
47
46
 
48
47
  if (!await AreDependenciesCoherentAsync(repoRootPath, projectPath, dependencyName, logger, buildFiles, tfms))
@@ -226,10 +225,10 @@ internal static class SdkPackageUpdater
226
225
  logger.Log($" Adding [{dependencyName}/{newDependencyVersion}] as a top-level package reference.");
227
226
 
228
227
  // see https://learn.microsoft.com/nuget/consume-packages/install-use-packages-dotnet-cli
229
- var (exitCode, _, _) = await ProcessEx.RunAsync("dotnet", $"add {projectPath} package {dependencyName} --version {newDependencyVersion}", workingDirectory: Path.GetDirectoryName(projectPath));
228
+ var (exitCode, stdout, stderr) = await ProcessEx.RunAsync("dotnet", $"add {projectPath} package {dependencyName} --version {newDependencyVersion}", workingDirectory: Path.GetDirectoryName(projectPath));
230
229
  if (exitCode != 0)
231
230
  {
232
- logger.Log($" Transitive dependency [{dependencyName}/{newDependencyVersion}] was not added.");
231
+ logger.Log($" Transitive dependency [{dependencyName}/{newDependencyVersion}] was not added.\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}");
233
232
  }
234
233
  }
235
234
 
@@ -288,8 +287,10 @@ internal static class SdkPackageUpdater
288
287
  return packagesAndVersions;
289
288
  }
290
289
 
291
- private static void UpdateTopLevelDepdendency(
290
+ private static async Task UpdateTopLevelDepdendency(
291
+ string repoRootPath,
292
292
  ImmutableArray<ProjectBuildFile> buildFiles,
293
+ string[] targetFrameworks,
293
294
  string dependencyName,
294
295
  string previousDependencyVersion,
295
296
  string newDependencyVersion,
@@ -307,6 +308,43 @@ internal static class SdkPackageUpdater
307
308
  {
308
309
  TryUpdateDependencyVersion(buildFiles, packageName, previousDependencyVersion: null, newDependencyVersion: packageVersion, logger);
309
310
  }
311
+
312
+ // now make all dependency requirements coherent
313
+ Dependency[] updatedTopLevelDependencies = MSBuildHelper.GetTopLevelPackageDependencyInfos(buildFiles).ToArray();
314
+ foreach (ProjectBuildFile projectFile in buildFiles)
315
+ {
316
+ foreach (string tfm in targetFrameworks)
317
+ {
318
+ Dependency[]? resolvedDependencies = await MSBuildHelper.ResolveDependencyConflicts(repoRootPath, projectFile.Path, tfm, updatedTopLevelDependencies, logger);
319
+ if (resolvedDependencies is null)
320
+ {
321
+ logger.Log($" Unable to resolve dependency conflicts for {projectFile.Path}.");
322
+ continue;
323
+ }
324
+
325
+ // ensure the originally requested dependency was resolved to the correct version
326
+ var specificResolvedDependency = resolvedDependencies.Where(d => d.Name.Equals(dependencyName, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
327
+ if (specificResolvedDependency is null)
328
+ {
329
+ logger.Log($" Unable resolve requested dependency for {dependencyName} in {projectFile.Path}.");
330
+ continue;
331
+ }
332
+
333
+ if (!newDependencyVersion.Equals(specificResolvedDependency.Version, StringComparison.OrdinalIgnoreCase))
334
+ {
335
+ logger.Log($" Inconsistent resolution for {dependencyName}; attempted upgrade to {newDependencyVersion} but resolved {specificResolvedDependency.Version}.");
336
+ continue;
337
+ }
338
+
339
+ // update all other dependencies
340
+ foreach (Dependency resolvedDependency in resolvedDependencies
341
+ .Where(d => !d.Name.Equals(dependencyName, StringComparison.OrdinalIgnoreCase))
342
+ .Where(d => d.Version is not null))
343
+ {
344
+ TryUpdateDependencyVersion(buildFiles, resolvedDependency.Name, previousDependencyVersion: null, newDependencyVersion: resolvedDependency.Version!, logger);
345
+ }
346
+ }
347
+ }
310
348
  }
311
349
 
312
350
  private static UpdateResult TryUpdateDependencyVersion(
@@ -12,7 +12,7 @@ public class UpdaterWorker
12
12
 
13
13
  public async Task RunAsync(string repoRootPath, string workspacePath, string dependencyName, string previousDependencyVersion, string newDependencyVersion, bool isTransitive)
14
14
  {
15
- MSBuildHelper.RegisterMSBuild();
15
+ MSBuildHelper.RegisterMSBuild(Environment.CurrentDirectory, repoRootPath);
16
16
 
17
17
  if (!Path.IsPathRooted(workspacePath) || !File.Exists(workspacePath))
18
18
  {
@@ -2,6 +2,7 @@ using System.Collections.Immutable;
2
2
  using System.Diagnostics.CodeAnalysis;
3
3
  using System.Text;
4
4
  using System.Text.Json;
5
+ using System.Text.Json.Nodes;
5
6
  using System.Text.RegularExpressions;
6
7
  using System.Xml;
7
8
 
@@ -13,6 +14,7 @@ using Microsoft.Build.Locator;
13
14
  using Microsoft.Extensions.FileSystemGlobbing;
14
15
 
15
16
  using NuGet.Configuration;
17
+ using NuGet.Versioning;
16
18
 
17
19
  using NuGetUpdater.Core.Utilities;
18
20
 
@@ -24,106 +26,34 @@ internal static partial class MSBuildHelper
24
26
 
25
27
  public static bool IsMSBuildRegistered => MSBuildPath.Length > 0;
26
28
 
27
- static MSBuildHelper()
28
- {
29
- RegisterMSBuild();
30
- }
31
-
32
- public static void RegisterMSBuild()
29
+ public static void RegisterMSBuild(string currentDirectory, string rootDirectory)
33
30
  {
34
31
  // Ensure MSBuild types are registered before calling a method that loads the types
35
32
  if (!IsMSBuildRegistered)
36
33
  {
37
- var globalJsonPath = "global.json";
38
- var tempGlobalJsonPath = globalJsonPath + Guid.NewGuid().ToString();
39
- var globalJsonExists = File.Exists(globalJsonPath);
40
- try
34
+ var candidateDirectories = PathHelper.GetAllDirectoriesToRoot(currentDirectory, rootDirectory);
35
+ var globalJsonPaths = candidateDirectories.Select(d => Path.Combine(d, "global.json")).Where(File.Exists).Select(p => (p, p + Guid.NewGuid().ToString())).ToArray();
36
+ foreach (var (globalJsonPath, tempGlobalJsonPath) in globalJsonPaths)
41
37
  {
42
- if (globalJsonExists)
43
- {
44
- Console.WriteLine("Temporarily removing `global.json` for MSBuild detection.");
45
- File.Move(globalJsonPath, tempGlobalJsonPath);
46
- }
38
+ Console.WriteLine($"Temporarily removing `global.json` from `{Path.GetDirectoryName(globalJsonPath)}` for MSBuild detection.");
39
+ File.Move(globalJsonPath, tempGlobalJsonPath);
40
+ }
47
41
 
42
+ try
43
+ {
48
44
  var defaultInstance = MSBuildLocator.QueryVisualStudioInstances().First();
49
45
  MSBuildPath = defaultInstance.MSBuildPath;
50
46
  MSBuildLocator.RegisterInstance(defaultInstance);
51
47
  }
52
48
  finally
53
49
  {
54
- if (globalJsonExists)
55
- {
56
- Console.WriteLine("Restoring `global.json` after MSBuild detection.");
57
- File.Move(tempGlobalJsonPath, globalJsonPath);
58
- }
59
- }
60
- }
61
- }
62
-
63
- public static string[] GetTargetFrameworkMonikers(ImmutableArray<ProjectBuildFile> buildFiles)
64
- {
65
- HashSet<string> targetFrameworkValues = new(StringComparer.OrdinalIgnoreCase);
66
- Dictionary<string, Property> propertyInfo = new(StringComparer.OrdinalIgnoreCase);
67
-
68
- foreach (var buildFile in buildFiles)
69
- {
70
- var projectRoot = CreateProjectRootElement(buildFile);
71
-
72
- foreach (var property in projectRoot.Properties)
73
- {
74
- if (property.Name.Equals("TargetFramework", StringComparison.OrdinalIgnoreCase) ||
75
- property.Name.Equals("TargetFrameworks", StringComparison.OrdinalIgnoreCase))
76
- {
77
- if (buildFile.IsOutsideBasePath)
78
- {
79
- continue;
80
- }
81
-
82
- foreach (var tfm in property.Value.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
83
- {
84
- targetFrameworkValues.Add(tfm);
85
- }
86
- }
87
- else if (property.Name.Equals("TargetFrameworkVersion", StringComparison.OrdinalIgnoreCase))
50
+ foreach (var (globalJsonpath, tempGlobalJsonPath) in globalJsonPaths)
88
51
  {
89
- if (buildFile.IsOutsideBasePath)
90
- {
91
- continue;
92
- }
93
-
94
- // For packages.config projects that use TargetFrameworkVersion, we need to convert it to TargetFramework
95
- targetFrameworkValues.Add($"net{property.Value.TrimStart('v').Replace(".", "")}");
52
+ Console.WriteLine($"Restoring `global.json` to `{Path.GetDirectoryName(globalJsonpath)}` after MSBuild discovery.");
53
+ File.Move(tempGlobalJsonPath, globalJsonpath);
96
54
  }
97
- else
98
- {
99
- propertyInfo[property.Name] = new(property.Name, property.Value, buildFile.RelativePath);
100
- }
101
- }
102
- }
103
-
104
- HashSet<string> targetFrameworks = new(StringComparer.OrdinalIgnoreCase);
105
-
106
- foreach (var targetFrameworkValue in targetFrameworkValues)
107
- {
108
- var (resultType, _, tfms, _, errorMessage) =
109
- GetEvaluatedValue(targetFrameworkValue, propertyInfo, propertiesToIgnore: ["TargetFramework", "TargetFrameworks"]);
110
- if (resultType != EvaluationResultType.Success)
111
- {
112
- continue;
113
- }
114
-
115
- if (string.IsNullOrEmpty(tfms))
116
- {
117
- continue;
118
- }
119
-
120
- foreach (var tfm in tfms.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
121
- {
122
- targetFrameworks.Add(tfm);
123
55
  }
124
56
  }
125
-
126
- return targetFrameworks.ToArray();
127
57
  }
128
58
 
129
59
  public static IEnumerable<string> GetProjectPathsFromSolution(string solutionPath)
@@ -379,6 +309,124 @@ internal static partial class MSBuildHelper
379
309
  }
380
310
  }
381
311
 
312
+ internal static async Task<Dependency[]?> ResolveDependencyConflicts(string repoRoot, string projectPath, string targetFramework, Dependency[] packages, Logger logger)
313
+ {
314
+ var tempDirectory = Directory.CreateTempSubdirectory("package-dependency-coherence_");
315
+ try
316
+ {
317
+ var tempProjectPath = await CreateTempProjectAsync(tempDirectory, repoRoot, projectPath, targetFramework, packages);
318
+ var (exitCode, stdOut, stdErr) = await ProcessEx.RunAsync("dotnet", $"restore \"{tempProjectPath}\"", workingDirectory: tempDirectory.FullName);
319
+
320
+ // simple cases first
321
+ // if restore failed, nothing we can do
322
+ if (exitCode != 0)
323
+ {
324
+ return null;
325
+ }
326
+
327
+ // if no problems found, just return the current set
328
+ if (!stdOut.Contains("NU1608"))
329
+ {
330
+ return packages;
331
+ }
332
+
333
+ // now it gets complicated; look for the packages with issues
334
+ MatchCollection matches = PackageIncompatibilityWarningPattern().Matches(stdOut);
335
+ (string, NuGetVersion)[] badPackagesAndVersions = matches.Select(m => (m.Groups["PackageName"].Value, NuGetVersion.Parse(m.Groups["PackageVersion"].Value))).ToArray();
336
+ Dictionary<string, HashSet<NuGetVersion>> badPackagesAndCandidateVersionsDictionary = new(StringComparer.OrdinalIgnoreCase);
337
+
338
+ // and for each of those packages, find all versions greater than the one that's currently installed
339
+ foreach ((string packageName, NuGetVersion packageVersion) in badPackagesAndVersions)
340
+ {
341
+ // this command dumps a JSON object with all versions of the specified package from all package sources
342
+ (exitCode, stdOut, stdErr) = await ProcessEx.RunAsync("dotnet", $"package search {packageName} --exact-match --format json", workingDirectory: tempDirectory.FullName);
343
+ if (exitCode != 0)
344
+ {
345
+ continue;
346
+ }
347
+
348
+ // ensure collection exists
349
+ if (!badPackagesAndCandidateVersionsDictionary.ContainsKey(packageName))
350
+ {
351
+ badPackagesAndCandidateVersionsDictionary.Add(packageName, new HashSet<NuGetVersion>());
352
+ }
353
+
354
+ HashSet<NuGetVersion> foundVersions = badPackagesAndCandidateVersionsDictionary[packageName];
355
+
356
+ var json = JsonHelper.ParseNode(stdOut);
357
+ if (json?["searchResult"] is JsonArray searchResults)
358
+ {
359
+ foreach (var searchResult in searchResults)
360
+ {
361
+ if (searchResult?["packages"] is JsonArray packagesArray)
362
+ {
363
+ foreach (var package in packagesArray)
364
+ {
365
+ // in 8.0.xxx SDKs, the package version is in the `latestVersion` property, but in 9.0.xxx, it's `version`
366
+ var packageVersionProperty = package?["version"] ?? package?["latestVersion"];
367
+ if (packageVersionProperty is JsonValue latestVersion &&
368
+ latestVersion.GetValueKind() == JsonValueKind.String &&
369
+ NuGetVersion.TryParse(latestVersion.ToString(), out var nugetVersion) &&
370
+ nugetVersion > packageVersion)
371
+ {
372
+ foundVersions.Add(nugetVersion);
373
+ }
374
+ }
375
+ }
376
+ }
377
+ }
378
+ }
379
+
380
+ // generate all possible combinations
381
+ (string Key, NuGetVersion v)[][] expandedLists = badPackagesAndCandidateVersionsDictionary.Select(kvp => kvp.Value.Order().Select(v => (kvp.Key, v)).ToArray()).ToArray();
382
+ IEnumerable<(string PackageName, NuGetVersion PackageVersion)>[] product = expandedLists.CartesianProduct().ToArray();
383
+
384
+ // FUTURE WORK: pre-filter individual known package incompatibilities to reduce the number of combinations, e.g., if Package.A v1.0.0
385
+ // is incompatible with Package.B v2.0.0, then remove _all_ combinations with that pair
386
+
387
+ // this is the slow part
388
+ foreach (IEnumerable<(string PackageName, NuGetVersion PackageVersion)> candidateSet in product)
389
+ {
390
+ // rebuild candidate dependency list with the relevant versions
391
+ Dictionary<string, NuGetVersion> packageVersions = candidateSet.ToDictionary(candidateSet => candidateSet.PackageName, candidateSet => candidateSet.PackageVersion);
392
+ Dependency[] candidatePackages = packages.Select(p =>
393
+ {
394
+ if (packageVersions.TryGetValue(p.Name, out var version))
395
+ {
396
+ // create a new dependency with the updated version
397
+ return new Dependency(p.Name, version.ToString(), p.Type, IsDevDependency: p.IsDevDependency, IsOverride: p.IsOverride, IsUpdate: p.IsUpdate);
398
+ }
399
+
400
+ // not the dependency we're looking for, use whatever it already was in this set
401
+ return p;
402
+ }).ToArray();
403
+
404
+ if (await DependenciesAreCoherentAsync(repoRoot, projectPath, targetFramework, candidatePackages, logger))
405
+ {
406
+ // return as soon as we find a coherent set
407
+ return candidatePackages;
408
+ }
409
+ }
410
+
411
+ // no package resolution set found
412
+ return null;
413
+ }
414
+ finally
415
+ {
416
+ tempDirectory.Delete(recursive: true);
417
+ }
418
+ }
419
+
420
+ // fully expand all possible combinations using the algorithm from here:
421
+ // https://ericlippert.com/2010/06/28/computing-a-cartesian-product-with-linq/
422
+ private static IEnumerable<IEnumerable<T>> CartesianProduct<T>(this IEnumerable<IEnumerable<T>> sequences)
423
+ {
424
+ IEnumerable<IEnumerable<T>> emptyProduct = [[]];
425
+ return sequences.Aggregate(emptyProduct, (accumulator, sequence) => from accseq in accumulator
426
+ from item in sequence
427
+ select accseq.Concat([item]));
428
+ }
429
+
382
430
  private static ProjectRootElement CreateProjectRootElement(ProjectBuildFile buildFile)
383
431
  {
384
432
  var xmlString = buildFile.Contents.ToFullString();
@@ -569,7 +617,7 @@ internal static partial class MSBuildHelper
569
617
  return directoryPackagesPropsPath is not null;
570
618
  }
571
619
 
572
- internal static async Task<ImmutableArray<ProjectBuildFile>> LoadBuildFilesAsync(string repoRootPath, string projectPath, bool includeSdkPropsAndTargets = false)
620
+ internal static async Task<(ImmutableArray<ProjectBuildFile> ProjectBuildFiles, string[] TargetFrameworks)> LoadBuildFilesAndTargetFrameworksAsync(string repoRootPath, string projectPath)
573
621
  {
574
622
  var buildFileList = new List<string>
575
623
  {
@@ -579,6 +627,7 @@ internal static partial class MSBuildHelper
579
627
  // a global.json file might cause problems with the dotnet msbuild command; create a safe version temporarily
580
628
  TryGetGlobalJsonPath(repoRootPath, projectPath, out var globalJsonPath);
581
629
  var safeGlobalJsonName = $"{globalJsonPath}{Guid.NewGuid()}";
630
+ HashSet<string> targetFrameworks = new(StringComparer.OrdinalIgnoreCase);
582
631
 
583
632
  try
584
633
  {
@@ -607,16 +656,51 @@ internal static partial class MSBuildHelper
607
656
  // load the project even if it imports a file that doesn't exist (e.g. a file that's generated at restore
608
657
  // or build time).
609
658
  using var projectCollection = new ProjectCollection(); // do this in a one-off instance and don't pollute the global collection
610
- var project = Project.FromFile(projectPath, new ProjectOptions
659
+ Project project = Project.FromFile(projectPath, new ProjectOptions
611
660
  {
612
661
  LoadSettings = ProjectLoadSettings.IgnoreMissingImports,
613
662
  ProjectCollection = projectCollection,
614
663
  });
615
664
  buildFileList.AddRange(project.Imports.Select(i => i.ImportedProject.FullPath.NormalizePathToUnix()));
665
+
666
+ // use the MSBuild-evaluated value so we don't have to try to manually parse XML
667
+ IEnumerable<ProjectProperty> targetFrameworkProperties = project.Properties.Where(p => p.Name.Equals("TargetFramework", StringComparison.OrdinalIgnoreCase)).ToList();
668
+ IEnumerable<ProjectProperty> targetFrameworksProperties = project.Properties.Where(p => p.Name.Equals("TargetFrameworks", StringComparison.OrdinalIgnoreCase)).ToList();
669
+ IEnumerable<ProjectProperty> targetFrameworkVersionProperties = project.Properties.Where(p => p.Name.Equals("TargetFrameworkVersion", StringComparison.OrdinalIgnoreCase)).ToList();
670
+ foreach (ProjectProperty tfm in targetFrameworkProperties)
671
+ {
672
+ if (!string.IsNullOrWhiteSpace(tfm.EvaluatedValue))
673
+ {
674
+ targetFrameworks.Add(tfm.EvaluatedValue);
675
+ }
676
+ }
677
+
678
+ foreach (ProjectProperty tfms in targetFrameworksProperties)
679
+ {
680
+ foreach (string tfmValue in tfms.EvaluatedValue.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
681
+ {
682
+ targetFrameworks.Add(tfmValue);
683
+ }
684
+ }
685
+
686
+ if (targetFrameworks.Count == 0)
687
+ {
688
+ // Only try this if we haven't been able to resolve anything yet. This is because deep in the SDK, a
689
+ // `TargetFramework` of `netstandard2.0` (eventually) gets turned into `v2.0` and we don't want to
690
+ // interpret that as a .NET Framework 2.0 project.
691
+ foreach (ProjectProperty tfvm in targetFrameworkVersionProperties)
692
+ {
693
+ // `v0.0` is an error case where no TFM could be evaluated
694
+ if (tfvm.EvaluatedValue != "v0.0")
695
+ {
696
+ targetFrameworks.Add($"net{tfvm.EvaluatedValue.TrimStart('v').Replace(".", "")}");
697
+ }
698
+ }
699
+ }
616
700
  }
617
701
  catch (InvalidProjectFileException)
618
702
  {
619
- return [];
703
+ return ([], []);
620
704
  }
621
705
  finally
622
706
  {
@@ -627,18 +711,22 @@ internal static partial class MSBuildHelper
627
711
  }
628
712
 
629
713
  var repoRootPathPrefix = repoRootPath.NormalizePathToUnix() + "/";
630
- var buildFiles = includeSdkPropsAndTargets
631
- ? buildFileList.Distinct()
632
- : buildFileList
633
- .Where(f => f.StartsWith(repoRootPathPrefix, StringComparison.OrdinalIgnoreCase))
634
- .Distinct();
714
+ var buildFiles = buildFileList
715
+ .Where(f => f.StartsWith(repoRootPathPrefix, StringComparison.OrdinalIgnoreCase))
716
+ .Distinct();
635
717
  var result = buildFiles
636
718
  .Where(File.Exists)
637
719
  .Select(path => ProjectBuildFile.Open(repoRootPath, path))
638
720
  .ToImmutableArray();
639
- return result;
721
+ return (result, targetFrameworks.ToArray());
640
722
  }
641
723
 
642
724
  [GeneratedRegex("^\\s*NuGetData::Package=(?<PackageName>[^,]+), Version=(?<PackageVersion>.+)$")]
643
725
  private static partial Regex PackagePattern();
726
+
727
+ // Example output:
728
+ // NU1608: Detected package version outside of dependency constraint: SpecFlow.Tools.MsBuild.Generation 3.3.30 requires SpecFlow(= 3.3.30) but version SpecFlow 3.9.74 was resolved.
729
+ // PackageName-|+++++++++++++++++++++++++++++++| |++++|-PackageVersion
730
+ [GeneratedRegex("NU1608: [^:]+: (?<PackageName>[^ ]+) (?<PackageVersion>[^ ]+)")]
731
+ private static partial Regex PackageIncompatibilityWarningPattern();
644
732
  }
@@ -34,20 +34,11 @@ internal static class PathHelper
34
34
  public static string GetFullPathFromRelative(string rootPath, string relativePath)
35
35
  => Path.GetFullPath(JoinPath(rootPath, relativePath.NormalizePathToUnix()));
36
36
 
37
- /// <summary>
38
- /// Check in every directory from <paramref name="initialPath"/> up to <paramref name="rootPath"/> for the file specified in <paramref name="fileName"/>.
39
- /// </summary>
40
- /// <returns>The path of the found file or null.</returns>
41
- public static string? GetFileInDirectoryOrParent(string initialPath, string rootPath, string fileName, bool caseSensitive = true)
37
+ public static string[] GetAllDirectoriesToRoot(string initialDirectoryPath, string rootDirectoryPath)
42
38
  {
43
- if (File.Exists(initialPath))
44
- {
45
- initialPath = Path.GetDirectoryName(initialPath)!;
46
- }
47
-
48
39
  var candidatePaths = new List<string>();
49
- var rootDirectory = new DirectoryInfo(rootPath);
50
- var candidateDirectory = new DirectoryInfo(initialPath);
40
+ var rootDirectory = new DirectoryInfo(rootDirectoryPath);
41
+ var candidateDirectory = new DirectoryInfo(initialDirectoryPath);
51
42
  while (candidateDirectory.FullName != rootDirectory.FullName)
52
43
  {
53
44
  candidatePaths.Add(candidateDirectory.FullName);
@@ -58,8 +49,22 @@ internal static class PathHelper
58
49
  }
59
50
  }
60
51
 
61
- candidatePaths.Add(rootPath);
52
+ candidatePaths.Add(rootDirectoryPath);
53
+ return candidatePaths.ToArray();
54
+ }
55
+
56
+ /// <summary>
57
+ /// Check in every directory from <paramref name="initialPath"/> up to <paramref name="rootPath"/> for the file specified in <paramref name="fileName"/>.
58
+ /// </summary>
59
+ /// <returns>The path of the found file or null.</returns>
60
+ public static string? GetFileInDirectoryOrParent(string initialPath, string rootPath, string fileName, bool caseSensitive = true)
61
+ {
62
+ if (File.Exists(initialPath))
63
+ {
64
+ initialPath = Path.GetDirectoryName(initialPath)!;
65
+ }
62
66
 
67
+ var candidatePaths = GetAllDirectoriesToRoot(initialPath, rootPath);
63
68
  foreach (var candidatePath in candidatePaths)
64
69
  {
65
70
  try
@@ -1,4 +1,5 @@
1
1
  using System.Collections.Immutable;
2
+ using System.Diagnostics.CodeAnalysis;
2
3
  using System.Text.Json;
3
4
 
4
5
  using NuGetUpdater.Core.Discover;
@@ -29,7 +30,7 @@ public class DiscoveryWorkerTestBase
29
30
  protected static void ValidateWorkspaceResult(ExpectedWorkspaceDiscoveryResult expectedResult, WorkspaceDiscoveryResult actualResult)
30
31
  {
31
32
  Assert.NotNull(actualResult);
32
- Assert.Equal(expectedResult.FilePath, actualResult.FilePath);
33
+ Assert.Equal(expectedResult.FilePath.NormalizePathToUnix(), actualResult.FilePath.NormalizePathToUnix());
33
34
  ValidateDirectoryPackagesProps(expectedResult.DirectoryPackagesProps, actualResult.DirectoryPackagesProps);
34
35
  ValidateResultWithDependencies(expectedResult.GlobalJson, actualResult.GlobalJson);
35
36
  ValidateResultWithDependencies(expectedResult.DotNetToolsJson, actualResult.DotNetToolsJson);
@@ -50,7 +51,7 @@ public class DiscoveryWorkerTestBase
50
51
  Assert.NotNull(actualResult);
51
52
  }
52
53
 
53
- Assert.Equal(expectedResult.FilePath, actualResult.FilePath);
54
+ Assert.Equal(expectedResult.FilePath.NormalizePathToUnix(), actualResult.FilePath.NormalizePathToUnix());
54
55
  ValidateDependencies(expectedResult.Dependencies, actualResult.Dependencies);
55
56
  Assert.Equal(expectedResult.ExpectedDependencyCount ?? expectedResult.Dependencies.Length, actualResult.Dependencies.Length);
56
57
  }
@@ -64,12 +65,12 @@ public class DiscoveryWorkerTestBase
64
65
 
65
66
  foreach (var expectedProject in expectedProjects)
66
67
  {
67
- var actualProject = actualProjects.Single(p => p.FilePath == expectedProject.FilePath);
68
+ var actualProject = actualProjects.Single(p => p.FilePath.NormalizePathToUnix() == expectedProject.FilePath.NormalizePathToUnix());
68
69
 
69
- Assert.Equal(expectedProject.FilePath, actualProject.FilePath);
70
- AssertEx.Equal(expectedProject.Properties, actualProject.Properties);
70
+ Assert.Equal(expectedProject.FilePath.NormalizePathToUnix(), actualProject.FilePath.NormalizePathToUnix());
71
+ AssertEx.Equal(expectedProject.Properties, actualProject.Properties, PropertyComparer.Instance);
71
72
  AssertEx.Equal(expectedProject.TargetFrameworks, actualProject.TargetFrameworks);
72
- AssertEx.Equal(expectedProject.ReferencedProjectPaths, actualProject.ReferencedProjectPaths);
73
+ AssertEx.Equal(expectedProject.ReferencedProjectPaths.Select(PathHelper.NormalizePathToUnix), actualProject.ReferencedProjectPaths.Select(PathHelper.NormalizePathToUnix));
73
74
  ValidateDependencies(expectedProject.Dependencies, actualProject.Dependencies);
74
75
  Assert.Equal(expectedProject.ExpectedDependencyCount ?? expectedProject.Dependencies.Length, actualProject.Dependencies.Length);
75
76
  }
@@ -114,4 +115,21 @@ public class DiscoveryWorkerTestBase
114
115
  var resultJson = await File.ReadAllTextAsync(resultPath);
115
116
  return JsonSerializer.Deserialize<WorkspaceDiscoveryResult>(resultJson, DiscoveryWorker.SerializerOptions)!;
116
117
  }
118
+
119
+ internal class PropertyComparer : IEqualityComparer<Property>
120
+ {
121
+ public static PropertyComparer Instance { get; } = new();
122
+
123
+ public bool Equals(Property? x, Property? y)
124
+ {
125
+ return x?.Name == y?.Name &&
126
+ x?.Value == y?.Value &&
127
+ x?.SourceFilePath.NormalizePathToUnix() == y?.SourceFilePath.NormalizePathToUnix();
128
+ }
129
+
130
+ public int GetHashCode([DisallowNull] Property obj)
131
+ {
132
+ throw new NotImplementedException();
133
+ }
134
+ }
117
135
  }
@@ -25,11 +25,14 @@ public partial class DiscoveryWorkerTests
25
25
  <package id="NuGet.Core" version="2.11.1" targetFramework="net46" />
26
26
  <package id="NuGet.Server" version="2.11.2" targetFramework="net46" />
27
27
  <package id="RouteMagic" version="1.3" targetFramework="net46" />
28
- <package id="WebActivatorEx" version="2.1.0" targetFramework="net46"></package>
28
+ <package id="WebActivatorEx" version="2.1.0" targetFramework="net46" />
29
29
  </packages>
30
30
  """),
31
31
  ("myproj.csproj", """
32
32
  <Project>
33
+ <PropertyGroup>
34
+ <TargetFramework>net46</TargetFramework>
35
+ </PropertyGroup>
33
36
  </Project>
34
37
  """)
35
38
  ],
@@ -40,16 +43,21 @@ public partial class DiscoveryWorkerTests
40
43
  new()
41
44
  {
42
45
  FilePath = "myproj.csproj",
46
+ Properties = [
47
+ new("TargetFramework", "net46", "myproj.csproj"),
48
+ ],
49
+ TargetFrameworks = ["net46"],
43
50
  Dependencies = [
44
- new("Microsoft.CodeDom.Providers.DotNetCompilerPlatform", "1.0.0", DependencyType.PackagesConfig, TargetFrameworks: []),
45
- new("Microsoft.Net.Compilers", "1.0.1", DependencyType.PackagesConfig, TargetFrameworks: []),
46
- new("Microsoft.Web.Infrastructure", "1.0.0.0", DependencyType.PackagesConfig, TargetFrameworks: []),
47
- new("Microsoft.Web.Xdt", "2.1.1", DependencyType.PackagesConfig, TargetFrameworks: []),
48
- new("Newtonsoft.Json", "8.0.3", DependencyType.PackagesConfig, TargetFrameworks: []),
49
- new("NuGet.Core", "2.11.1", DependencyType.PackagesConfig, TargetFrameworks: []),
50
- new("NuGet.Server", "2.11.2", DependencyType.PackagesConfig, TargetFrameworks: []),
51
- new("RouteMagic", "1.3", DependencyType.PackagesConfig, TargetFrameworks: []),
52
- new("WebActivatorEx", "2.1.0", DependencyType.PackagesConfig, TargetFrameworks: []),
51
+ new("Microsoft.NETFramework.ReferenceAssemblies", "1.0.3", DependencyType.Unknown, TargetFrameworks: ["net46"], IsTransitive: true),
52
+ new("Microsoft.CodeDom.Providers.DotNetCompilerPlatform", "1.0.0", DependencyType.PackagesConfig, TargetFrameworks: ["net46"]),
53
+ new("Microsoft.Net.Compilers", "1.0.1", DependencyType.PackagesConfig, TargetFrameworks: ["net46"]),
54
+ new("Microsoft.Web.Infrastructure", "1.0.0.0", DependencyType.PackagesConfig, TargetFrameworks: ["net46"]),
55
+ new("Microsoft.Web.Xdt", "2.1.1", DependencyType.PackagesConfig, TargetFrameworks: ["net46"]),
56
+ new("Newtonsoft.Json", "8.0.3", DependencyType.PackagesConfig, TargetFrameworks: ["net46"]),
57
+ new("NuGet.Core", "2.11.1", DependencyType.PackagesConfig, TargetFrameworks: ["net46"]),
58
+ new("NuGet.Server", "2.11.2", DependencyType.PackagesConfig, TargetFrameworks: ["net46"]),
59
+ new("RouteMagic", "1.3", DependencyType.PackagesConfig, TargetFrameworks: ["net46"]),
60
+ new("WebActivatorEx", "2.1.0", DependencyType.PackagesConfig, TargetFrameworks: ["net46"]),
53
61
  ],
54
62
  }
55
63
  ],
@@ -376,5 +376,30 @@ public partial class DiscoveryWorkerTests
376
376
  ],
377
377
  });
378
378
  }
379
+
380
+ [Fact]
381
+
382
+ public async Task NoDependenciesReturnedIfNoTargetFrameworkCanBeResolved()
383
+ {
384
+ await TestDiscoveryAsync(
385
+ workspacePath: "",
386
+ files: [
387
+ ("myproj.csproj", """
388
+ <Project Sdk="Microsoft.NET.Sdk">
389
+ <PropertyGroup>
390
+ <TargetFramework>$(SomeCommonTfmThatCannotBeResolved)</TargetFramework>
391
+ </PropertyGroup>
392
+ <ItemGroup>
393
+ <PackageReference Include="Some.Package" Version="1.2.3" />
394
+ </ItemGroup>
395
+ </Project>
396
+ """)
397
+ ],
398
+ expectedResult: new()
399
+ {
400
+ FilePath = "",
401
+ Projects = []
402
+ });
403
+ }
379
404
  }
380
405
  }
@@ -0,0 +1,10 @@
1
+ namespace NuGetUpdater.Core.Test
2
+ {
3
+ public abstract class TestBase
4
+ {
5
+ protected TestBase()
6
+ {
7
+ MSBuildHelper.RegisterMSBuild(Environment.CurrentDirectory, Environment.CurrentDirectory);
8
+ }
9
+ }
10
+ }
@@ -1,16 +1,9 @@
1
- using System.Collections.Generic;
2
-
3
1
  using Xunit;
4
2
 
5
3
  namespace NuGetUpdater.Core.Test.Update;
6
4
 
7
- public class PackagesConfigUpdaterTests
5
+ public class PackagesConfigUpdaterTests : TestBase
8
6
  {
9
- public PackagesConfigUpdaterTests()
10
- {
11
- MSBuildHelper.RegisterMSBuild();
12
- }
13
-
14
7
  [Theory]
15
8
  [MemberData(nameof(PackagesDirectoryPathTestData))]
16
9
  public void PathToPackagesDirectoryCanBeDetermined(string projectContents, string dependencyName, string dependencyVersion, string expectedPackagesDirectoryPath)
@@ -10,7 +10,7 @@ namespace NuGetUpdater.Core.Test.Update;
10
10
  using TestFile = (string Path, string Content);
11
11
  using TestProject = (string Path, string Content, Guid ProjectId);
12
12
 
13
- public abstract class UpdateWorkerTestBase
13
+ public abstract class UpdateWorkerTestBase : TestBase
14
14
  {
15
15
  protected static Task TestNoChange(
16
16
  string dependencyName,
@@ -10,11 +10,6 @@ public partial class UpdateWorkerTests
10
10
  {
11
11
  public class DirsProj : UpdateWorkerTestBase
12
12
  {
13
- public DirsProj()
14
- {
15
- MSBuildHelper.RegisterMSBuild();
16
- }
17
-
18
13
  [Fact]
19
14
  public async Task UpdateSingleDependencyInDirsProj()
20
15
  {
@@ -8,11 +8,6 @@ public partial class UpdateWorkerTests
8
8
  {
9
9
  public class DotNetTools : UpdateWorkerTestBase
10
10
  {
11
- public DotNetTools()
12
- {
13
- MSBuildHelper.RegisterMSBuild();
14
- }
15
-
16
11
  [Fact]
17
12
  public async Task NoChangeWhenDotNetToolsJsonNotFound()
18
13
  {
@@ -8,11 +8,6 @@ public partial class UpdateWorkerTests
8
8
  {
9
9
  public class GlobalJson : UpdateWorkerTestBase
10
10
  {
11
- public GlobalJson()
12
- {
13
- MSBuildHelper.RegisterMSBuild();
14
- }
15
-
16
11
  [Fact]
17
12
  public async Task NoChangeWhenGlobalJsonNotFound()
18
13
  {
@@ -8,11 +8,6 @@ public partial class UpdateWorkerTests
8
8
  {
9
9
  public class Mixed : UpdateWorkerTestBase
10
10
  {
11
- public Mixed()
12
- {
13
- MSBuildHelper.RegisterMSBuild();
14
- }
15
-
16
11
  [Fact]
17
12
  public async Task ForPackagesProject_UpdatePackageReference_InBuildProps()
18
13
  {
@@ -9,11 +9,6 @@ public partial class UpdateWorkerTests
9
9
  {
10
10
  public class PackagesConfig : UpdateWorkerTestBase
11
11
  {
12
- public PackagesConfig()
13
- {
14
- MSBuildHelper.RegisterMSBuild();
15
- }
16
-
17
12
  [Fact]
18
13
  public async Task UpdateSingleDependencyInPackagesConfig()
19
14
  {
@@ -8,11 +8,6 @@ public partial class UpdateWorkerTests
8
8
  {
9
9
  public class Sdk : UpdateWorkerTestBase
10
10
  {
11
- public Sdk()
12
- {
13
- MSBuildHelper.RegisterMSBuild();
14
- }
15
-
16
11
  [Theory]
17
12
  [InlineData("net472")]
18
13
  [InlineData("netstandard2.0")]
@@ -175,24 +170,6 @@ public partial class UpdateWorkerTests
175
170
  ]);
176
171
  }
177
172
 
178
- [Fact]
179
- public async Task NoChange_WhenPackageHasVersionConstraint()
180
- {
181
- // Dependency package has version constraint
182
- await TestNoChangeforProject("AWSSDK.Core", "3.3.21.19", "3.7.300.20",
183
- projectContents: $"""
184
- <Project Sdk="Microsoft.NET.Sdk">
185
- <PropertyGroup>
186
- <TargetFramework>netstandard2.0</TargetFramework>
187
- </PropertyGroup>
188
- <ItemGroup>
189
- <PackageReference Include="AWSSDK.S3" Version="3.3.17.3" />
190
- <PackageReference Include="AWSSDK.Core" Version="3.3.21.19" />
191
- </ItemGroup>
192
- </Project>
193
- """);
194
- }
195
-
196
173
  [Fact]
197
174
  public async Task UpdateVersionAttribute_InProjectFile_ForPackageReferenceInclude_Windows()
198
175
  {
@@ -2558,5 +2535,35 @@ public partial class UpdateWorkerTests
2558
2535
  """
2559
2536
  );
2560
2537
  }
2538
+
2539
+ [Fact]
2540
+ public async Task UpdatingPackageAlsoUpdatesAnythingWithADependencyOnTheUpdatedPackage()
2541
+ {
2542
+ // updating SpecFlow from 3.3.30 requires that SpecFlow.Tools.MsBuild.Generation also be updated
2543
+ await TestUpdateForProject("SpecFlow", "3.3.30", "3.4.3",
2544
+ projectContents: """
2545
+ <Project Sdk="Microsoft.NET.Sdk">
2546
+ <PropertyGroup>
2547
+ <TargetFramework>netstandard2.0</TargetFramework>
2548
+ </PropertyGroup>
2549
+ <ItemGroup>
2550
+ <PackageReference Include="SpecFlow" Version="3.3.30" />
2551
+ <PackageReference Include="SpecFlow.Tools.MsBuild.Generation" Version="3.3.30" />
2552
+ </ItemGroup>
2553
+ </Project>
2554
+ """,
2555
+ expectedProjectContents: """
2556
+ <Project Sdk="Microsoft.NET.Sdk">
2557
+ <PropertyGroup>
2558
+ <TargetFramework>netstandard2.0</TargetFramework>
2559
+ </PropertyGroup>
2560
+ <ItemGroup>
2561
+ <PackageReference Include="SpecFlow" Version="3.4.3" />
2562
+ <PackageReference Include="SpecFlow.Tools.MsBuild.Generation" Version="3.4.3" />
2563
+ </ItemGroup>
2564
+ </Project>
2565
+ """
2566
+ );
2567
+ }
2561
2568
  }
2562
2569
  }
@@ -6,13 +6,8 @@ namespace NuGetUpdater.Core.Test.Utilities;
6
6
 
7
7
  using TestFile = (string Path, string Content);
8
8
 
9
- public class MSBuildHelperTests
9
+ public class MSBuildHelperTests : TestBase
10
10
  {
11
- public MSBuildHelperTests()
12
- {
13
- MSBuildHelper.RegisterMSBuild();
14
- }
15
-
16
11
  [Fact]
17
12
  public void GetRootedValue_FindsValue()
18
13
  {
@@ -110,15 +105,16 @@ public class MSBuildHelperTests
110
105
  [InlineData("<Project><PropertyGroup><TargetFrameworks>netstandard2.0</TargetFrameworks></PropertyGroup></Project>", "netstandard2.0", null)]
111
106
  [InlineData("<Project><PropertyGroup><TargetFrameworks> ; netstandard2.0 ; </TargetFrameworks></PropertyGroup></Project>", "netstandard2.0", null)]
112
107
  [InlineData("<Project><PropertyGroup><TargetFrameworks>netstandard2.0 ; netstandard2.1 ; </TargetFrameworks></PropertyGroup></Project>", "netstandard2.0", "netstandard2.1")]
113
- public void TfmsCanBeDeterminedFromProjectContents(string projectContents, string? expectedTfm1, string? expectedTfm2)
108
+ [InlineData("<Project><PropertyGroup><TargetFramework>netstandard2.0</TargetFramework><TargetFrameworkVersion Condition='False'>v4.7.2</TargetFrameworkVersion></PropertyGroup></Project>", "netstandard2.0", null)]
109
+ [InlineData("<Project><PropertyGroup><TargetFramework>$(PropertyThatCannotBeResolved)</TargetFramework></PropertyGroup></Project>", null, null)]
110
+ public async Task TfmsCanBeDeterminedFromProjectContents(string projectContents, string? expectedTfm1, string? expectedTfm2)
114
111
  {
115
112
  var projectPath = Path.GetTempFileName();
116
113
  try
117
114
  {
118
115
  File.WriteAllText(projectPath, projectContents);
119
116
  var expectedTfms = new[] { expectedTfm1, expectedTfm2 }.Where(tfm => tfm is not null).ToArray();
120
- var buildFile = ProjectBuildFile.Open(Path.GetDirectoryName(projectPath)!, projectPath);
121
- var actualTfms = MSBuildHelper.GetTargetFrameworkMonikers(ImmutableArray.Create(buildFile));
117
+ var (_buildFiles, actualTfms) = await MSBuildHelper.LoadBuildFilesAndTargetFrameworksAsync(Path.GetDirectoryName(projectPath)!, projectPath);
122
118
  AssertEx.Equal(expectedTfms, actualTfms);
123
119
  }
124
120
  finally
@@ -493,6 +489,45 @@ public class MSBuildHelperTests
493
489
  }
494
490
  }
495
491
 
492
+ [Fact]
493
+ public async Task DependencyConflictsCanBeResolved()
494
+ {
495
+ // the package `SpecFlow` was already updated from 3.3.30 to 3.4.3, but this causes a conflict with
496
+ // `SpecFlow.Tools.MsBuild.Generation` that needs to be resolved
497
+ var repoRoot = Directory.CreateTempSubdirectory($"test_{nameof(DependencyConflictsCanBeResolved)}_");
498
+ try
499
+ {
500
+ var projectPath = Path.Join(repoRoot.FullName, "project.csproj");
501
+ await File.WriteAllTextAsync(projectPath, """
502
+ <Project Sdk="Microsoft.NET.Sdk">
503
+ <PropertyGroup>
504
+ <TargetFramework>netstandard2.0</TargetFramework>
505
+ </PropertyGroup>
506
+ <ItemGroup>
507
+ <PackageReference Include="SpecFlow" Version="3.4.3" />
508
+ <PackageReference Include="SpecFlow.Tools.MsBuild.Generation" Version="3.3.30" />
509
+ </ItemGroup>
510
+ </Project>
511
+ """);
512
+ var dependencies = new[]
513
+ {
514
+ new Dependency("SpecFlow", "3.4.3", DependencyType.PackageReference),
515
+ new Dependency("SpecFlow.Tools.MsBuild.Generation", "3.3.30", DependencyType.PackageReference),
516
+ };
517
+ var resolvedDependencies = await MSBuildHelper.ResolveDependencyConflicts(repoRoot.FullName, projectPath, "netstandard2.0", dependencies, new Logger(true));
518
+ Assert.NotNull(resolvedDependencies);
519
+ Assert.Equal(2, resolvedDependencies.Length);
520
+ Assert.Equal("SpecFlow", resolvedDependencies[0].Name);
521
+ Assert.Equal("3.4.3", resolvedDependencies[0].Version);
522
+ Assert.Equal("SpecFlow.Tools.MsBuild.Generation", resolvedDependencies[1].Name);
523
+ Assert.Equal("3.4.3", resolvedDependencies[1].Version);
524
+ }
525
+ finally
526
+ {
527
+ repoRoot.Delete(recursive: true);
528
+ }
529
+ }
530
+
496
531
  public static IEnumerable<object[]> GetTopLevelPackageDependencyInfosTestData()
497
532
  {
498
533
  // simple case
@@ -167,7 +167,7 @@ namespace NuGetUpdater.Core.Test.Utilities
167
167
 
168
168
  private static async Task<string[]> LoadBuildFilesFromTemp(TemporaryDirectory temporaryDirectory, string relativeProjectPath)
169
169
  {
170
- var buildFiles = await MSBuildHelper.LoadBuildFilesAsync(temporaryDirectory.DirectoryPath, $"{temporaryDirectory.DirectoryPath}/{relativeProjectPath}");
170
+ var (buildFiles, _tfms) = await MSBuildHelper.LoadBuildFilesAndTargetFrameworksAsync(temporaryDirectory.DirectoryPath, $"{temporaryDirectory.DirectoryPath}/{relativeProjectPath}");
171
171
  var buildFilePaths = buildFiles.Select(f => f.RelativePath.NormalizePathToUnix()).ToArray();
172
172
  return buildFilePaths;
173
173
  }
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.252.0
4
+ version: 0.253.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-04-11 00:00:00.000000000 Z
11
+ date: 2024-04-18 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.252.0
19
+ version: 0.253.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.252.0
26
+ version: 0.253.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rubyzip
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -184,20 +184,6 @@ dependencies:
184
184
  - - "~>"
185
185
  - !ruby/object:Gem::Version
186
186
  version: 0.7.3
187
- - !ruby/object:Gem::Dependency
188
- name: stackprof
189
- requirement: !ruby/object:Gem::Requirement
190
- requirements:
191
- - - "~>"
192
- - !ruby/object:Gem::Version
193
- version: 0.2.16
194
- type: :development
195
- prerelease: false
196
- version_requirements: !ruby/object:Gem::Requirement
197
- requirements:
198
- - - "~>"
199
- - !ruby/object:Gem::Version
200
- version: 0.2.16
201
187
  - !ruby/object:Gem::Dependency
202
188
  name: turbo_tests
203
189
  requirement: !ruby/object:Gem::Requirement
@@ -314,6 +300,7 @@ files:
314
300
  - helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/FrameworkChecker/SupportedFrameworkFacts.cs
315
301
  - helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/NuGetUpdater.Core.Test.csproj
316
302
  - helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/TemporaryDirectory.cs
303
+ - helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/TestBase.cs
317
304
  - helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/TestExtensions.cs
318
305
  - helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/PackagesConfigUpdaterTests.cs
319
306
  - helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTestBase.cs
@@ -417,7 +404,7 @@ licenses:
417
404
  - Nonstandard
418
405
  metadata:
419
406
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
420
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.252.0
407
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.253.0
421
408
  post_install_message:
422
409
  rdoc_options: []
423
410
  require_paths: