-
Notifications
You must be signed in to change notification settings - Fork 734
Add dotnet nuget why command
#5761
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
Changes from 49 commits
708f4e2
10245f8
2098b79
3d428bc
db9a7ce
ef25ca2
9170c26
b1b74f5
067430a
08e95fc
ae50998
3f153f9
8a750f1
25804a8
a44299b
073e9b5
7ed373a
aaee328
ebc4732
4a42f3e
d9a6482
904740e
69bfb3b
cb0c043
e55583a
7891af4
fa67ec9
961b7df
cf5771f
efaee79
0bba3a1
8ee8eba
31a6bf3
5c3ad52
852edf0
923a1df
527d448
506ee92
1297105
fec9de8
b164b31
2a1c576
dccf34c
50394da
8f3db89
d9c8890
6074b25
7aceac8
8aff030
ee68859
118735d
7b9a2e9
4a60b13
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| // 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. | ||
|
|
||
| #nullable enable | ||
|
|
||
| using System; | ||
| using System.Collections.Generic; | ||
| using NuGet.Shared; | ||
|
|
||
| namespace NuGet.CommandLine.XPlat | ||
| { | ||
| /// <summary> | ||
| /// Represents a node in the package dependency graph. | ||
| /// </summary> | ||
| internal class DependencyNode | ||
| { | ||
| public string Id { get; set; } | ||
| public string Version { get; set; } | ||
| public HashSet<DependencyNode> Children { get; set; } | ||
|
|
||
| public DependencyNode(string id, string version) | ||
| { | ||
| Id = id ?? throw new ArgumentNullException(nameof(id)); | ||
| Version = version ?? throw new ArgumentNullException(nameof(version)); | ||
| Children = new HashSet<DependencyNode>(new DependencyNodeComparer()); | ||
| } | ||
|
|
||
| public override int GetHashCode() | ||
| { | ||
| var hashCodeCombiner = new HashCodeCombiner(); | ||
| hashCodeCombiner.AddObject(Id); | ||
| hashCodeCombiner.AddObject(Version); | ||
| hashCodeCombiner.AddUnorderedSequence(Children); | ||
| return hashCodeCombiner.CombinedHash; | ||
| } | ||
| } | ||
|
|
||
| internal class DependencyNodeComparer : IEqualityComparer<DependencyNode> | ||
| { | ||
| public bool Equals(DependencyNode? x, DependencyNode? y) | ||
| { | ||
| if (x == null || y == null) | ||
| return false; | ||
|
|
||
| return string.Equals(x.Id, y.Id, StringComparison.CurrentCultureIgnoreCase); | ||
| } | ||
|
|
||
| public int GetHashCode(DependencyNode obj) | ||
| { | ||
| return obj.Id.GetHashCode(); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| // 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.CommandLine; | ||
| using System.CommandLine.Help; | ||
|
|
||
| namespace NuGet.CommandLine.XPlat | ||
| { | ||
| internal static class WhyCommand | ||
| { | ||
| private static CliArgument<string> Path = new CliArgument<string>("PROJECT|SOLUTION") | ||
| { | ||
| Description = Strings.WhyCommand_PathArgument_Description, | ||
| Arity = ArgumentArity.ExactlyOne | ||
| }; | ||
|
|
||
| private static CliArgument<string> Package = new CliArgument<string>("PACKAGE") | ||
| { | ||
| Description = Strings.WhyCommand_PackageArgument_Description, | ||
| Arity = ArgumentArity.ExactlyOne | ||
| }; | ||
|
|
||
| private static CliOption<List<string>> Frameworks = new CliOption<List<string>>("--framework", "-f") | ||
| { | ||
| Description = Strings.WhyCommand_FrameworksOption_Description, | ||
| Arity = ArgumentArity.OneOrMore | ||
| }; | ||
|
|
||
| private static HelpOption Help = new HelpOption() | ||
| { | ||
| Arity = ArgumentArity.Zero | ||
| }; | ||
|
|
||
| internal static void Register(CliCommand rootCommand, Func<ILoggerWithColor> getLogger) | ||
| { | ||
| var whyCommand = new CliCommand("why", Strings.WhyCommand_Description); | ||
|
|
||
| whyCommand.Arguments.Add(Path); | ||
| whyCommand.Arguments.Add(Package); | ||
| whyCommand.Options.Add(Frameworks); | ||
| whyCommand.Options.Add(Help); | ||
|
|
||
| whyCommand.SetAction((parseResult) => | ||
| { | ||
| ILoggerWithColor logger = getLogger(); | ||
|
|
||
| try | ||
| { | ||
| var whyCommandArgs = new WhyCommandArgs( | ||
| parseResult.GetValue(Path), | ||
| parseResult.GetValue(Package), | ||
| parseResult.GetValue(Frameworks), | ||
| logger); | ||
|
|
||
| int exitCode = WhyCommandRunner.ExecuteCommand(whyCommandArgs); | ||
| return exitCode; | ||
| } | ||
| catch (ArgumentException ex) | ||
| { | ||
| logger.LogError(ex.Message); | ||
| return ExitCodes.InvalidArguments; | ||
| } | ||
| }); | ||
|
|
||
| rootCommand.Subcommands.Add(whyCommand); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| // 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. | ||
|
|
||
| #nullable enable | ||
|
|
||
| using System; | ||
| using System.Collections.Generic; | ||
|
|
||
| namespace NuGet.CommandLine.XPlat | ||
| { | ||
| internal class WhyCommandArgs | ||
| { | ||
| public string Path { get; } | ||
| public string Package { get; } | ||
| public List<string> Frameworks { get; } | ||
| public ILoggerWithColor Logger { get; } | ||
|
|
||
| /// <summary> | ||
| /// A constructor for the arguments of the 'why' command. | ||
| /// </summary> | ||
| /// <param name="path">The path to the solution or project file.</param> | ||
| /// <param name="package">The package for which we show the dependency graphs.</param> | ||
| /// <param name="frameworks">The target framework(s) for which we show the dependency graphs.</param> | ||
| /// <param name="logger"></param> | ||
| public WhyCommandArgs( | ||
| string path, | ||
| string package, | ||
| List<string> frameworks, | ||
| ILoggerWithColor 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,201 @@ | ||
| // 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. | ||
|
|
||
| #nullable enable | ||
|
|
||
| using System; | ||
zivkan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| using System.Collections.Generic; | ||
| using System.Globalization; | ||
| using System.IO; | ||
| using System.Linq; | ||
| using Microsoft.Build.Evaluation; | ||
| using NuGet.CommandLine.XPlat.WhyCommandUtility; | ||
| using NuGet.ProjectModel; | ||
|
|
||
| namespace NuGet.CommandLine.XPlat | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick: Just wondering why the namespace is different to the project's default namespace + directory structure?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All commands and files in this project fall in the
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO the reasons to organize code on the filesystem is identical to the reasons to organize code in namespaces. Therefore, I consider it bad when they're out of sync. But I recognise that NuGet's existing code does this. I just think it's a bad pattern that we should stop copying. But I'm not going to hold up this PR because of this, and I don't know if anyone else in the team share my opinion. But for what it's worth, since this file's class is internal, there's no public API risk of moving it to a "more appropriate" namespace. |
||
| { | ||
| internal static class WhyCommandRunner | ||
| { | ||
| private const string ProjectAssetsFile = "ProjectAssetsFile"; | ||
|
|
||
| /// <summary> | ||
| /// Executes the 'why' command. | ||
| /// </summary> | ||
| /// <param name="whyCommandArgs">CLI arguments for the 'why' command.</param> | ||
| public static int ExecuteCommand(WhyCommandArgs whyCommandArgs) | ||
| { | ||
| bool validArgumentsUsed = ValidatePathArgument(whyCommandArgs.Path, whyCommandArgs.Logger) | ||
| && ValidatePackageArgument(whyCommandArgs.Package, whyCommandArgs.Logger); | ||
| if (!validArgumentsUsed) | ||
| { | ||
| return ExitCodes.InvalidArguments; | ||
| } | ||
|
|
||
| string targetPackage = whyCommandArgs.Package; | ||
|
|
||
| IEnumerable<string> projectPaths = Path.GetExtension(whyCommandArgs.Path).Equals(".sln") | ||
| ? MSBuildAPIUtility.GetProjectsFromSolution(whyCommandArgs.Path).Where(f => File.Exists(f)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm guessing that MSBuild only returns project files which exist. @jeffkl would know for sure. |
||
| : [whyCommandArgs.Path]; | ||
|
|
||
| foreach (var projectPath in projectPaths) | ||
| { | ||
| Project project = MSBuildAPIUtility.GetProject(projectPath); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we can enhance the MSBuildAPIUtility to just return the ProjectAndSolution type instead of selecting only the paths in I'm not familiar with these types, so let's see if @jeffkl has opinions on this. |
||
| LockFile? assetsFile = GetProjectAssetsFile(project, whyCommandArgs.Logger); | ||
|
|
||
| if (assetsFile != null) | ||
| { | ||
| ValidateFrameworksOptions(assetsFile, whyCommandArgs.Frameworks, whyCommandArgs.Logger); | ||
|
|
||
| Dictionary<string, List<DependencyNode>?>? dependencyGraphPerFramework = DependencyGraphFinder.GetAllDependencyGraphsForTarget( | ||
| assetsFile, | ||
| whyCommandArgs.Package, | ||
| whyCommandArgs.Frameworks); | ||
|
|
||
| if (dependencyGraphPerFramework != null) | ||
| { | ||
| whyCommandArgs.Logger.LogMinimal( | ||
nkolev92 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| string.Format( | ||
| Strings.WhyCommand_Message_DependencyGraphsFoundInProject, | ||
| assetsFile.PackageSpec.Name, | ||
| targetPackage)); | ||
|
|
||
| DependencyGraphPrinter.PrintAllDependencyGraphs(dependencyGraphPerFramework, targetPackage, whyCommandArgs.Logger); | ||
| } | ||
| else | ||
| { | ||
| whyCommandArgs.Logger.LogMinimal( | ||
| string.Format( | ||
| Strings.WhyCommand_Message_NoDependencyGraphsFoundInProject, | ||
| assetsFile.PackageSpec.Name, | ||
| targetPackage)); | ||
| } | ||
| } | ||
|
|
||
| ProjectCollection.GlobalProjectCollection.UnloadProject(project); | ||
| } | ||
|
|
||
| return ExitCodes.Success; | ||
| } | ||
|
|
||
| private static bool ValidatePathArgument(string path, ILoggerWithColor logger) | ||
| { | ||
| if (string.IsNullOrEmpty(path)) | ||
| { | ||
| logger.LogError( | ||
| string.Format( | ||
| CultureInfo.CurrentCulture, | ||
| Strings.WhyCommand_Error_ArgumentCannotBeEmpty, | ||
| "PROJECT|SOLUTION")); | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| if (!File.Exists(path) | ||
| || (!path.EndsWith("proj", StringComparison.OrdinalIgnoreCase) | ||
| && !path.EndsWith(".sln", StringComparison.OrdinalIgnoreCase))) | ||
| { | ||
| logger.LogError( | ||
| string.Format( | ||
| CultureInfo.CurrentCulture, | ||
| Strings.WhyCommand_Error_PathIsMissingOrInvalid, | ||
| path)); | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| private static bool ValidatePackageArgument(string package, ILoggerWithColor logger) | ||
| { | ||
| if (string.IsNullOrEmpty(package)) | ||
| { | ||
| logger.LogError( | ||
| string.Format( | ||
| CultureInfo.CurrentCulture, | ||
| Strings.WhyCommand_Error_ArgumentCannotBeEmpty, | ||
| "PACKAGE")); | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Validates and returns the assets file for the given project. | ||
| /// </summary> | ||
| /// <param name="project">Evaluated MSBuild project</param> | ||
| /// <param name="logger">Logger for the 'why' command</param> | ||
| /// <returns>Assets file for the given project. Returns null if there was any issue finding or parsing the assets file.</returns> | ||
| private static LockFile? GetProjectAssetsFile(Project project, ILoggerWithColor logger) | ||
| { | ||
| if (!MSBuildAPIUtility.IsPackageReferenceProject(project)) | ||
| { | ||
| logger.LogError( | ||
| string.Format( | ||
| CultureInfo.CurrentCulture, | ||
| Strings.Error_NotPRProject, | ||
| project.FullPath)); | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| string assetsPath = project.GetPropertyValue(ProjectAssetsFile); | ||
|
|
||
| if (!File.Exists(assetsPath)) | ||
| { | ||
| logger.LogError( | ||
| string.Format( | ||
| CultureInfo.CurrentCulture, | ||
| Strings.Error_AssetsFileNotFound, | ||
| project.FullPath)); | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| var lockFileFormat = new LockFileFormat(); | ||
| LockFile assetsFile = lockFileFormat.Read(assetsPath); | ||
|
|
||
| // assets file validation | ||
| if (assetsFile.PackageSpec == null | ||
| || assetsFile.Targets == null | ||
| || assetsFile.Targets.Count == 0) | ||
| { | ||
| logger.LogError( | ||
| string.Format( | ||
| CultureInfo.CurrentCulture, | ||
| Strings.WhyCommand_Error_InvalidAssetsFile, | ||
| assetsFile.Path, | ||
| project.FullPath)); | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| return assetsFile; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Validates that the input frameworks options have corresponding targets in the assets file. Outputs a warning message if a framework does not exist. | ||
| /// </summary> | ||
| /// <param name="assetsFile"></param> | ||
| /// <param name="inputFrameworks"></param> | ||
| /// <param name="logger"></param> | ||
| private static void ValidateFrameworksOptions(LockFile assetsFile, List<string> inputFrameworks, ILoggerWithColor logger) | ||
| { | ||
| foreach (var frameworkAlias in inputFrameworks) | ||
| { | ||
| if (assetsFile.GetTarget(frameworkAlias, runtimeIdentifier: null) == null) | ||
| { | ||
| logger.LogWarning( | ||
| string.Format( | ||
| CultureInfo.CurrentCulture, | ||
| Strings.WhyCommand_Warning_AssetsFileDoesNotContainSpecifiedTarget, | ||
| assetsFile.Path, | ||
| assetsFile.PackageSpec.Name, | ||
| frameworkAlias)); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.