dependabot-nuget 0.240.0 → 0.242.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/helpers/build +45 -0
  3. data/helpers/lib/NuGetUpdater/.editorconfig +364 -0
  4. data/helpers/lib/NuGetUpdater/.gitignore +5 -0
  5. data/helpers/lib/NuGetUpdater/Directory.Build.props +10 -0
  6. data/helpers/lib/NuGetUpdater/Directory.Common.props +16 -0
  7. data/helpers/lib/NuGetUpdater/NuGetProjects/Directory.Build.props +14 -0
  8. data/helpers/lib/NuGetUpdater/NuGetProjects/Directory.Build.targets +7 -0
  9. data/helpers/lib/NuGetUpdater/NuGetProjects/Directory.Packages.props +29 -0
  10. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.Build.Tasks/NuGet.Build.Tasks.csproj +27 -0
  11. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.CommandLine/AssemblyMetadataExtractor.cs +203 -0
  12. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.CommandLine/NuGet.CommandLine.csproj +33 -0
  13. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.Commands/NuGet.Commands.csproj +26 -0
  14. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.Common/NuGet.Common.csproj +21 -0
  15. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.Config +6 -0
  16. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.Configuration/NuGet.Configuration.csproj +24 -0
  17. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.Credentials/NuGet.Credentials.csproj +20 -0
  18. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.DependencyResolver.Core/NuGet.DependencyResolver.Core.csproj +22 -0
  19. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.Frameworks/NuGet.Frameworks.csproj +17 -0
  20. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.LibraryModel/NuGet.LibraryModel.csproj +17 -0
  21. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.PackageManagement/NuGet.PackageManagement.csproj +27 -0
  22. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.Packaging/NuGet.Packaging.csproj +28 -0
  23. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.ProjectModel/NuGet.ProjectModel.csproj +20 -0
  24. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.Protocol/NuGet.Protocol.csproj +21 -0
  25. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.Resolver/NuGet.Resolver.csproj +20 -0
  26. data/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.Versioning/NuGet.Versioning.csproj +17 -0
  27. data/helpers/lib/NuGetUpdater/NuGetProjects/README.md +1 -0
  28. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/FrameworkCheckCommand.cs +35 -0
  29. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/UpdateCommand.cs +43 -0
  30. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/NuGetUpdater.Cli.csproj +20 -0
  31. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Program.cs +31 -0
  32. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.FrameworkCheck.cs +42 -0
  33. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Update.cs +323 -0
  34. data/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/NuGetUpdater.Cli.Test.csproj +24 -0
  35. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Dependency.cs +3 -0
  36. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/DependencyType.cs +12 -0
  37. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/BuildFile.cs +97 -0
  38. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/DotNetToolsJsonBuildFile.cs +23 -0
  39. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/GlobalJsonBuildFile.cs +36 -0
  40. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/JsonBuildFile.cs +47 -0
  41. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/PackagesConfigBuildFile.cs +31 -0
  42. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/ProjectBuildFile.cs +94 -0
  43. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/XmlBuildFile.cs +14 -0
  44. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/CompatabilityChecker.cs +39 -0
  45. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/FrameworkCompatibilityService.cs +73 -0
  46. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/SupportedFrameworks.cs +146 -0
  47. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/NuGetUpdater.Core.csproj +27 -0
  48. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectManager.cs +316 -0
  49. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectResolver.cs +87 -0
  50. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/ConfigurationFile.cs +3 -0
  51. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/DotNetToolsJsonUpdater.cs +66 -0
  52. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/GlobalJsonUpdater.cs +48 -0
  53. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackagesConfigUpdater.cs +172 -0
  54. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs +498 -0
  55. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdateResult.cs +7 -0
  56. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdaterWorker.cs +105 -0
  57. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/JsonHelper.cs +222 -0
  58. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/Logger.cs +24 -0
  59. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +460 -0
  60. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/NuGetHelper.cs +15 -0
  61. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/PathHelper.cs +69 -0
  62. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/ProcessExtensions.cs +83 -0
  63. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/XmlExtensions.cs +124 -0
  64. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/DotNetToolsJsonBuildFileTests.cs +53 -0
  65. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/GlobalJsonBuildFileTests.cs +80 -0
  66. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/PackagesConfigBuildFileTests.cs +63 -0
  67. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/ProjectBuildFileTests.cs +154 -0
  68. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/FrameworkChecker/CompatibilityCheckerFacts.cs +64 -0
  69. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/FrameworkChecker/FrameworkCompatibilityServiceFacts.cs +122 -0
  70. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/FrameworkChecker/SupportedFrameworkFacts.cs +68 -0
  71. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/NuGetUpdater.Core.Test.csproj +23 -0
  72. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/TemporaryDirectory.cs +36 -0
  73. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/TestExtensions.cs +15 -0
  74. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/PackagesConfigUpdaterTests.cs +79 -0
  75. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorker.DirsProj.cs +201 -0
  76. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTestBase.cs +147 -0
  77. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.DotNetTools.cs +225 -0
  78. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.GlobalJson.cs +217 -0
  79. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.Mixed.cs +94 -0
  80. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.PackagesConfig.cs +938 -0
  81. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.Sdk.cs +2177 -0
  82. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/JsonHelperTests.cs +239 -0
  83. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs +598 -0
  84. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/SdkPackageUpdaterHelperTests.cs +179 -0
  85. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/SdkPackageUpdaterTests.cs +238 -0
  86. data/helpers/lib/NuGetUpdater/NuGetUpdater.sln +152 -0
  87. data/helpers/lib/NuGetUpdater/xunit.runner.json +4 -0
  88. data/lib/dependabot/nuget/metadata_finder.rb +4 -4
  89. metadata +91 -5
@@ -0,0 +1,460 @@
1
+ using System;
2
+ using System.Collections.Generic;
3
+ using System.Collections.Immutable;
4
+ using System.Diagnostics.CodeAnalysis;
5
+ using System.IO;
6
+ using System.Linq;
7
+ using System.Text;
8
+ using System.Text.Json;
9
+ using System.Text.RegularExpressions;
10
+ using System.Threading.Tasks;
11
+ using System.Xml;
12
+
13
+ using Microsoft.Build.Construction;
14
+ using Microsoft.Build.Definition;
15
+ using Microsoft.Build.Evaluation;
16
+ using Microsoft.Build.Exceptions;
17
+ using Microsoft.Build.Locator;
18
+
19
+ using NuGetUpdater.Core.Utilities;
20
+
21
+ namespace NuGetUpdater.Core;
22
+
23
+ internal static partial class MSBuildHelper
24
+ {
25
+ public static string MSBuildPath { get; private set; } = string.Empty;
26
+
27
+ public static bool IsMSBuildRegistered => MSBuildPath.Length > 0;
28
+
29
+ static MSBuildHelper()
30
+ {
31
+ RegisterMSBuild();
32
+ }
33
+
34
+ public static void RegisterMSBuild()
35
+ {
36
+ // Ensure MSBuild types are registered before calling a method that loads the types
37
+ if (!IsMSBuildRegistered)
38
+ {
39
+ var defaultInstance = MSBuildLocator.QueryVisualStudioInstances().First();
40
+ MSBuildPath = defaultInstance.MSBuildPath;
41
+ MSBuildLocator.RegisterInstance(defaultInstance);
42
+ }
43
+ }
44
+
45
+ public static string[] GetTargetFrameworkMonikers(ImmutableArray<ProjectBuildFile> buildFiles)
46
+ {
47
+ HashSet<string> targetFrameworkValues = new(StringComparer.OrdinalIgnoreCase);
48
+ Dictionary<string, string> propertyInfo = new(StringComparer.OrdinalIgnoreCase);
49
+
50
+ foreach (var buildFile in buildFiles)
51
+ {
52
+ var projectRoot = CreateProjectRootElement(buildFile);
53
+
54
+ foreach (var property in projectRoot.Properties)
55
+ {
56
+ if (property.Name.Equals("TargetFramework", StringComparison.OrdinalIgnoreCase) ||
57
+ property.Name.Equals("TargetFrameworks", StringComparison.OrdinalIgnoreCase))
58
+ {
59
+ targetFrameworkValues.Add(property.Value);
60
+ }
61
+ else if (property.Name.Equals("TargetFrameworkVersion", StringComparison.OrdinalIgnoreCase))
62
+ {
63
+ // For packages.config projects that use TargetFrameworkVersion, we need to convert it to TargetFramework
64
+ targetFrameworkValues.Add($"net{property.Value.TrimStart('v').Replace(".", "")}");
65
+ }
66
+ else
67
+ {
68
+ propertyInfo[property.Name] = property.Value;
69
+ }
70
+ }
71
+ }
72
+
73
+ HashSet<string> targetFrameworks = new(StringComparer.OrdinalIgnoreCase);
74
+
75
+ foreach (var targetFrameworkValue in targetFrameworkValues)
76
+ {
77
+ var tfms = targetFrameworkValue;
78
+ tfms = GetRootedValue(tfms, propertyInfo);
79
+
80
+ if (string.IsNullOrEmpty(tfms))
81
+ {
82
+ continue;
83
+ }
84
+
85
+ foreach (var tfm in tfms.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
86
+ {
87
+ targetFrameworks.Add(tfm);
88
+ }
89
+ }
90
+
91
+ return targetFrameworks.ToArray();
92
+ }
93
+
94
+ public static IEnumerable<string> GetProjectPathsFromSolution(string solutionPath)
95
+ {
96
+ var solution = SolutionFile.Parse(solutionPath);
97
+ return solution.ProjectsInOrder.Select(p => p.AbsolutePath);
98
+ }
99
+
100
+ public static IEnumerable<string> GetProjectPathsFromProject(string projFilePath)
101
+ {
102
+ var projectStack = new Stack<(string folderPath, ProjectRootElement)>();
103
+ var projectRootElement = ProjectRootElement.Open(projFilePath);
104
+
105
+ projectStack.Push((Path.GetFullPath(Path.GetDirectoryName(projFilePath)!), projectRootElement));
106
+
107
+ while (projectStack.Count > 0)
108
+ {
109
+ var (folderPath, tmpProject) = projectStack.Pop();
110
+ foreach (var projectReference in tmpProject.Items.Where(static x => x.ItemType == "ProjectReference" || x.ItemType == "ProjectFile"))
111
+ {
112
+ if (projectReference.Include is not { } projectPath)
113
+ {
114
+ continue;
115
+ }
116
+
117
+ projectPath = PathHelper.GetFullPathFromRelative(folderPath, projectPath);
118
+
119
+ var projectExtension = Path.GetExtension(projectPath).ToLowerInvariant();
120
+ if (projectExtension == ".proj")
121
+ {
122
+ // If there is some MSBuild logic that needs to run to fully resolve the path skip the project
123
+ if (File.Exists(projectPath))
124
+ {
125
+ var additionalProjectRootElement = ProjectRootElement.Open(projectPath);
126
+ projectStack.Push((Path.GetFullPath(Path.GetDirectoryName(projectPath)!), additionalProjectRootElement));
127
+ }
128
+ }
129
+ else if (projectExtension == ".csproj" || projectExtension == ".vbproj" || projectExtension == ".fsproj")
130
+ {
131
+ yield return projectPath;
132
+ }
133
+ }
134
+ }
135
+ }
136
+
137
+ public static IEnumerable<Dependency> GetTopLevelPackageDependenyInfos(ImmutableArray<ProjectBuildFile> buildFiles)
138
+ {
139
+ Dictionary<string, (string, bool)> packageInfo = new(StringComparer.OrdinalIgnoreCase);
140
+ Dictionary<string, string> packageVersionInfo = new(StringComparer.OrdinalIgnoreCase);
141
+ Dictionary<string, string> propertyInfo = new(StringComparer.OrdinalIgnoreCase);
142
+
143
+ foreach (var buildFile in buildFiles)
144
+ {
145
+ var projectRoot = CreateProjectRootElement(buildFile);
146
+
147
+ foreach (var packageItem in projectRoot.Items
148
+ .Where(i => (i.ItemType == "PackageReference" || i.ItemType == "GlobalPackageReference")))
149
+ {
150
+ var versionSpecification = packageItem.Metadata.FirstOrDefault(m => m.Name.Equals("Version", StringComparison.OrdinalIgnoreCase))?.Value
151
+ ?? packageItem.Metadata.FirstOrDefault(m => m.Name.Equals("VersionOverride", StringComparison.OrdinalIgnoreCase))?.Value
152
+ ?? string.Empty;
153
+ foreach (var attributeValue in new[] { packageItem.Include, packageItem.Update })
154
+ {
155
+ if (!string.IsNullOrWhiteSpace(attributeValue))
156
+ {
157
+ if (packageInfo.TryGetValue(attributeValue, out var existingInfo))
158
+ {
159
+ var existingVersion = existingInfo.Item1;
160
+ var existingUpdate = existingInfo.Item2;
161
+ // Retain the version from the Update reference since the intention
162
+ // would be to override the version of the Include reference.
163
+ var vSpec = string.IsNullOrEmpty(versionSpecification) || existingUpdate ? existingVersion : versionSpecification;
164
+
165
+ var isUpdate = existingUpdate && string.IsNullOrEmpty(packageItem.Include);
166
+ packageInfo[attributeValue] = (vSpec, isUpdate);
167
+ }
168
+ else
169
+ {
170
+ var isUpdate = !string.IsNullOrEmpty(packageItem.Update);
171
+ packageInfo[attributeValue] = (versionSpecification, isUpdate);
172
+ }
173
+ }
174
+ }
175
+ }
176
+
177
+ foreach (var packageItem in projectRoot.Items
178
+ .Where(i => i.ItemType == "PackageVersion" && !string.IsNullOrEmpty(i.Include)))
179
+ {
180
+ packageVersionInfo[packageItem.Include] = packageItem.Metadata.FirstOrDefault(m => m.Name.Equals("Version", StringComparison.OrdinalIgnoreCase))?.Value
181
+ ?? string.Empty;
182
+ }
183
+
184
+ foreach (var property in projectRoot.Properties)
185
+ {
186
+ // Short of evaluating the entire project, there's no way to _really_ know what package version is
187
+ // going to be used, and even then we might not be able to update it. As a best guess, we'll simply
188
+ // skip any property that has a condition _or_ where the condition is checking for an empty string.
189
+ var hasEmptyCondition = string.IsNullOrEmpty(property.Condition);
190
+ var conditionIsCheckingForEmptyString = string.Equals(property.Condition, $"$({property.Name}) == ''", StringComparison.OrdinalIgnoreCase);
191
+ if (hasEmptyCondition || conditionIsCheckingForEmptyString)
192
+ {
193
+ propertyInfo[property.Name] = property.Value;
194
+ }
195
+ }
196
+ }
197
+
198
+ foreach (var (name, info) in packageInfo)
199
+ {
200
+ var (version, isUpdate) = info;
201
+ if (version.Length != 0 || !packageVersionInfo.TryGetValue(name, out var packageVersion))
202
+ {
203
+ packageVersion = version;
204
+ }
205
+
206
+ // Walk the property replacements until we don't find another one.
207
+ packageVersion = GetRootedValue(packageVersion, propertyInfo);
208
+
209
+ packageVersion = packageVersion.TrimStart('[', '(').TrimEnd(']', ')');
210
+
211
+ // We don't know the version for range requirements or wildcard
212
+ // requirements, so return "" for these.
213
+ yield return packageVersion.Contains(',') || packageVersion.Contains('*')
214
+ ? new Dependency(name, string.Empty, DependencyType.Unknown, IsUpdate: isUpdate)
215
+ : new Dependency(name, packageVersion, DependencyType.Unknown, IsUpdate: isUpdate);
216
+ }
217
+ }
218
+
219
+ /// <summary>
220
+ /// Given an MSBuild string and a set of properties, returns our best guess at the final value MSBuild will evaluate to.
221
+ /// </summary>
222
+ /// <param name="msbuildString"></param>
223
+ /// <param name="propertyInfo"></param>
224
+ /// <returns></returns>
225
+ public static string GetRootedValue(string msbuildString, Dictionary<string, string> propertyInfo)
226
+ {
227
+ var seenProperties = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
228
+ while (TryGetPropertyName(msbuildString, out var propertyName))
229
+ {
230
+ if (!seenProperties.Add(propertyName))
231
+ {
232
+ throw new InvalidDataException($"Property '{propertyName}' has a circular reference.");
233
+ }
234
+
235
+ msbuildString = propertyInfo.TryGetValue(propertyName, out var propertyValue)
236
+ ? msbuildString.Replace($"$({propertyName})", propertyValue)
237
+ : throw new InvalidDataException($"Property '{propertyName}' was not found.");
238
+ }
239
+
240
+ return msbuildString;
241
+ }
242
+
243
+ public static bool TryGetPropertyName(string versionContent, [NotNullWhen(true)] out string? propertyName)
244
+ {
245
+ var startIndex = versionContent.IndexOf("$(", StringComparison.Ordinal);
246
+ if (startIndex != -1)
247
+ {
248
+ var endIndex = versionContent.IndexOf(')', startIndex);
249
+ if (endIndex != -1)
250
+ {
251
+ propertyName = versionContent.Substring(startIndex + 2, endIndex - startIndex - 2);
252
+ return true;
253
+ }
254
+ }
255
+
256
+ propertyName = null;
257
+ return false;
258
+ }
259
+
260
+ internal static async Task<bool> DependenciesAreCoherentAsync(string repoRoot, string projectPath, string targetFramework, Dependency[] packages, Logger logger)
261
+ {
262
+ var tempDirectory = Directory.CreateTempSubdirectory("package-dependency-coherence_");
263
+ try
264
+ {
265
+ var tempProjectPath = await CreateTempProjectAsync(tempDirectory, repoRoot, projectPath, targetFramework, packages);
266
+ var (exitCode, stdOut, stdErr) = await ProcessEx.RunAsync("dotnet", $"build \"{tempProjectPath}\"");
267
+
268
+ // NU1608: Detected package version outside of dependency constraint
269
+
270
+ return exitCode == 0 && !stdOut.Contains("NU1608");
271
+ }
272
+ finally
273
+ {
274
+ tempDirectory.Delete(recursive: true);
275
+ }
276
+ }
277
+
278
+ private static ProjectRootElement CreateProjectRootElement(ProjectBuildFile buildFile)
279
+ {
280
+ var xmlString = buildFile.Contents.ToFullString();
281
+ using var xmlStream = new MemoryStream(Encoding.UTF8.GetBytes(xmlString));
282
+ using var xmlReader = XmlReader.Create(xmlStream);
283
+ var projectRoot = ProjectRootElement.Create(xmlReader);
284
+
285
+ return projectRoot;
286
+ }
287
+
288
+ private static async Task<string> CreateTempProjectAsync(DirectoryInfo tempDir, string repoRoot, string projectPath, string targetFramework, Dependency[] packages)
289
+ {
290
+ var projectDirectory = Path.GetDirectoryName(projectPath);
291
+ projectDirectory ??= repoRoot;
292
+ var topLevelFiles = Directory.GetFiles(repoRoot);
293
+ var nugetConfigPath = PathHelper.GetFileInDirectoryOrParent(projectDirectory, repoRoot, "NuGet.Config", caseSensitive: false);
294
+ if (nugetConfigPath is not null)
295
+ {
296
+ File.Copy(nugetConfigPath, Path.Combine(tempDir.FullName, "NuGet.Config"));
297
+ }
298
+
299
+ var packageReferences = string.Join(
300
+ Environment.NewLine,
301
+ packages
302
+ .Where(p => !string.IsNullOrWhiteSpace(p.Version)) // empty `Version` attributes will cause the temporary project to not build
303
+ // If all PackageReferences for a package are update-only mark it as such, otherwise it can cause package incoherence errors which do not exist in the repo.
304
+ .Select(static p => $"<PackageReference {(p.IsUpdate ? "Update" : "Include")}=\"{p.Name}\" Version=\"[{p.Version}]\" />"));
305
+
306
+ var projectContents = $"""
307
+ <Project Sdk="Microsoft.NET.Sdk">
308
+ <PropertyGroup>
309
+ <TargetFramework>{targetFramework}</TargetFramework>
310
+ <GenerateDependencyFile>true</GenerateDependencyFile>
311
+ </PropertyGroup>
312
+ <ItemGroup>
313
+ {packageReferences}
314
+ </ItemGroup>
315
+ <Target Name="_CollectDependencies" DependsOnTargets="GenerateBuildDependencyFile">
316
+ <ItemGroup>
317
+ <_NuGetPackageData Include="@(NativeCopyLocalItems)" />
318
+ <_NuGetPackageData Include="@(ResourceCopyLocalItems)" />
319
+ <_NuGetPackageData Include="@(RuntimeCopyLocalItems)" />
320
+ <_NuGetPackageData Include="@(ResolvedAnalyzers)" />
321
+ <_NuGetPackageData Include="@(_PackageDependenciesDesignTime)">
322
+ <NuGetPackageId>%(_PackageDependenciesDesignTime.Name)</NuGetPackageId>
323
+ <NuGetPackageVersion>%(_PackageDependenciesDesignTime.Version)</NuGetPackageVersion>
324
+ </_NuGetPackageData>
325
+ </ItemGroup>
326
+ </Target>
327
+ <Target Name="_ReportDependencies" DependsOnTargets="_CollectDependencies">
328
+ <Message Text="NuGetData::Package=%(_NuGetPackageData.NuGetPackageId), Version=%(_NuGetPackageData.NuGetPackageVersion)"
329
+ Condition="'%(_NuGetPackageData.NuGetPackageId)' != '' AND '%(_NuGetPackageData.NuGetPackageVersion)' != ''"
330
+ Importance="High" />
331
+ </Target>
332
+ </Project>
333
+ """;
334
+ var tempProjectPath = Path.Combine(tempDir.FullName, "Project.csproj");
335
+ await File.WriteAllTextAsync(tempProjectPath, projectContents);
336
+
337
+ // prevent directory crawling
338
+ await File.WriteAllTextAsync(Path.Combine(tempDir.FullName, "Directory.Build.props"), "<Project />");
339
+ await File.WriteAllTextAsync(Path.Combine(tempDir.FullName, "Directory.Build.targets"), "<Project />");
340
+ await File.WriteAllTextAsync(Path.Combine(tempDir.FullName, "Directory.Packages.props"), "<Project />");
341
+
342
+ return tempProjectPath;
343
+ }
344
+
345
+ internal static async Task<Dependency[]> GetAllPackageDependenciesAsync(
346
+ string repoRoot, string projectPath, string targetFramework, Dependency[] packages, Logger? logger = null)
347
+ {
348
+ var tempDirectory = Directory.CreateTempSubdirectory("package-dependency-resolution_");
349
+ try
350
+ {
351
+ var tempProjectPath = await CreateTempProjectAsync(tempDirectory, repoRoot, projectPath, targetFramework, packages);
352
+
353
+ var (exitCode, stdout, stderr) = await ProcessEx.RunAsync("dotnet", $"build \"{tempProjectPath}\" /t:_ReportDependencies");
354
+
355
+ if (exitCode == 0)
356
+ {
357
+ var lines = stdout.Split('\n').Select(line => line.Trim());
358
+ var pattern = PackagePattern();
359
+ var allDependencies = lines
360
+ .Select(line => pattern.Match(line))
361
+ .Where(match => match.Success)
362
+ .Select(match => new Dependency(match.Groups["PackageName"].Value, match.Groups["PackageVersion"].Value, DependencyType.Unknown))
363
+ .ToArray();
364
+
365
+ return allDependencies;
366
+ }
367
+ else
368
+ {
369
+ logger?.Log($"dotnet build in {nameof(GetAllPackageDependenciesAsync)} failed. STDOUT: {stdout} STDERR: {stderr}");
370
+ return Array.Empty<Dependency>();
371
+ }
372
+ }
373
+ finally
374
+ {
375
+ try
376
+ {
377
+ tempDirectory.Delete(recursive: true);
378
+ }
379
+ catch
380
+ {
381
+ }
382
+ }
383
+ }
384
+
385
+ internal static string? GetGlobalJsonPath(string repoRootPath, string projectPath)
386
+ {
387
+ return PathHelper.GetFileInDirectoryOrParent(Path.GetDirectoryName(projectPath)!, repoRootPath, "global.json");
388
+ }
389
+
390
+ internal static async Task<ImmutableArray<ProjectBuildFile>> LoadBuildFiles(string repoRootPath, string projectPath)
391
+ {
392
+ var buildFileList = new List<string>()
393
+ {
394
+ projectPath.NormalizePathToUnix() // always include the starting project
395
+ };
396
+
397
+ // a global.json file might cause problems with the dotnet msbuild command; create a safe version temporarily
398
+ var globalJsonPath = GetGlobalJsonPath(repoRootPath, projectPath);
399
+ var safeGlobalJsonName = $"{globalJsonPath}{Guid.NewGuid()}";
400
+
401
+ try
402
+ {
403
+ // move the original
404
+ if (globalJsonPath is not null)
405
+ {
406
+ File.Move(globalJsonPath, safeGlobalJsonName);
407
+
408
+ // create a safe version with only certain top-level keys
409
+ var globalJsonContent = await File.ReadAllTextAsync(safeGlobalJsonName);
410
+ var json = JsonHelper.ParseNode(globalJsonContent);
411
+ var sdks = json["msbuild-sdks"];
412
+ if (sdks is not null)
413
+ {
414
+ var newObject = new Dictionary<string, object>()
415
+ {
416
+ { "msbuild-sdks", sdks }
417
+ };
418
+ var newContent = JsonSerializer.Serialize(newObject);
419
+ await File.WriteAllTextAsync(globalJsonPath, newContent);
420
+ }
421
+ }
422
+
423
+ // This is equivalent to running the command `dotnet msbuild <projectPath> /pp` to preprocess the file.
424
+ // The only difference is that we're specifying the `IgnoreMissingImports` flag which will allow us to
425
+ // load the project even if it imports a file that doesn't exist (e.g. a file that's generated at restore
426
+ // or build time).
427
+ using var projectCollection = new ProjectCollection(); // do this in a one-off instance and don't pollute the global collection
428
+ var project = Project.FromFile(projectPath, new ProjectOptions()
429
+ {
430
+ LoadSettings = ProjectLoadSettings.IgnoreMissingImports,
431
+ ProjectCollection = projectCollection,
432
+ });
433
+ buildFileList.AddRange(project.Imports.Select(i => i.ImportedProject.FullPath.NormalizePathToUnix()));
434
+ }
435
+ catch (InvalidProjectFileException)
436
+ {
437
+ return [];
438
+ }
439
+ finally
440
+ {
441
+ if (globalJsonPath is not null)
442
+ {
443
+ File.Move(safeGlobalJsonName, globalJsonPath, overwrite: true);
444
+ }
445
+ }
446
+
447
+ var repoRootPathPrefix = repoRootPath.NormalizePathToUnix() + "/";
448
+ var buildFilesInRepo = buildFileList
449
+ .Where(f => f.StartsWith(repoRootPathPrefix, StringComparison.OrdinalIgnoreCase))
450
+ .Distinct()
451
+ .ToArray();
452
+ var result = buildFilesInRepo
453
+ .Select(path => ProjectBuildFile.Open(repoRootPath, path))
454
+ .ToImmutableArray();
455
+ return result;
456
+ }
457
+
458
+ [GeneratedRegex("^\\s*NuGetData::Package=(?<PackageName>[^,]+), Version=(?<PackageVersion>.+)$")]
459
+ private static partial Regex PackagePattern();
460
+ }
@@ -0,0 +1,15 @@
1
+ using System.IO;
2
+
3
+ namespace NuGetUpdater.Core;
4
+
5
+ internal static class NuGetHelper
6
+ {
7
+ internal const string PackagesConfigFileName = "packages.config";
8
+
9
+ public static bool HasProjectConfigFile(string projectPath)
10
+ {
11
+ var projectDirectory = Path.GetDirectoryName(projectPath);
12
+ var packagesConfigPath = PathHelper.JoinPath(projectDirectory, PackagesConfigFileName);
13
+ return File.Exists(packagesConfigPath);
14
+ }
15
+ }
@@ -0,0 +1,69 @@
1
+ using System.Collections.Generic;
2
+ using System.IO;
3
+ using System.Linq;
4
+
5
+ namespace NuGetUpdater.Core;
6
+
7
+ internal static class PathHelper
8
+ {
9
+ private static readonly EnumerationOptions _caseInsensitiveEnumerationOptions = new()
10
+ {
11
+ MatchCasing = MatchCasing.CaseInsensitive,
12
+ };
13
+ private static readonly EnumerationOptions _caseSensitiveEnumerationOptions = new()
14
+ {
15
+ MatchCasing = MatchCasing.CaseSensitive,
16
+ };
17
+
18
+ public static string JoinPath(string? path1, string path2)
19
+ {
20
+ // don't root out the second path
21
+ if (path2.StartsWith('/'))
22
+ {
23
+ path2 = path2[1..];
24
+ }
25
+
26
+ return path1 is null
27
+ ? path2
28
+ : Path.Combine(path1, path2);
29
+ }
30
+
31
+ public static string NormalizePathToUnix(this string path) => path.Replace("\\", "/");
32
+
33
+ public static string GetFullPathFromRelative(string rootPath, string relativePath)
34
+ => Path.GetFullPath(JoinPath(rootPath, relativePath.NormalizePathToUnix()));
35
+
36
+ /// <summary>
37
+ /// Check in every directory from <paramref name="initialPath"/> up to <paramref name="rootPath"/> for the file specified in <paramref name="fileName"/>.
38
+ /// </summary>
39
+ /// <returns>The path of the found file or null.</returns>
40
+ public static string? GetFileInDirectoryOrParent(string initialPath, string rootPath, string fileName, bool caseSensitive = true)
41
+ {
42
+ var candidatePaths = new List<string>();
43
+ var rootDirectory = new DirectoryInfo(rootPath);
44
+ var candidateDirectory = new DirectoryInfo(initialPath);
45
+ while (candidateDirectory.FullName != rootDirectory.FullName)
46
+ {
47
+ candidatePaths.Add(candidateDirectory.FullName);
48
+ candidateDirectory = candidateDirectory.Parent;
49
+ if (candidateDirectory is null)
50
+ {
51
+ break;
52
+ }
53
+ }
54
+
55
+ candidatePaths.Add(rootPath);
56
+
57
+ foreach (var candidatePath in candidatePaths)
58
+ {
59
+ var files = Directory.EnumerateFiles(candidatePath, fileName, caseSensitive ? _caseSensitiveEnumerationOptions : _caseInsensitiveEnumerationOptions);
60
+
61
+ if (files.Any())
62
+ {
63
+ return files.First();
64
+ }
65
+ }
66
+
67
+ return null;
68
+ }
69
+ }
@@ -0,0 +1,83 @@
1
+ using System;
2
+ using System.Diagnostics;
3
+ using System.Text;
4
+ using System.Threading;
5
+ using System.Threading.Tasks;
6
+
7
+ namespace NuGetUpdater.Core;
8
+
9
+ public static class ProcessEx
10
+ {
11
+ public static Task<(int ExitCode, string Output, string Error)> RunAsync(string fileName, string arguments = "", string? workingDirectory = null)
12
+ {
13
+ var tcs = new TaskCompletionSource<(int, string, string)>();
14
+
15
+ var redirectInitiated = new ManualResetEventSlim();
16
+ var process = new Process
17
+ {
18
+ StartInfo =
19
+ {
20
+ FileName = fileName,
21
+ Arguments = arguments,
22
+ UseShellExecute = false, // required to redirect output
23
+ RedirectStandardOutput = true,
24
+ RedirectStandardError = true,
25
+ },
26
+ EnableRaisingEvents = true
27
+ };
28
+
29
+ if (workingDirectory is not null)
30
+ {
31
+ process.StartInfo.WorkingDirectory = workingDirectory;
32
+ }
33
+
34
+ var stdout = new StringBuilder();
35
+ var stderr = new StringBuilder();
36
+
37
+ process.OutputDataReceived += (_, e) => stdout.AppendLine(e.Data);
38
+ process.ErrorDataReceived += (_, e) => stderr.AppendLine(e.Data);
39
+
40
+ process.Exited += (sender, args) =>
41
+ {
42
+ // It is necessary to wait until we have invoked 'BeginXReadLine' for our redirected IO. Then,
43
+ // we must call WaitForExit to make sure we've received all OutputDataReceived/ErrorDataReceived calls
44
+ // or else we'll be returning a list we're still modifying. For paranoia, we'll start a task here rather
45
+ // than enter right back into the Process type and start a wait which isn't guaranteed to be safe.
46
+ Task.Run(() =>
47
+ {
48
+ redirectInitiated.Wait();
49
+ redirectInitiated.Dispose();
50
+ redirectInitiated = null;
51
+
52
+ process.WaitForExit();
53
+
54
+ tcs.TrySetResult((process.ExitCode, stdout.ToString(), stderr.ToString()));
55
+ process.Dispose();
56
+ });
57
+ };
58
+
59
+ #if DEBUG
60
+ // don't hang when running locally
61
+ var timeout = TimeSpan.FromSeconds(20);
62
+ Task.Delay(timeout).ContinueWith(_ =>
63
+ {
64
+ if (!tcs.Task.IsCompleted && !Debugger.IsAttached)
65
+ {
66
+ tcs.SetException(new Exception($"Process failed to exit after {timeout.TotalSeconds} seconds: {fileName} {arguments}\nstdout:\n{stdout}\n\nstderr:\n{stderr}"));
67
+ }
68
+ });
69
+ #endif
70
+
71
+ if (!process.Start())
72
+ {
73
+ throw new InvalidOperationException("Process failed to start");
74
+ }
75
+
76
+ process.BeginOutputReadLine();
77
+ process.BeginErrorReadLine();
78
+
79
+ redirectInitiated.Set();
80
+
81
+ return tcs.Task;
82
+ }
83
+ }