-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Add dotnet tool exec
command for one-shot tool execution
#49329
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 all commits
6ec461c
a36b14b
2f27844
9cc3c78
1322829
2b2e18c
7706c72
b9a03d2
da3e28e
3349a73
dd13c53
0ea6696
f689647
69cf295
623d36a
1f6fc19
aebf421
dda7732
fdc0161
1beb4bb
37f00f9
5ccd294
e07a46b
eae9ad8
9fd3045
fd1cc46
d15b933
48c5699
729fe49
4c9b747
6fab085
790d2cf
6f3e9a8
a4ee7d1
fac59a8
321fd59
56cc63a
68f0638
cde3627
2e33374
cb66e95
4bbafef
1d61173
ccde6b0
b080028
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,181 @@ | ||
// 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.CommandFactory; | ||
using Microsoft.DotNet.Cli.CommandFactory.CommandResolution; | ||
using Microsoft.DotNet.Cli.Commands.Tool.Install; | ||
using Microsoft.DotNet.Cli.Commands.Tool.Restore; | ||
using Microsoft.DotNet.Cli.Commands.Tool.Run; | ||
using Microsoft.DotNet.Cli.Extensions; | ||
using Microsoft.DotNet.Cli.NuGetPackageDownloader; | ||
using Microsoft.DotNet.Cli.ToolManifest; | ||
using Microsoft.DotNet.Cli.ToolPackage; | ||
using Microsoft.DotNet.Cli.Utils; | ||
using Microsoft.DotNet.Cli.Utils.Extensions; | ||
|
||
using Microsoft.Extensions.EnvironmentAbstractions; | ||
using NuGet.Common; | ||
using NuGet.Configuration; | ||
using NuGet.Packaging.Core; | ||
using NuGet.Versioning; | ||
|
||
|
||
namespace Microsoft.DotNet.Cli.Commands.Tool.Execute; | ||
|
||
internal class ToolExecuteCommand(ParseResult result, ToolManifestFinder? toolManifestFinder = null, string? currentWorkingDirectory = null) : CommandBase(result) | ||
{ | ||
const int ERROR_CANCELLED = 1223; // Windows error code for "Operation canceled by user" | ||
|
||
private readonly PackageIdentity _packageToolIdentityArgument = result.GetRequiredValue(ToolExecuteCommandParser.PackageIdentityArgument); | ||
private readonly IEnumerable<string> _forwardArguments = result.GetValue(ToolExecuteCommandParser.CommandArgument) ?? Enumerable.Empty<string>(); | ||
private readonly bool _allowRollForward = result.GetValue(ToolExecuteCommandParser.RollForwardOption); | ||
private readonly string? _configFile = result.GetValue(ToolExecuteCommandParser.ConfigOption); | ||
private readonly string[] _sources = result.GetValue(ToolExecuteCommandParser.SourceOption) ?? []; | ||
private readonly string[] _addSource = result.GetValue(ToolExecuteCommandParser.AddSourceOption) ?? []; | ||
private readonly bool _interactive = result.GetValue(ToolExecuteCommandParser.InteractiveOption); | ||
private readonly VerbosityOptions _verbosity = result.GetValue(ToolExecuteCommandParser.VerbosityOption); | ||
private readonly bool _yes = result.GetValue(ToolExecuteCommandParser.YesOption); | ||
private readonly IToolPackageDownloader _toolPackageDownloader = ToolPackageFactory.CreateToolPackageStoresAndDownloader().downloader; | ||
|
||
private readonly RestoreActionConfig _restoreActionConfig = new RestoreActionConfig(DisableParallel: result.GetValue(ToolCommandRestorePassThroughOptions.DisableParallelOption), | ||
NoCache: result.GetValue(ToolCommandRestorePassThroughOptions.NoCacheOption) || result.GetValue(ToolCommandRestorePassThroughOptions.NoHttpCacheOption), | ||
IgnoreFailedSources: result.GetValue(ToolCommandRestorePassThroughOptions.IgnoreFailedSourcesOption), | ||
Interactive: result.GetValue(ToolExecuteCommandParser.InteractiveOption)); | ||
|
||
private readonly ToolManifestFinder _toolManifestFinder = toolManifestFinder ?? new ToolManifestFinder(new DirectoryPath(currentWorkingDirectory ?? Directory.GetCurrentDirectory())); | ||
|
||
public override int Execute() | ||
{ | ||
VersionRange versionRange = _parseResult.GetVersionRange(); | ||
PackageId packageId = new PackageId(_packageToolIdentityArgument.Id); | ||
|
||
// Look in local tools manifest first, but only if version is not specified | ||
if (versionRange == null) | ||
{ | ||
var localToolsResolverCache = new LocalToolsResolverCache(); | ||
|
||
if (_toolManifestFinder.TryFindPackageId(packageId, out var toolManifestPackage)) | ||
{ | ||
var toolPackageRestorer = new ToolPackageRestorer( | ||
_toolPackageDownloader, | ||
_sources, | ||
overrideSources: [], | ||
_verbosity, | ||
_restoreActionConfig, | ||
localToolsResolverCache, | ||
new FileSystemWrapper()); | ||
|
||
var restoreResult = toolPackageRestorer.InstallPackage(toolManifestPackage, _configFile == null ? null : new FilePath(_configFile)); | ||
|
||
if (!restoreResult.IsSuccess) | ||
{ | ||
Reporter.Error.WriteLine(restoreResult.Message.Red()); | ||
return 1; | ||
} | ||
|
||
var localToolsCommandResolver = new LocalToolsCommandResolver( | ||
_toolManifestFinder, | ||
localToolsResolverCache); | ||
|
||
return ToolRunCommand.ExecuteCommand(localToolsCommandResolver, toolManifestPackage.CommandNames.Single().Value, _forwardArguments, _allowRollForward); | ||
} | ||
} | ||
|
||
var packageLocation = new PackageLocation( | ||
nugetConfig: _configFile != null ? new(_configFile) : null, | ||
sourceFeedOverrides: _sources, | ||
additionalFeeds: _addSource); | ||
|
||
(var bestVersion, var packageSource) = _toolPackageDownloader.GetNuGetVersion(packageLocation, packageId, _verbosity, versionRange, _restoreActionConfig); | ||
|
||
IToolPackage toolPackage; | ||
|
||
// TargetFramework is null, which means to use the current framework. Global tools can override the target framework to use (or select assets for), | ||
// but we don't support this for local or one-shot tools. | ||
if (!_toolPackageDownloader.TryGetDownloadedTool(packageId, bestVersion, targetFramework: null, out toolPackage)) | ||
{ | ||
if (!UserAgreedToRunFromSource(packageId, bestVersion, packageSource)) | ||
{ | ||
if (_interactive) | ||
{ | ||
Reporter.Error.WriteLine(CliCommandStrings.ToolDownloadCanceled.Red().Bold()); | ||
return ERROR_CANCELLED; | ||
} | ||
else | ||
{ | ||
Reporter.Error.WriteLine(CliCommandStrings.ToolDownloadNeedsConfirmation.Red().Bold()); | ||
return 1; | ||
} | ||
} | ||
|
||
// We've already determined which source we will use and displayed that in a confirmation message to the user. | ||
// So set the package location here to override the source feeds to just the source we already resolved to. | ||
// This does mean that we won't work with feeds that have a primary package but where the RID-specific packages are on | ||
// other feeds, but this is probably OK. | ||
var downloadPackageLocation = new PackageLocation( | ||
nugetConfig: _configFile != null ? new(_configFile) : null, | ||
sourceFeedOverrides: [packageSource.Source], | ||
additionalFeeds: _addSource); | ||
|
||
toolPackage = _toolPackageDownloader.InstallPackage( | ||
downloadPackageLocation, | ||
packageId: packageId, | ||
verbosity: _verbosity, | ||
versionRange: new VersionRange(bestVersion, true, bestVersion, true), | ||
isGlobalToolRollForward: false, | ||
restoreActionConfig: _restoreActionConfig); | ||
} | ||
|
||
var commandSpec = ToolCommandSpecCreator.CreateToolCommandSpec(toolPackage.Command.Name.Value, toolPackage.Command.Executable.Value, toolPackage.Command.Runner, _allowRollForward, _forwardArguments); | ||
var command = CommandFactoryUsingResolver.Create(commandSpec); | ||
var result = command.Execute(); | ||
return result.ExitCode; | ||
} | ||
|
||
private bool UserAgreedToRunFromSource(PackageId packageId, NuGetVersion version, PackageSource source) | ||
{ | ||
if (_yes) | ||
{ | ||
return true; | ||
} | ||
|
||
if (!_interactive) | ||
{ | ||
return false; | ||
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. Good call to default to 'no' in this instance! 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. Per @baronfel, npx does it the other way 😀. So we still might change it. 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. Ah, that actually makes sense. No is better than no default, but I think we should change it too. It would make scripts more verbose. I don't see a scenario in CI where I'd write this but want to say no. |
||
} | ||
|
||
string promptMessage = string.Format(CliCommandStrings.ToolDownloadConfirmationPrompt, packageId, version.ToString(), source.Source); | ||
|
||
static string AddPromptOptions(string message) | ||
{ | ||
return $"{message} [{CliCommandStrings.ConfirmationPromptYesValue}/{CliCommandStrings.ConfirmationPromptNoValue}] ({CliCommandStrings.ConfirmationPromptYesValue}): "; | ||
} | ||
|
||
Console.Write(AddPromptOptions(promptMessage)); | ||
|
||
static bool KeyMatches(ConsoleKeyInfo pressedKey, string valueKey) | ||
{ | ||
// Apparently you can't do invariant case insensitive comparison on a char directly, so we have to convert it to a string. | ||
// The resource string should be a single character, but we take the first character just to be sure. | ||
return pressedKey.KeyChar.ToString().ToLowerInvariant().Equals( | ||
valueKey.ToLowerInvariant().Substring(0, 1)); | ||
} | ||
|
||
while (true) | ||
{ | ||
var key = Console.ReadKey(); | ||
Console.WriteLine(); | ||
if (key.Key == ConsoleKey.Enter || KeyMatches(key, CliCommandStrings.ConfirmationPromptYesValue)) | ||
{ | ||
return true; | ||
} | ||
if (key.Key == ConsoleKey.Escape || KeyMatches(key, CliCommandStrings.ConfirmationPromptNoValue)) | ||
{ | ||
return false; | ||
} | ||
|
||
Console.Write(AddPromptOptions(string.Format(CliCommandStrings.ConfirmationPromptInvalidChoiceMessage, CliCommandStrings.ConfirmationPromptYesValue, CliCommandStrings.ConfirmationPromptNoValue))); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
// 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.Commands.Tool.Install; | ||
using NuGet.Packaging.Core; | ||
|
||
namespace Microsoft.DotNet.Cli.Commands.Tool.Execute; | ||
|
||
internal static class ToolExecuteCommandParser | ||
|
||
{ | ||
public static readonly Argument<PackageIdentity> PackageIdentityArgument = ToolInstallCommandParser.PackageIdentityArgument; | ||
|
||
public static readonly Argument<IEnumerable<string>> CommandArgument = new("commandArguments") | ||
{ | ||
Description = CliCommandStrings.ToolRunArgumentsDescription | ||
}; | ||
|
||
public static readonly Option<string> VersionOption = ToolInstallCommandParser.VersionOption; | ||
public static readonly Option<bool> RollForwardOption = ToolInstallCommandParser.RollForwardOption; | ||
public static readonly Option<bool> PrereleaseOption = ToolInstallCommandParser.PrereleaseOption; | ||
public static readonly Option<string> ConfigOption = ToolInstallCommandParser.ConfigOption; | ||
public static readonly Option<string[]> SourceOption = ToolInstallCommandParser.SourceOption; | ||
public static readonly Option<string[]> AddSourceOption = ToolInstallCommandParser.AddSourceOption; | ||
public static readonly Option<bool> InteractiveOption = CommonOptions.InteractiveOption(); | ||
public static readonly Option<bool> YesOption = CommonOptions.YesOption; | ||
public static readonly Option<VerbosityOptions> VerbosityOption = ToolInstallCommandParser.VerbosityOption; | ||
|
||
|
||
public static readonly Command Command = ConstructCommand(); | ||
public static Command GetCommand() | ||
{ | ||
return Command; | ||
} | ||
|
||
private static Command ConstructCommand() | ||
{ | ||
Command command = new("execute", CliCommandStrings.ToolExecuteCommandDescription); | ||
|
||
command.Aliases.Add("exec"); | ||
|
||
command.Arguments.Add(PackageIdentityArgument); | ||
command.Arguments.Add(CommandArgument); | ||
|
||
command.Options.Add(VersionOption); | ||
command.Options.Add(YesOption); | ||
command.Options.Add(InteractiveOption); | ||
command.Options.Add(RollForwardOption); | ||
command.Options.Add(PrereleaseOption); | ||
command.Options.Add(ConfigOption); | ||
command.Options.Add(SourceOption); | ||
command.Options.Add(AddSourceOption); | ||
command.Options.Add(ToolCommandRestorePassThroughOptions.DisableParallelOption); | ||
command.Options.Add(ToolCommandRestorePassThroughOptions.IgnoreFailedSourcesOption); | ||
command.Options.Add(ToolCommandRestorePassThroughOptions.NoCacheOption); | ||
command.Options.Add(ToolCommandRestorePassThroughOptions.NoHttpCacheOption); | ||
command.Options.Add(VerbosityOption); | ||
|
||
command.SetAction((parseResult) => new ToolExecuteCommand(parseResult).Execute()); | ||
|
||
return command; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't actually think this needs to be locked, I've updated the confirmation prompt to support localization better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't accept imply yes (whereas decline implies no)?