|
// 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 disable
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.Globalization;
using System.Linq;
using System.Threading;
using Microsoft.Extensions.CommandLineUtils;
using NuGet.Commands;
using NuGet.Common;
#if DEBUG
using NuGet.CommandLine.XPlat.Commands.Package.Update;
using NuGet.CommandLine.XPlat.Commands.Package.PackageDownload;
#endif
namespace NuGet.CommandLine.XPlat
{
public static class Program
{
#if DEBUG
private const string DebugOption = "--debug";
#endif
private const string DotnetNuGetAppName = "dotnet nuget";
private const string DotnetPackageAppName = "NuGet.CommandLine.XPlat.dll package";
private const int DotnetPackageSearchTimeOut = 15;
internal static int Main(string[] args)
{
return MainInternal(args, virtualProjectBuilder: null);
}
#nullable enable
public static int Run(string[] args, IVirtualProjectBuilder virtualProjectBuilder)
{
return MainInternal(args, virtualProjectBuilder);
}
private static int MainInternal(string[] args, IVirtualProjectBuilder? virtualProjectBuilder)
{
var log = new CommandOutputLogger(LogLevel.Information);
return MainInternal(args, log, EnvironmentVariableWrapper.Instance, virtualProjectBuilder);
}
#nullable disable
/// <summary>
/// Internal Main. This is used for testing.
/// </summary>
internal static int MainInternal(string[] args, CommandOutputLogger log, IEnvironmentVariableReader environmentVariableReader, IVirtualProjectBuilder virtualProjectBuilder = null)
{
#if USEMSBUILDLOCATOR
try
{
// .NET JIT compiles one method at a time. If this method calls `MSBuildLocator` directly, the
// try block is never entered if Microsoft.Build.Locator.dll can't be found. So, run it in a
// lambda function to ensure we're in the try block. C# IIFE!
((Action)(() => Microsoft.Build.Locator.MSBuildLocator.RegisterDefaults()))();
}
catch
{
// MSBuildLocator is used only to enable Visual Studio debugging.
// It's not needed when using a patched dotnet sdk, so it doesn't matter if it fails.
}
#endif
#if DEBUG
var debugNuGetXPlat = environmentVariableReader.GetEnvironmentVariable("DEBUG_NUGET_XPLAT");
if (args.Contains(DebugOption) || string.Equals(bool.TrueString, debugNuGetXPlat, StringComparison.OrdinalIgnoreCase))
{
args = args.Where(arg => !StringComparer.OrdinalIgnoreCase.Equals(arg, DebugOption)).ToArray();
System.Diagnostics.Debugger.Launch();
}
#endif
// Optionally disable localization.
if (args.Any(arg => string.Equals(arg, CommandConstants.ForceEnglishOutputOption, StringComparison.OrdinalIgnoreCase)))
{
CultureUtility.DisableLocalization();
}
else
{
UILanguageOverride.Setup(log, environmentVariableReader);
}
log.LogDebug(string.Format(CultureInfo.CurrentCulture, Strings.Debug_CurrentUICulture, CultureInfo.DefaultThreadCurrentUICulture));
NuGet.Common.Migrations.MigrationRunner.Run();
// TODO: Migrating from Microsoft.Extensions.CommandLineUtils.CommandLineApplication to System.Commandline.Command
// If we are looking to add further commands here, we should also look to redesign this parsing logic at that time
// See related issues:
// - https://github.com/NuGet/Home/issues/11996
// - https://github.com/NuGet/Home/issues/11997
// - https://github.com/NuGet/Home/issues/13089
if (IsSystemCommandLineParsedCommand(args))
{
Func<ILoggerWithColor> getHidePrefixLogger = () =>
{
log.HidePrefixForInfoAndMinimal = true;
return log;
};
RootCommand rootCommand = new RootCommand();
// Commands called directly from the SDK CLI will use the SDK's common interactive option.
Option<bool> interactiveOption = new Option<bool>("--interactive");
interactiveOption.Description = Strings.AddPkg_InteractiveDescription;
interactiveOption.DefaultValueFactory = _ => Console.IsOutputRedirected;
if (args[0] == "package")
{
var packageCommand = new Command("package");
rootCommand.Subcommands.Add(packageCommand);
PackageSearchCommand.Register(packageCommand, getHidePrefixLogger);
#if DEBUG
PackageUpdateCommand.Register(packageCommand, interactiveOption, virtualProjectBuilder);
PackageDownloadCommand.Register(packageCommand, interactiveOption);
#endif
}
else
{
var nugetCommand = new Command("nuget");
rootCommand.Subcommands.Add(nugetCommand);
var lazyConsole = new Lazy<Spectre.Console.IAnsiConsole>(() => Spectre.Console.AnsiConsole.Console);
ConfigCommand.Register(nugetCommand, getHidePrefixLogger);
ConfigCommand.Register(rootCommand, getHidePrefixLogger);
Commands.Why.WhyCommand.Register(nugetCommand, lazyConsole, virtualProjectBuilder);
Commands.Why.WhyCommand.Register(rootCommand, lazyConsole, virtualProjectBuilder);
}
CancellationTokenSource tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromMinutes(DotnetPackageSearchTimeOut));
int exitCodeValue = 0;
ParseResult parseResult = rootCommand.Parse(args);
try
{
exitCodeValue = parseResult.Invoke();
}
catch (Exception ex)
{
LogException(ex, log);
exitCodeValue = ExitCodes.Error;
}
return exitCodeValue;
}
var app = InitializeApp(args, log, virtualProjectBuilder);
// Remove the correct item in array for "package" commands. Only do this when "add package", "remove package", etc... are being run.
if (app.Name == DotnetPackageAppName)
{
// package add ...
args[0] = null;
args = args
.Where(e => e != null)
.ToArray();
}
NetworkProtocolUtility.SetConnectionLimit();
XPlatUtility.SetUserAgent();
app.OnExecute(() =>
{
app.ShowHelp();
return 0;
});
log.LogVerbose(string.Format(CultureInfo.CurrentCulture, Strings.OutputNuGetVersion, app.FullName, app.LongVersionGetter()));
int exitCode = 0;
try
{
exitCode = app.Execute(args);
}
catch (Exception e)
{
bool handled = false;
string verb = null;
if (args.Length > 1)
{
// Redirect users nicely if they do 'dotnet nuget sources add' or 'dotnet nuget add sources'
if (StringComparer.OrdinalIgnoreCase.Compare(args[0], "sources") == 0)
{
verb = args[1];
}
else if (StringComparer.OrdinalIgnoreCase.Compare(args[1], "sources") == 0)
{
verb = args[0];
}
if (verb != null)
{
switch (verb.ToLowerInvariant())
{
case "add":
case "remove":
case "update":
case "enable":
case "disable":
case "list":
log.LogMinimal(string.Format(CultureInfo.CurrentCulture,
Strings.Sources_Redirect, $"dotnet nuget {verb} source"));
handled = true;
break;
default:
break;
}
}
}
if (!handled)
{
// Log the error
if (ExceptionLogger.Instance.ShowStack)
{
log.LogError(e.ToString());
}
else
{
log.LogError(ExceptionUtilities.DisplayMessage(e));
}
// Log the stack trace as verbose output.
log.LogVerbose(e.ToString());
if (e is CommandParsingException)
{
ShowBestHelp(app, args);
}
exitCode = 1;
}
}
// Limit the exit code range to 0-255 to support POSIX
if (exitCode < 0 || exitCode > 255)
{
exitCode = 1;
}
return exitCode;
}
private static bool IsSystemCommandLineParsedCommand(string[] args)
{
if (args.Length == 0)
{
return false;
}
string arg0 = args[0];
if (arg0 == "config" || arg0 == "why")
{
return true;
}
if (args.Length >= 2 && arg0 == "package")
{
string arg1 = args[1];
#if DEBUG
if (arg1 == "update" || arg1 == "download")
{
return true;
}
#endif
if (arg1 == "search")
{
return true;
}
}
return false;
}
internal static void LogException(Exception e, ILogger log)
{
// Log the error
if (ExceptionLogger.Instance.ShowStack)
{
log.LogError(e.ToString());
}
else
{
log.LogError(ExceptionUtilities.DisplayMessage(e));
}
// Log the stack trace as verbose output.
log.LogVerbose(e.ToString());
}
private static CommandLineApplication InitializeApp(string[] args, CommandOutputLogger log, IVirtualProjectBuilder virtualProjectBuilder)
{
// Many commands don't want prefixes output. Use this func instead of () => log to set the HidePrefix property first.
Func<ILoggerWithColor> getHidePrefixLogger = () =>
{
log.HidePrefixForInfoAndMinimal = true;
return log;
};
// Allow commands to set the NuGet log level
Action<LogLevel> setLogLevel = (logLevel) => log.VerbosityLevel = logLevel;
var app = new CommandLineApplication();
var msbuild = new MSBuildAPIUtility(log, virtualProjectBuilder);
if (args.Any() && args[0] == "package")
{
// "dotnet * package" commands
app.Name = DotnetPackageAppName;
AddPackageReferenceCommand.Register(app, () => log, () => new AddPackageReferenceCommandRunner(), () => msbuild.VirtualProjectBuilder);
RemovePackageReferenceCommand.Register(app, () => log, () => new RemovePackageReferenceCommandRunner(), () => msbuild.VirtualProjectBuilder);
ListPackageCommand.Register(app, getHidePrefixLogger, setLogLevel, () => new ListPackageCommandRunner(msbuild));
}
else
{
// "dotnet nuget *" commands
app.Name = DotnetNuGetAppName;
CommandParsers.Register(app, getHidePrefixLogger);
DeleteCommand.Register(app, getHidePrefixLogger);
PushCommand.Register(app, getHidePrefixLogger);
LocalsCommand.Register(app, getHidePrefixLogger);
VerifyCommand.Register(app, getHidePrefixLogger, setLogLevel, () => new VerifyCommandRunner());
TrustedSignersCommand.Register(app, getHidePrefixLogger, setLogLevel);
SignCommand.Register(app, getHidePrefixLogger, setLogLevel, () => new SignCommandRunner());
// The commands below are implemented with System.CommandLine, and are here only for `dotnet nuget --help`
ConfigCommand.Register(app);
Commands.Why.WhyCommand.Register(app);
}
app.FullName = Strings.App_FullName;
app.HelpOption(XPlatUtility.HelpOption);
app.VersionOption("--version", typeof(Program).Assembly.GetName().Version.ToString());
return app;
}
private static void ShowBestHelp(CommandLineApplication app, string[] args)
{
CommandLineApplication lastCommand = null;
List<CommandLineApplication> commands = app.Commands;
// tunnel down into the args, and show the best help possible.
foreach (string arg in args)
{
foreach (CommandLineApplication command in commands)
{
if (arg == command.Name)
{
lastCommand = command;
commands = command.Commands;
break;
}
}
}
if (lastCommand != null)
{
lastCommand.ShowHelp();
}
else
{
app.ShowHelp();
}
}
}
}
|