dependabot-nuget 0.240.0 → 0.242.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ }