Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
708f4e2
applied changes from pragnya17-dotnet-nuget-why
advay26 Mar 28, 2024
10245f8
cleaning up some old code
advay26 Mar 28, 2024
2098b79
wipping
advay26 Apr 3, 2024
3d428bc
fixed duplicate paths bug
advay26 Apr 3, 2024
db9a7ce
trees are beautiful
advay26 Apr 5, 2024
ef25ca2
rip my sleep schedule
advay26 Apr 5, 2024
9170c26
everything works now
advay26 Apr 5, 2024
b1b74f5
refactoring
advay26 Apr 6, 2024
067430a
cleaning up strings
advay26 Apr 6, 2024
08e95fc
fixed redundant traversal bug
advay26 Apr 7, 2024
ae50998
minor string stuff
advay26 Apr 8, 2024
3f153f9
remove (*) deduplication, fix project reference bug
advay26 Apr 10, 2024
8a750f1
wip: recursion -> stack
advay26 Apr 16, 2024
25804a8
recursion -> stack is working now
advay26 Apr 17, 2024
a44299b
wipping
advay26 Apr 18, 2024
073e9b5
removed TODOs to clean up
advay26 Apr 18, 2024
7ed373a
added basic test; failed to run basic test;
advay26 Apr 21, 2024
aaee328
test stubs - commented out
advay26 Apr 22, 2024
ebc4732
cleanup + removed debugging code
advay26 Apr 23, 2024
4a42f3e
Moved files to diff folder
advay26 Apr 23, 2024
d9a6482
experimenting with tests
advay26 Apr 24, 2024
904740e
started addressing feedback, but wow it's a lot of work
advay26 May 3, 2024
69bfb3b
adjusted tests to static class
advay26 May 3, 2024
cb0c043
wip
advay26 May 7, 2024
e55583a
nit
advay26 May 8, 2024
7891af4
temp: working on project refs
advay26 May 9, 2024
fa67ec9
still wipping....
advay26 May 9, 2024
961b7df
added tests, TODOs for targetAlias matching in GetResolvedVersions; r…
advay26 May 10, 2024
cf5771f
added new test files, will fill them out later
advay26 May 10, 2024
efaee79
fixed failing test
advay26 May 10, 2024
0bba3a1
addressin smore feedback
advay26 May 10, 2024
8ee8eba
added func tests + dotnet integration tests
advay26 May 13, 2024
31a6bf3
cleaning up for PR
advay26 May 13, 2024
5c3ad52
cleaning up
advay26 May 13, 2024
852edf0
register dotnet nuget/package * commands separately
advay26 May 15, 2024
923a1df
moved print methods out to new class
advay26 May 15, 2024
527d448
wip: addressing feedback, trialing new method for top level packages/…
advay26 May 15, 2024
506ee92
refactoring
advay26 May 16, 2024
1297105
reverted MSBuildAPIUtility changes
advay26 May 16, 2024
fec9de8
nit
advay26 May 16, 2024
b164b31
fixed tests (at least locally)
advay26 May 16, 2024
2a1c576
whitespace changes?
advay26 May 16, 2024
dccf34c
added unit tests, fixed func tests, + some small nits
advay26 May 16, 2024
50394da
removed unnecessary param
advay26 May 16, 2024
8f3db89
trying to fix 'fully qualified path' error on linux and mac tests
advay26 May 16, 2024
d9c8890
address feedback, try to fix unit tests path issue
advay26 May 29, 2024
6074b25
fixed whitespace issue
advay26 May 29, 2024
7aceac8
fixed unit test path issue for remaining file paths
advay26 May 29, 2024
8aff030
trying to fix integration tests
advay26 May 29, 2024
ee68859
removed WhyCommandUtility sub directory
advay26 May 29, 2024
118735d
oops
advay26 May 29, 2024
7b9a2e9
please work
advay26 May 30, 2024
4a60b13
skip non-sdk stype projects + some more null dereference handling
advay26 May 31, 2024
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
Next Next commit
applied changes from pragnya17-dotnet-nuget-why
  • Loading branch information
advay26 committed May 3, 2024
commit 708f4e2770377791abcd756a6a4ce19b41f70e2b
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Threading.Tasks;

namespace NuGet.CommandLine.XPlat
{
internal interface IWhyPackageCommandRunner
{
Task ExecuteCommandAsync(WhyPackageArgs whyPackageArgs);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using NuGet.Common;

namespace NuGet.CommandLine.XPlat
{
internal class WhyPackageArgs
{
public ILogger Logger { get; }
public string Path { get; }
public string Package { get; }
public IEnumerable<string> Frameworks { get; }


/// <summary>
/// A constructor for the arguments of Why package
/// command. This is used to execute the runner's
/// method
/// </summary>
/// <param name="path"> The path to the solution or project file </param>
/// <param name="package">The package to look up the dependency paths for </param>
/// <param name="frameworks"> The user inputted frameworks to look up for their packages </param>
/// <param name="logger"></param>
public WhyPackageArgs(
string path,
string package,
IEnumerable<string> frameworks,
ILogger logger)
{
Path = path ?? throw new ArgumentNullException(nameof(path));
Package = package ?? throw new ArgumentNullException(nameof(package));
Frameworks = frameworks ?? throw new ArgumentNullException(nameof(frameworks));
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Build.Evaluation;
using NuGet.ProjectModel;

namespace NuGet.CommandLine.XPlat
{
internal class WhyPackageCommandRunner : IWhyPackageCommandRunner
{
private const string ProjectAssetsFile = "ProjectAssetsFile";
private const string ProjectName = "MSBuildProjectName";

/// <summary>
/// Use CLI arguments to execute why command.
/// </summary>
/// <param name="whyPackageArgs">CLI arguments.</param>
/// <returns></returns>
public Task ExecuteCommandAsync(WhyPackageArgs whyPackageArgs)
{
//TODO: figure out how to use current directory if path is not passed in
var projectsPaths = Path.GetExtension(whyPackageArgs.Path).Equals(".sln") ?
MSBuildAPIUtility.GetProjectsFromSolution(whyPackageArgs.Path).Where(f => File.Exists(f)) :
new List<string>(new string[] { whyPackageArgs.Path });

// the package you want to print the dependency paths for
var package = whyPackageArgs.Package;

var msBuild = new MSBuildAPIUtility(whyPackageArgs.Logger);

foreach (var projectPath in projectsPaths)
{
//Open project to evaluate properties for the assets
//file and the name of the project
var project = MSBuildAPIUtility.GetProject(projectPath);

if (!MSBuildAPIUtility.IsPackageReferenceProject(project))
{
Console.Error.WriteLine(string.Format(CultureInfo.CurrentCulture,
Strings.Error_NotPRProject,
projectPath));
Console.WriteLine();
continue;
}

var projectName = project.GetPropertyValue(ProjectName);

var assetsPath = project.GetPropertyValue(ProjectAssetsFile);

// If the file was not found, print an error message and continue to next project
if (!File.Exists(assetsPath))
{
Console.Error.WriteLine(string.Format(CultureInfo.CurrentCulture,
Strings.Error_AssetsFileNotFound,
projectPath));
Console.WriteLine();
}
else
{
var lockFileFormat = new LockFileFormat();
var assetsFile = lockFileFormat.Read(assetsPath);

// Assets file validation
if (assetsFile.PackageSpec != null &&
assetsFile.Targets != null &&
assetsFile.Targets.Count != 0)
{

// Get all the packages that are referenced in a project
// TODO: for now, just passing in true for the last two args (hardcoded) so I need to change that
var packages = msBuild.GetResolvedVersions(project.FullPath, whyPackageArgs.Frameworks, assetsFile, true, true);

// If packages equals null, it means something wrong happened
// with reading the packages and it was handled and message printed
// in MSBuildAPIUtility function, but we need to move to the next project
if (packages != null)
{
// No packages means that no package references at all were found in the current framework
if (!packages.Any())
{
Console.WriteLine(string.Format(Strings.WhyPkg_NoPackagesFoundForFrameworks, projectName));
}
else
{
Console.Write($"Project '{projectName}' has the following dependency graph for '{package}'\n");
RunWhyCommand(packages, assetsFile.Targets, package);
}
}
}
else
{
Console.WriteLine(string.Format(Strings.ListPkg_ErrorReadingAssetsFile, assetsPath));
}

// Unload project
ProjectCollection.GlobalProjectCollection.UnloadProject(project);
}
}

return Task.CompletedTask;
}

/// <summary>
/// Run the why command, print out output to console.
/// </summary>
/// <param name="packages">All packages in the project. Split up by top level packages and transitive packages.</param>
/// <param name="targetFrameworks">All target frameworks in project and corresponding info about frameworks.</param>
/// <param name="package">Package passed in as CLI argument.</param>
private void RunWhyCommand(IEnumerable<FrameworkPackages> packages, IList<LockFileTarget> targetFrameworks, string package)
{
foreach (var frameworkPackages in packages)
{
// Print framework name
var frameworkName = frameworkPackages.Framework;
PrintFrameworkHeader(frameworkName);

// Get all the top level packages in the framework
var frameworkTopLevelPackages = frameworkPackages.TopLevelPackages;
// Get all the libraries in the framework
var libraries = targetFrameworks.FirstOrDefault(i => i.Name == frameworkPackages.Framework).Libraries;

var dependencyGraph = FindPaths(frameworkTopLevelPackages, libraries, package);
PrintDependencyGraphInFramework(dependencyGraph);
}
}

/// <summary>
/// Find all dependency paths.
/// </summary>
/// <param name="topLevelPackages">"root nodes" of the graph.</param>
/// <param name="libraries">All libraries in a given project. </param>
/// <param name="destination">The package name CLI argument.</param>
/// <returns></returns>
private List<List<Dependency>> FindPaths(IEnumerable<InstalledPackageReference> topLevelPackages, IList<LockFileTargetLibrary> libraries, string destination)
{
List<List<Dependency>> dependencyGraph = new List<List<Dependency>>();
List<List<Dependency>> listOfPaths = new List<List<Dependency>>();
HashSet<Dependency> visited = new HashSet<Dependency>();
foreach (var package in topLevelPackages)
{
Dependency dep;
dep.name = package.Name;
dep.version = package.OriginalRequestedVersion;

List<Dependency> path = new List<Dependency>
{
dep
};

var dependencyPathsInFramework = DfsTraversal(package.Name, libraries, visited, path, listOfPaths, destination);
dependencyGraph.AddRange(dependencyPathsInFramework);
}
return dependencyGraph;
}

struct Dependency
{
public string name;
public string version;
}

/// <summary>
/// DFS from root node until destination is found (if destination ID exists)
/// </summary>
/// <param name="rootPackage">Top level packahe.</param>
/// <param name="libraries">All libraries in the target framework.</param>
/// <param name="visited">A set to keep track of all nodes that have been visisted.</param>
/// <param name="path">Keep track of path as DFS happens.</param>
/// <param name="listOfPaths">List of all dependency paths that lead to destination.</param>
/// <param name="destination">CLI argument with the package that is passed in.</param>
/// <returns></returns>
private List<List<Dependency>> DfsTraversal(string rootPackage, IList<LockFileTargetLibrary> libraries, HashSet<Dependency> visited, List<Dependency> path, List<List<Dependency>> listOfPaths, string destination)
{
if (rootPackage == destination)
{
// copy what is stored in list variable over to list that you allocate memory for
List<Dependency> pathToAdd = new List<Dependency>();
foreach (var p in path)
{
pathToAdd.Add(p);
}
listOfPaths.Add(pathToAdd);
return listOfPaths;
}

// Find the library that matches the root package's ID and get all its dependencies
LockFileTargetLibrary library = libraries.FirstOrDefault(i => i.Name == rootPackage);
var listDependencies = library.Dependencies;

if (listDependencies.Count != 0)
{
foreach (var dependency in listDependencies)
{
Dependency dep;
dep.name = dependency.Id;
dep.version = dependency.VersionRange.MinVersion.Version.ToString();
if (!visited.Contains(dep))
{
visited.Add(dep);
path.Add(dep);

// recurse
DfsTraversal(dependency.Id, libraries, visited, path, listOfPaths, destination);

// backtrack
path.RemoveAt(path.Count - 1);
visited.Remove(dep);
}
}
}

return listOfPaths;
}

/// <summary>
/// Print dependency graph with syntax/punctuation.
/// </summary>
/// <param name="listOfPaths">List of all paths that lead to destination.</param>
private void PrintDependencyGraphInFramework(List<List<Dependency>> listOfPaths)
{
if (listOfPaths.Count == 0)
{
Console.Write("\t\t");
Console.Write("No dependency paths found.");
}

foreach (var path in listOfPaths)
{
Console.Write("\t\t");
int iteration = 0;
foreach (var package in path)
{
Console.Write($"{package.name} ({package.version})");
// don't print arrows after the last package in the path
if (iteration < path.Count - 1)
{
Console.Write(" -> ");
}
iteration++;
}
Console.Write("\n");
}
}

/// <summary>
/// Print framework header.
/// </summary>
/// <param name="frameworkName">Name of framework.</param>
private void PrintFrameworkHeader(string frameworkName)
{
Console.Write("\t");
Console.Write($"[{frameworkName}]");
Console.Write(":\n");
}
}
}
63 changes: 63 additions & 0 deletions src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/WhyCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Globalization;
using Microsoft.Extensions.CommandLineUtils;
using NuGet.Common;

namespace NuGet.CommandLine.XPlat
{
internal class WhyCommand
{
public static void Register(CommandLineApplication app, Func<ILogger> getLogger, Func<IWhyPackageCommandRunner> getCommandRunner)
{
app.Command("why", why =>
{
why.Description = Strings.Why_Description;
why.HelpOption(XPlatUtility.HelpOption);

CommandArgument path = why.Argument(
"<PROJECT | SOLUTION>",
Strings.Why_PathDescription,
multipleValues: false);

CommandArgument package = why.Argument(
"<PACKAGE_NAME>",
Strings.WhyCommandPackageDescription,
multipleValues: false);

CommandOption frameworks = why.Option(
"--framework",
Strings.WhyFrameworkDescription,
CommandOptionType.MultipleValue);

why.OnExecute(async () =>
{
ValidatePackage(package);

var logger = getLogger();
var WhyPackageArgs = new WhyPackageArgs(
path.Value,
package.Value,
frameworks.Values,
logger);

var WhyPackageCommandRunner = getCommandRunner();
await WhyPackageCommandRunner.ExecuteCommandAsync(WhyPackageArgs);
return 0;
});
});
}

private static void ValidatePackage(CommandArgument argument)
{
if (string.IsNullOrEmpty(argument.Value))
{
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Strings.Error_PkgMissingArgument,
"Why",
argument.Name));
}
}
}
}
1 change: 1 addition & 0 deletions src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ private static CommandLineApplication InitializeApp(string[] args, CommandOutput
VerifyCommand.Register(app, getHidePrefixLogger, setLogLevel, () => new VerifyCommandRunner());
TrustedSignersCommand.Register(app, getHidePrefixLogger, setLogLevel);
SignCommand.Register(app, getHidePrefixLogger, setLogLevel, () => new SignCommandRunner());
WhyCommand.Register(app, getHidePrefixLogger, () => new WhyPackageCommandRunner());
}

app.FullName = Strings.App_FullName;
Expand Down
Loading