diff --git a/src/Cli/dotnet/Parser.cs b/src/Cli/dotnet/Parser.cs index 94e254147151..dcf28f549f2f 100644 --- a/src/Cli/dotnet/Parser.cs +++ b/src/Cli/dotnet/Parser.cs @@ -9,6 +9,7 @@ using Microsoft.DotNet.Tools; using Microsoft.DotNet.Tools.Format; using Microsoft.DotNet.Tools.Help; +using Microsoft.DotNet.Tools.Info; using Microsoft.DotNet.Tools.MSBuild; using Microsoft.DotNet.Tools.NuGet; using Microsoft.TemplateEngine.Cli; @@ -51,7 +52,8 @@ public static class Parser HelpCommandParser.GetCommand(), SdkCommandParser.GetCommand(), InstallSuccessCommand, - WorkloadCommandParser.GetCommand() + WorkloadCommandParser.GetCommand(), + InfoCommandParser.GetCommand() }; public static readonly CliOption DiagOption = CommonOptionsFactory.CreateDiagnosticsOption(recursive: false); diff --git a/src/Cli/dotnet/commands/dotnet-info/InfoCommand.cs b/src/Cli/dotnet/commands/dotnet-info/InfoCommand.cs new file mode 100644 index 000000000000..6d0f69a1d629 --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-info/InfoCommand.cs @@ -0,0 +1,197 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.CommandLine; +using Microsoft.DotNet.Cli; +using System.Text.Json; +using Microsoft.DotNet.Cli.Utils; + +using LocalizableStrings = Microsoft.DotNet.Cli.Utils.LocalizableStrings; +using RuntimeEnvironment = Microsoft.DotNet.Cli.Utils.RuntimeEnvironment; + +namespace Microsoft.DotNet.Tools.Info +{ + public class InfoCommand + { + private readonly ParseResult _parseResult; + + public InfoCommand(ParseResult parseResult) + { + _parseResult = parseResult; + } + + public static int Run(ParseResult result) + { + result.HandleDebugSwitch(); + var format = result.GetValue(InfoCommandParser.FormatOption); + if (format != InfoCommandParser.FormatOptions.json) { + PrintInfo(); + } else { + // To be implemented + PrintJsonInfo(); + } + return 0; + } + + public static void PrintVersion() + { + Reporter.Output.WriteLine(Product.Version); + } + + public static void PrintInfo() + { + DotnetVersionFile versionFile = DotnetFiles.VersionFileObject; + var commitSha = versionFile.CommitSha ?? "N/A"; + Reporter.Output.WriteLine($"{LocalizableStrings.DotNetSdkInfoLabel}"); + Reporter.Output.WriteLine($" Version: {Product.Version}"); + Reporter.Output.WriteLine($" Commit: {commitSha}"); + Reporter.Output.WriteLine($" Workload version: {WorkloadCommandParser.GetWorkloadsVersion()}"); + Reporter.Output.WriteLine(); + Reporter.Output.WriteLine($"{LocalizableStrings.DotNetRuntimeInfoLabel}"); + Reporter.Output.WriteLine($" OS Name: {RuntimeEnvironment.OperatingSystem}"); + Reporter.Output.WriteLine($" OS Version: {RuntimeEnvironment.OperatingSystemVersion}"); + Reporter.Output.WriteLine($" OS Platform: {RuntimeEnvironment.OperatingSystemPlatform}"); + Reporter.Output.WriteLine($" RID: {GetDisplayRid(versionFile)}"); + Reporter.Output.WriteLine($" Base Path: {AppContext.BaseDirectory}"); + PrintWorkloadsInfo(); + } + + private static void PrintWorkloadsInfo() + { + Reporter.Output.WriteLine(); + Reporter.Output.WriteLine($"{LocalizableStrings.DotnetWorkloadInfoLabel}"); + WorkloadCommandParser.ShowWorkloadsInfo(); + } + + private static string GetDisplayRid(DotnetVersionFile versionFile) + { + FrameworkDependencyFile fxDepsFile = new(); + + string currentRid = RuntimeInformation.RuntimeIdentifier; + + // if the current RID isn't supported by the shared framework, display the RID the CLI was + // built with instead, so the user knows which RID they should put in their "runtimes" section. + return fxDepsFile.IsRuntimeSupported(currentRid) ? + currentRid : + versionFile.BuildRid; + } + + public class RuntimeEnvironmentInfo + { + public string Name { get; set; } + public string Version { get; set; } + public string Platform { get; set; } + public string Rid { get; set; } + } + + public class SdkInfo + { + public string Commit { get; set; } + public string Version { get; set; } + } + public class HostInfo + { + public string Arch { get; set; } + public string Commit { get; set; } + public string Version { get; set; } + } + + public class InstalledRuntime + { + public string Name { get; set; } + public string Path { get; set; } + public string Version { get; set; } + } + + public class OtherArchInfo + { + public string Arch { get; set; } + public string BasePath { get; set; } + } + + public class WorkloadManifest + { + public string InstallType { get; set; } + public string Path { get; set; } + public string Version { get; set; } + } + + public class Workload + { + public List InstallSources { get; set; } + public WorkloadManifest Manifest { get; set; } + public string Name { get; set; } + } + + public class InstallSource + { + public string Name { get; set; } + public string Version { get; set; } + } + + public class DotNetInfo + { + public string BasePath { get; set; } + public Dictionary EnvVars { get; set; } + public string GlobalJson { get; set; } + public HostInfo Host { get; set; } + public List InstalledRuntimes { get; set; } + public List OtherArch { get; set; } + public RuntimeEnvironmentInfo RuntimeEnv { get; set; } + public SdkInfo Sdk { get; set; } + public List Workloads { get; set; } + + public string ToJson() + { + return JsonSerializer.Serialize(this, new JsonSerializerOptions { WriteIndented = true }); + } + } + + public static void PrintJsonInfo() + { + DotnetVersionFile versionFile = DotnetFiles.VersionFileObject; + var commitSha = versionFile.CommitSha ?? "N/A"; + var basePath = AppContext.BaseDirectory; + + var dotNetInfo = new DotNetInfo + { + BasePath = basePath, + EnvVars = new Dictionary + { + // Populate with environment variables + }, + GlobalJson = Environment.CurrentDirectory, + Host = new HostInfo + { + Arch = RuntimeInformation.ProcessArchitecture.ToString(), + Commit = commitSha, + Version = Product.Version + }, + InstalledRuntimes = new List + { + // Populate with installed runtime information + }, + OtherArch = new List + { + // Populate with other architecture information + }, + RuntimeEnv = new RuntimeEnvironmentInfo + { + Name = RuntimeEnvironment.OperatingSystem, + Platform = RuntimeEnvironment.OperatingSystemPlatform.ToString(), + Rid = GetDisplayRid(versionFile), + Version = RuntimeEnvironment.OperatingSystemVersion + }, + Sdk = new SdkInfo + { + Commit = commitSha, + Version = Product.Version + }, + Workloads = WorkloadCommandParser.GetWorkloadsInfo() + }; + + string jsonOutput = dotNetInfo.ToJson(); + Reporter.Output.WriteLine(jsonOutput); + } + } +} diff --git a/src/Cli/dotnet/commands/dotnet-info/InfoCommandParser.cs b/src/Cli/dotnet/commands/dotnet-info/InfoCommandParser.cs new file mode 100644 index 000000000000..3810229e87fc --- /dev/null +++ b/src/Cli/dotnet/commands/dotnet-info/InfoCommandParser.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.CommandLine; +using Microsoft.DotNet.Cli; + +namespace Microsoft.DotNet.Tools.Info +{ + internal static class InfoCommandParser + { + public static readonly string DocsLink = "TODO"; + + public enum FormatOptions + { + text, + json + } + + public static readonly CliOption FormatOption = new("--format", "-f") + { + Description = "" + }; + + private static readonly CliCommand Command = ConstructCommand(); + + public static CliCommand GetCommand() + { + return Command; + } + + private static CliCommand ConstructCommand() + { + DocumentedCommand command = new("info", DocsLink); + command.Options.Add(FormatOption); + command.SetAction(InfoCommand.Run); + + return command; + } + } +} + diff --git a/src/Cli/dotnet/commands/dotnet-workload/WorkloadCommandParser.cs b/src/Cli/dotnet/commands/dotnet-workload/WorkloadCommandParser.cs index a66915b36625..2e06dfa82bf0 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/WorkloadCommandParser.cs +++ b/src/Cli/dotnet/commands/dotnet-workload/WorkloadCommandParser.cs @@ -15,6 +15,9 @@ using CommonStrings = Microsoft.DotNet.Workloads.Workload.LocalizableStrings; using IReporter = Microsoft.DotNet.Cli.Utils.IReporter; +using Workload = Microsoft.DotNet.Tools.Info.InfoCommand.Workload; +using WorkloadManifest = Microsoft.DotNet.Tools.Info.InfoCommand.WorkloadManifest; + namespace Microsoft.DotNet.Cli { internal static class WorkloadCommandParser @@ -47,6 +50,41 @@ internal static string GetWorkloadsVersion(WorkloadInfoHelper workloadInfoHelper return workloadInfoHelper.ManifestProvider.GetWorkloadVersion(); } + internal static List GetWorkloadsInfo(ParseResult parseResult = null, WorkloadInfoHelper workloadInfoHelper = null, string dotnetDir = null) + { + var workloads = new List(); + workloadInfoHelper ??= new WorkloadInfoHelper(parseResult != null ? parseResult.HasOption(SharedOptions.InteractiveOption) : false); + IEnumerable installedList = workloadInfoHelper.InstalledSdkWorkloadIds; + InstalledWorkloadsCollection installedWorkloads = workloadInfoHelper.AddInstalledVsWorkloads(installedList); + + if (installedWorkloads.Count == 0) + { + return workloads; + } + + var manifestInfoDict = workloadInfoHelper.WorkloadResolver.GetInstalledManifests().ToDictionary(info => info.Id, StringComparer.OrdinalIgnoreCase); + + foreach (var workloadEntry in installedWorkloads.AsEnumerable()) + { + var workloadManifest = workloadInfoHelper.WorkloadResolver.GetManifestFromWorkload(new WorkloadId(workloadEntry.Key)); + var workloadFeatureBand = manifestInfoDict[workloadManifest.Id].ManifestFeatureBand; + + var workload = new Workload + { + Name = workloadEntry.Key, + Manifest = new WorkloadManifest + { + InstallType = WorkloadInstallType.GetWorkloadInstallType(new SdkFeatureBand(workloadFeatureBand), dotnetDir ?? Path.GetDirectoryName(Environment.ProcessPath)).ToString(), + Path = workloadManifest.ManifestPath, + Version = workloadManifest.Version + } + }; + + workloads.Add(workload); + } + + return workloads; + } internal static void ShowWorkloadsInfo(ParseResult parseResult = null, WorkloadInfoHelper workloadInfoHelper = null, IReporter reporter = null, string dotnetDir = null) { workloadInfoHelper ??= new WorkloadInfoHelper(parseResult != null ? parseResult.HasOption(SharedOptions.InteractiveOption) : false); diff --git a/src/Tests/dotnet-info.Tests/GivenThatIWantToShowInfoForDotnetCommand.cs b/src/Tests/dotnet-info.Tests/GivenThatIWantToShowInfoForDotnetCommand.cs new file mode 100644 index 000000000000..1ab635330a18 --- /dev/null +++ b/src/Tests/dotnet-info.Tests/GivenThatIWantToShowInfoForDotnetCommand.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.DotNet.Tools.Info; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Schema; + +namespace Microsoft.DotNet.Info.Tests +{ + public class GivenThatIWantToShowInfoForDotnetCommand : SdkTest + { + private const string InfoTextRegex = +@"\.NET SDK:\s*(?:.*\n?)+?Runtime Environment:\s*(?:.*\n?)+?\.NET workloads installed:\s*(?:.*\n?)+?"; + private const string RuntimeEnv = "RuntimeEnv"; + private const string Sdk = "Sdk"; + private const string Workloads = "Workloads"; + + public GivenThatIWantToShowInfoForDotnetCommand(ITestOutputHelper log) : base(log) + { + } + + [Fact] + public void WhenInfoCommandIsPassedToDotnetItPrintsInfo() + { + var cmd = new DotnetCommand(Log, "info") + .Execute(); + cmd.Should().Pass(); + cmd.StdOut.Should().MatchRegex(InfoTextRegex); + } + + [Fact] + public void WhenInfoCommandWithTextOptionIsPassedToDotnetItPrintsInfo() + { + var cmd = new DotnetCommand(Log, "info") + .Execute("--format", "text"); + + cmd.Should().Pass(); + cmd.StdOut.Should().MatchRegex(InfoTextRegex); + } + + [Fact] + public void WhenInfoCommandWithJsonOptionIsPassedToDotnetItPrintsJsonInfo() + { + var cmd = new DotnetCommand(Log, "info") + .Execute("--format", "json"); + + cmd.Should().Pass(); + + JToken parsedJson = JToken.Parse(cmd.StdOut); + + var expectedKeys = new List { Sdk, RuntimeEnv, Workloads }; + foreach (var key in expectedKeys) + { + Assert.True(parsedJson[key] != null, $"Expected key '{key}' not found in JSON output."); + } + + Assert.True(parsedJson[Sdk].Type == JTokenType.Object, "SDK should be an object."); + Assert.True(parsedJson[RuntimeEnv].Type == JTokenType.Object, "RuntimeEnvironment should be an object."); + Assert.True(parsedJson[Workloads].Type == JTokenType.Array, "Workloads should be an array."); + } + + [Fact] + public void WhenInvalidCommandIsPassedToDotnetInfoItPrintsError() + { + var cmd = new DotnetCommand(Log, "info") + .Execute("--invalid"); + + cmd.Should().Fail(); + cmd.StdErr.Should().Contain("Unrecognized command or argument"); + } + } +} diff --git a/src/Tests/dotnet.Tests/CommandTests/CompleteCommandTests.cs b/src/Tests/dotnet.Tests/CommandTests/CompleteCommandTests.cs index b4b2160fb370..60062d800ccf 100644 --- a/src/Tests/dotnet.Tests/CommandTests/CompleteCommandTests.cs +++ b/src/Tests/dotnet.Tests/CommandTests/CompleteCommandTests.cs @@ -34,6 +34,7 @@ public void GivenOnlyDotnetItSuggestsTopLevelCommandsAndOptions() "sdk", "fsi", "help", + "info", "list", "msbuild", "new", diff --git a/src/Tests/dotnet.Tests/dotnet.Tests.csproj b/src/Tests/dotnet.Tests/dotnet.Tests.csproj index 0179861656c2..b18dce4ed797 100644 --- a/src/Tests/dotnet.Tests/dotnet.Tests.csproj +++ b/src/Tests/dotnet.Tests/dotnet.Tests.csproj @@ -32,6 +32,7 @@ +