Skip to content

Commit

Permalink
improve packages directory detection (#10912)
Browse files Browse the repository at this point in the history
* improve packages directory detection
  • Loading branch information
brettfo authored Nov 11, 2024
1 parent b5d0600 commit 313fcff
Show file tree
Hide file tree
Showing 2 changed files with 141 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<object[]> PackagesDirectoryPathTestData()
public static IEnumerable<object?[]> PackagesDirectoryPathTestData()
{
// project with namespace
yield return
[
// project contents
"""
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
Expand All @@ -28,14 +37,20 @@ public static IEnumerable<object[]> PackagesDirectoryPathTestData()
</ItemGroup>
</Project>
""",
// 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
"""
<Project>
<ItemGroup>
Expand All @@ -46,14 +61,20 @@ public static IEnumerable<object[]> PackagesDirectoryPathTestData()
</ItemGroup>
</Project>
""",
// 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
"""
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
Expand All @@ -64,9 +85,89 @@ public static IEnumerable<object[]> PackagesDirectoryPathTestData()
</ItemGroup>
</Project>
""",
// 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
"""
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Reference Include="Some.Other.Package, Version=1.2.3.4, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed">
<HintPath>..\..\..\still-a-usable-path\Some.Other.Package.1.2.3\lib\net45\Some.Other.Package.dll</HintPath>
<Private>True</Private>
</Reference>
</ItemGroup>
</Project>
""",
// packages.config contents
"""
<packages>
<package id="Newtonsoft.Json" version="7.0.1" targetFramework="net45" />
</packages>
""",
// 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
"""
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
</ItemGroup>
</Project>
""",
// packages.config contents
"""
<packages>
<package id="Newtonsoft.Json" version="7.0.1" targetFramework="net45" />
</packages>
""",
// 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
"""
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
</ItemGroup>
</Project>
""",
// packages.config contents
"""
<packages>
</packages>
""",
// dependency name
"Newtonsoft.Json",
// dependency version
"7.0.1",
// expected packages directory path
null
];
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Diagnostics;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using System.Xml.XPath;

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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, @"^(?<PackagesPath>.*)[/\\](?<PackageNameAndVersion>[^/\\]+)[/\\]lib[/\\](?<Tfm>[^/\\]+)[/\\](?<AssemblyName>[^/\\]+)$");
// 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:
// <Reference Include="Some.Dependency, Version=1.0.0.0, Culture=neutral, PublicKeyToken=abcd">
Expand Down

0 comments on commit 313fcff

Please sign in to comment.