diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/PackagesConfigUpdaterTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/PackagesConfigUpdaterTests.cs index 78e831da8c..3697a1f2e9 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/PackagesConfigUpdaterTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/PackagesConfigUpdaterTests.cs @@ -6,18 +6,27 @@ public class PackagesConfigUpdaterTests : TestBase { [Theory] [MemberData(nameof(PackagesDirectoryPathTestData))] - public void PathToPackagesDirectoryCanBeDetermined(string projectContents, string dependencyName, string dependencyVersion, string expectedPackagesDirectoryPath) + public async Task PathToPackagesDirectoryCanBeDetermined(string projectContents, string? packagesConfigContents, string dependencyName, string dependencyVersion, string expectedPackagesDirectoryPath) { + using var tempDir = new TemporaryDirectory(); + string? packagesConfigPath = null; + if (packagesConfigContents is not null) + { + packagesConfigPath = Path.Join(tempDir.DirectoryPath, "packages.config"); + await File.WriteAllTextAsync(packagesConfigPath, packagesConfigContents); + } + var projectBuildFile = ProjectBuildFile.Parse("/", "project.csproj", projectContents); - var actualPackagesDirectorypath = PackagesConfigUpdater.GetPathToPackagesDirectory(projectBuildFile, dependencyName, dependencyVersion, "packages.config"); + var actualPackagesDirectorypath = PackagesConfigUpdater.GetPathToPackagesDirectory(projectBuildFile, dependencyName, dependencyVersion, packagesConfigPath); Assert.Equal(expectedPackagesDirectoryPath, actualPackagesDirectorypath); } - public static IEnumerable PackagesDirectoryPathTestData() + public static IEnumerable PackagesDirectoryPathTestData() { // project with namespace yield return [ + // project contents """ @@ -28,14 +37,20 @@ public static IEnumerable PackagesDirectoryPathTestData() """, + // packages.config contents + null, + // dependency name "Newtonsoft.Json", + // dependency version "7.0.1", + // expected packages directory path "../packages" ]; // project without namespace yield return [ + // project contents """ @@ -46,14 +61,20 @@ public static IEnumerable PackagesDirectoryPathTestData() """, + // packages.config contents + null, + // dependency name "Newtonsoft.Json", + // dependency version "7.0.1", + // expected packages directory path "../packages" ]; // project with non-standard packages path yield return [ + // project contents """ @@ -64,9 +85,89 @@ public static IEnumerable PackagesDirectoryPathTestData() """, + // packages.config contents + null, + // dependency name "Newtonsoft.Json", + // dependency version "7.0.1", + // expected packages directory path "../not-a-path-you-would-expect" ]; + + // project without expected packages path, but has others + yield return + [ + // project contents + """ + + + + ..\..\..\still-a-usable-path\Some.Other.Package.1.2.3\lib\net45\Some.Other.Package.dll + True + + + + """, + // packages.config contents + """ + + + + """, + // dependency name + "Newtonsoft.Json", + // dependency version + "7.0.1", + // expected packages directory path + "../../../still-a-usable-path" + ]; + + // project without expected package, but exists in packages.config, default is returned + yield return + [ + // project contents + """ + + + + + """, + // packages.config contents + """ + + + + """, + // dependency name + "Newtonsoft.Json", + // dependency version + "7.0.1", + // expected packages directory path + "../packages" + ]; + + // project without expected package and not in packages.config + yield return + [ + // project contents + """ + + + + + """, + // packages.config contents + """ + + + """, + // dependency name + "Newtonsoft.Json", + // dependency version + "7.0.1", + // expected packages directory path + null + ]; } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackagesConfigUpdater.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackagesConfigUpdater.cs index 4c0b129011..87203c8466 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackagesConfigUpdater.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackagesConfigUpdater.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.Text; +using System.Text.RegularExpressions; using System.Xml.Linq; using System.Xml.XPath; @@ -213,8 +214,8 @@ private static Process[] GetLikelyNuGetSpawnedProcesses() var hintPathSubString = $"{dependencyName}.{dependencyVersion}"; string? partialPathMatch = null; - var hintPathNodes = projectBuildFile.Contents.Descendants().Where(e => e.IsHintPathNodeForDependency(dependencyName)); - foreach (var hintPathNode in hintPathNodes) + var specificHintPathNodes = projectBuildFile.Contents.Descendants().Where(e => e.IsHintPathNodeForDependency(dependencyName)).ToArray(); + foreach (var hintPathNode in specificHintPathNodes) { var hintPath = hintPathNode.GetContentValue(); var hintPathSubStringLocation = hintPath.IndexOf(hintPathSubString, StringComparison.OrdinalIgnoreCase); @@ -255,18 +256,49 @@ private static Process[] GetLikelyNuGetSpawnedProcesses() if (hasPackage) { // the dependency exists in the packages.config file, so it must be the second case - // the vast majority of projects found in the wild use this, and since we have nothing to look for, we'll just have to hope - partialPathMatch = "../packages"; + // at this point there's no perfect way to determine what the packages path is, but there's a really good chance that + // for any given package it looks something like this: + // ..\..\packages\Package.Name.[version]\lib\Tfm\Package.Name.dll + var genericHintPathNodes = projectBuildFile.Contents.Descendants().Where(IsHintPathNode).ToArray(); + if (genericHintPathNodes.Length > 0) + { + foreach (var hintPathNode in genericHintPathNodes) + { + var hintPath = hintPathNode.GetContentValue(); + var match = Regex.Match(hintPath, @"^(?.*)[/\\](?[^/\\]+)[/\\]lib[/\\](?[^/\\]+)[/\\](?[^/\\]+)$"); + // e.g., ..\..\packages \ Some.Package.1.2.3 \ lib\ net45 \ Some.Package.dll + if (match.Success) + { + partialPathMatch = match.Groups["PackagesPath"].Value; + break; + } + } + } + else + { + // we know the dependency is used, but we have absolutely no idea where the packages path is, so we'll default to something reasonable + partialPathMatch = "../packages"; + } } } return partialPathMatch?.NormalizePathToUnix(); } - private static bool IsHintPathNodeForDependency(this IXmlElementSyntax element, string dependencyName) + private static bool IsHintPathNode(this IXmlElementSyntax element) { if (element.Name.Equals("HintPath", StringComparison.OrdinalIgnoreCase) && element.Parent.Name.Equals("Reference", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + return false; + } + + private static bool IsHintPathNodeForDependency(this IXmlElementSyntax element, string dependencyName) + { + if (element.IsHintPathNode()) { // the include attribute will look like one of the following: //