Skip to content

Add workload restore command #18910

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

Merged
merged 1 commit into from
Jul 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
46 changes: 46 additions & 0 deletions src/Cli/dotnet/CommonOptionsExtension.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using Microsoft.DotNet.Tools;
using System.CommandLine;
using System.IO;
using Microsoft.Build.Framework;
using Microsoft.DotNet.Tools.Common;

namespace Microsoft.DotNet.Cli
{
internal static class CommonOptionsExtension
{
public static LoggerVerbosity ToLoggerVerbosity(this VerbosityOptions verbosityOptions)
{
LoggerVerbosity verbosity = Build.Framework.LoggerVerbosity.Normal;
switch (verbosityOptions)
{
case VerbosityOptions.d:
case VerbosityOptions.detailed:
verbosity = Build.Framework.LoggerVerbosity.Detailed;
break;
case VerbosityOptions.diag:
case VerbosityOptions.diagnostic:
verbosity = Build.Framework.LoggerVerbosity.Diagnostic;
break;
case VerbosityOptions.m:
case VerbosityOptions.minimal:
verbosity = Build.Framework.LoggerVerbosity.Minimal;
break;
case VerbosityOptions.n:
case VerbosityOptions.normal:
verbosity = Build.Framework.LoggerVerbosity.Normal;
break;
case VerbosityOptions.q:
case VerbosityOptions.quiet:
verbosity = Build.Framework.LoggerVerbosity.Quiet;
break;
}


return verbosity;
}
}
}
17 changes: 3 additions & 14 deletions src/Cli/dotnet/Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,19 +72,18 @@ public static class Parser
// Argument
public static readonly Argument<string> DotnetSubCommand = new Argument<string>() { Arity = ArgumentArity.ExactlyOne, IsHidden = true };

private static Command ConfigureCommandLine(Command rootCommand, bool includeWorkloadCommands = false)
private static Command ConfigureCommandLine(Command rootCommand)
{
// Add subcommands
foreach (var subcommand in Subcommands)
{
rootCommand.AddCommand(subcommand);
}

// Workload command is behind a feature flag during development
rootCommand.AddCommand(WorkloadCommandParser.GetCommand(includeWorkloadCommands || Env.GetEnvironmentVariableAsBool("DEVENABLEWORKLOADCOMMAND", defaultValue: false)));
rootCommand.AddCommand(WorkloadCommandParser.GetCommand());

//Add internal commands
rootCommand.AddCommand(InstallSuccessCommand);
rootCommand.AddCommand(InstallSuccessCommand);

// Add options
rootCommand.AddOption(DiagOption);
Expand Down Expand Up @@ -115,16 +114,6 @@ private static CommandLineBuilder DisablePosixBinding(this CommandLineBuilder bu
.DisablePosixBinding()
.Build();

public static System.CommandLine.Parsing.Parser GetWorkloadsInstance { get; } = new CommandLineBuilder(ConfigureCommandLine(new RootCommand(), true))
.UseExceptionHandler(ExceptionHandler)
.UseHelp()
.UseHelpBuilder(context => new DotnetHelpBuilder(context.Console))
.UseResources(new CommandLineValidationMessages())
.UseParseDirective()
.UseSuggestDirective()
.DisablePosixBinding()
.Build();

private static void ExceptionHandler(Exception exception, InvocationContext context)
{
if (exception is TargetInvocationException)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace Microsoft.DotNet.Cli
{
internal static class WorkloadCommandParser
{
public static Command GetCommand(bool includeAllCommands)
public static Command GetCommand()
{
var command = new Command("workload", LocalizableStrings.CommandDescription);

Expand All @@ -18,11 +18,7 @@ public static Command GetCommand(bool includeAllCommands)
command.AddCommand(WorkloadSearchCommandParser.GetCommand());
command.AddCommand(WorkloadUninstallCommandParser.GetCommand());
command.AddCommand(WorkloadRepairCommandParser.GetCommand());
if (includeAllCommands)
{
command.AddCommand(WorkloadRestoreCommandParser.GetCommand());
}

command.AddCommand(WorkloadRestoreCommandParser.GetCommand());
command.AddCommand(WorkloadElevateCommandParser.GetCommand());

return command;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ public WorkloadInstallCommand(
string dotnetDir = null,
string userHome = null,
string tempDirPath = null,
string version = null)
string version = null,
IReadOnlyCollection<string> workloadIds = null)
: base(parseResult)
{
_reporter = reporter ?? Reporter.Output;
Expand All @@ -66,7 +67,7 @@ public WorkloadInstallCommand(
_printDownloadLinkOnly = parseResult.ValueForOption<bool>(WorkloadInstallCommandParser.PrintDownloadLinkOnlyOption);
_fromCacheOption = parseResult.ValueForOption<string>(WorkloadInstallCommandParser.FromCacheOption);
_downloadToCacheOption = parseResult.ValueForOption<string>(WorkloadInstallCommandParser.DownloadToCacheOption);
_workloadIds = parseResult.ValueForArgument<IEnumerable<string>>(WorkloadInstallCommandParser.WorkloadIdArgument).ToList().AsReadOnly();
_workloadIds = workloadIds ?? parseResult.ValueForArgument<IEnumerable<string>>(WorkloadInstallCommandParser.WorkloadIdArgument).ToList().AsReadOnly();
_verbosity = parseResult.ValueForOption<VerbosityOptions>(WorkloadInstallCommandParser.VerbosityOption);
_dotnetPath = dotnetDir ?? Path.GetDirectoryName(Environment.ProcessPath);
_sdkVersion = WorkloadOptionsExtensions.GetValidatedSdkVersion(parseResult.ValueForOption<string>(WorkloadInstallCommandParser.VersionOption), version, _dotnetPath);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ public static Command GetCommand()
var command = new Command("install", LocalizableStrings.CommandDescription);

command.AddArgument(WorkloadIdArgument);
AddWorkloadInstallCommandOptions(command);

return command;
}

internal static void AddWorkloadInstallCommandOptions(Command command)
{
command.AddOption(VersionOption);
command.AddOption(ConfigOption);
command.AddOption(SourceOption);
Expand All @@ -68,8 +75,6 @@ public static Command GetCommand()
command.AddOption(TempDirOption);
command.AddWorkloadCommandNuGetRestoreActionConfigOptions();
command.AddOption(VerbosityOption);

return command;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,49 +120,13 @@
<data name="CommandDescription" xml:space="preserve">
<value>Restore workloads required for the project.</value>
</data>
<data name="InvalidPackageWarning" xml:space="preserve">
<value>Warning: workload package '{0}' is invalid</value>
<data name="InstallingWorkloads" xml:space="preserve">
<value>Installing workloads: {0}</value>
</data>
<data name="PackageIdColumn" xml:space="preserve">
<value>Package Id</value>
<data name="CouldNotFindAProject" xml:space="preserve">
<value>Couldn't find a project. Ensure a project exists in {0}, or pass the path to the project using {1}.</value>
</data>
<data name="VersionColumn" xml:space="preserve">
<value>Version</value>
</data>
<data name="CommandsColumn" xml:space="preserve">
<value>Commands</value>
</data>
<data name="AddSourceOptionDescription" xml:space="preserve">
<value>Add an additional NuGet package source to use during installation.</value>
</data>
<data name="AddSourceOptionName" xml:space="preserve">
<value>SOURCE</value>
</data>
<data name="ConfigFileOptionDescription" xml:space="preserve">
<value>The NuGet configuration file to use.</value>
</data>
<data name="ConfigFileOptionName" xml:space="preserve">
<value>FILE</value>
</data>
<data name="VersionOptionDescription" xml:space="preserve">
<value>The version of the workload package to install.</value>
</data>
<data name="VersionOptionName" xml:space="preserve">
<value>VERSION</value>
</data>
<data name="PackageFailedToRestore" xml:space="preserve">
<value>Package "{0}" failed to restore, due to {1}</value>
</data>
<data name="RestoreSuccessful" xml:space="preserve">
<value>Workload '{0}' (version '{1}') was restored. Available commands: {2}</value>
</data>
<data name="RestorePartiallyFailed" xml:space="preserve">
<value>Restore partially failed.</value>
</data>
<data name="RestoreFailed" xml:space="preserve">
<value>Restore failed.</value>
</data>
<data name="NoWorkloadsWereRestored" xml:space="preserve">
<value>No workloads were restored.</value>
<data name="FailedToRunTarget" xml:space="preserve">
<value>Failed to restore workload for project {0}: Failed to run MSBuild Target _GetRequiredWorkloads.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,131 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.CommandLine.Parsing;
using System.IO;
using System.Linq;
using Microsoft.Build.Construction;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Microsoft.Build.Logging;
using Microsoft.DotNet.Cli;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Workloads.Workload.Install;
using Microsoft.Extensions.EnvironmentAbstractions;
using Microsoft.NET.Sdk.WorkloadManifestReader;

namespace Microsoft.DotNet.Workloads.Workload.Restore
{
internal class WorkloadRestoreCommand : CommandBase
{
private readonly string _configFilePath;
private readonly IReporter _errorReporter;
private readonly IFileSystem _fileSystem;
private readonly ParseResult _result;
private readonly IReporter _reporter;
private readonly string[] _sources;
private readonly string _verbosity;
private readonly IEnumerable<string> _slnOrProjectArgument;

public WorkloadRestoreCommand(
ParseResult result,
IFileSystem fileSystem = null,
IReporter reporter = null)
: base(result)
{
_fileSystem = fileSystem ?? new FileSystemWrapper();

_result = result;
_reporter = reporter ?? Reporter.Output;
_errorReporter = reporter ?? Reporter.Error;

_configFilePath = result.ValueForOption<string>(WorkloadRestoreCommandParser.ConfigOption);
_sources = result.ValueForOption<string[]>(WorkloadRestoreCommandParser.SourceOption);
_verbosity =
Enum.GetName(result.ValueForOption<VerbosityOptions>(WorkloadRestoreCommandParser.VerbosityOption));
_slnOrProjectArgument =
result.ValueForArgument<IEnumerable<string>>(RestoreCommandParser.SlnOrProjectArgument);
}

public override int Execute()
{
_reporter.WriteLine("WIP workload restore stub");
var allProjects = DiscoverAllProjects(Directory.GetCurrentDirectory(), _slnOrProjectArgument).Distinct();
List<WorkloadId> allWorkloadId = RunTargetToGetWorkloadIds(allProjects);
_reporter.WriteLine(string.Format(LocalizableStrings.InstallingWorkloads, string.Join(" ", allWorkloadId)));

var workloadInstallCommand = new WorkloadInstallCommand(_result,
workloadIds: allWorkloadId.Select(a => a.ToString()).ToList().AsReadOnly());

workloadInstallCommand.Execute();
return 0;
}

private List<WorkloadId> RunTargetToGetWorkloadIds(IEnumerable<string> allProjects)
{
var globalProperties = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{"SkipResolvePackageAssets", "true"}
};

var allWorkloadId = new List<WorkloadId>();
foreach (string projectFile in allProjects)
{
var project = new ProjectInstance(projectFile, globalProperties, null);

bool buildResult = project.Build(new[] {"_GetRequiredWorkloads"},
loggers: new ILogger[]
{
new ConsoleLogger(_result
.ValueForOption(WorkloadInstallCommandParser.VerbosityOption)
.ToLoggerVerbosity())
},
remoteLoggers: Enumerable.Empty<ForwardingLoggerRecord>(),
targetOutputs: out var targetOutputs);

if (buildResult == false)
{
throw new GracefulException(
string.Format(
LocalizableStrings.FailedToRunTarget,
projectFile));
}

var targetResult = targetOutputs["_GetRequiredWorkloads"];
allWorkloadId.AddRange(targetResult.Items.Select(item => new WorkloadId(item.ItemSpec)));
}

allWorkloadId = allWorkloadId.Distinct().ToList();
return allWorkloadId;
}


internal static List<string> DiscoverAllProjects(string currentDirectory,
IEnumerable<string> slnOrProjectArgument = null)
Comment on lines +92 to +93
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should discover all projects here. We should do the same thing as MSBuild, which is more or less to expect that there is a single project or solution file in the directory, and error out if not. The actual logic is in ProcessProjectSwitch: https://github.com/dotnet/msbuild/blob/1d845f30213e9ba4f36d4d5a366c0cc8285eed6e/src/MSBuild/XMake.cs#L2763

Ideally we wouldn't have to duplicate the logic. It looks like RunCommand has a simple form of it (FindSingleProjectInDirectory) which only looks for project files, not solution files.

Copy link
Author

@wli3 wli3 Jul 15, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about the same thing, but why make it difficult. I started with refactoring RunCommand. But when there are 2 projects in the folder what the user should do? It is valid, and workload restore can handle it (unlike runcommand), why fail and ask the user to input one by one?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All other MSBuild and dotnet commands require you to specify the project if there is more than one in the folder. It seems weird to make dotnet workload restore the one exception.

{
var slnFiles = new List<string>();
var projectFiles = new List<string>();
if (slnOrProjectArgument == null || !slnOrProjectArgument.Any())
{
slnFiles = Directory.GetFiles(currentDirectory, "*.sln").ToList();
projectFiles.AddRange(Directory.GetFiles(currentDirectory, "*.*proj"));
}
else
{
slnFiles = slnOrProjectArgument
.Where(s => Path.GetExtension(s).Equals(".sln", StringComparison.OrdinalIgnoreCase))
.Select(Path.GetFullPath).ToList();
projectFiles = slnOrProjectArgument
.Where(s => Path.GetExtension(s).EndsWith("proj", StringComparison.OrdinalIgnoreCase))
.Select(Path.GetFullPath).ToList();
}

foreach (string file in slnFiles)
{
var solutionFile = SolutionFile.Parse(file);
var projects = solutionFile.ProjectsInOrder;
foreach (var p in projects)
{
projectFiles.Add(p.AbsolutePath);
}
}

if (projectFiles.Count == 0)
{
throw new GracefulException(
LocalizableStrings.CouldNotFindAProject,
currentDirectory, "--project");
}

return projectFiles;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,12 @@ namespace Microsoft.DotNet.Cli
{
internal static class WorkloadRestoreCommandParser
{
public static readonly Option<string> ConfigOption = WorkloadInstallCommandParser.ConfigOption;

public static readonly Option<string[]> SourceOption = WorkloadInstallCommandParser.SourceOption;

public static readonly Option<VerbosityOptions> VerbosityOption = WorkloadInstallCommandParser.VerbosityOption;

public static Command GetCommand()
{
Command command = new Command("restore", LocalizableStrings.CommandDescription);

command.AddOption(ConfigOption);
command.AddOption(SourceOption);
command.AddWorkloadCommandNuGetRestoreActionConfigOptions();
command.AddOption(VerbosityOption);

command.AddArgument(RestoreCommandParser.SlnOrProjectArgument);
WorkloadInstallCommandParser.AddWorkloadInstallCommandOptions(command);
return command;
}
}
Expand Down
Loading