Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improve packages directory detection #10912

Merged
merged 2 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improved regex and comment describing what it does. It's harder to read, but much more accurate.

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
Loading