| File: Commands\create\TemplateCommand.cs | Web Access |
| Project: src\sdk\src\Cli\Microsoft.TemplateEngine.Cli\Microsoft.TemplateEngine.Cli.csproj (Microsoft.TemplateEngine.Cli) |
// 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.New; using Microsoft.DotNet.Cli.Utils; using Microsoft.TemplateEngine.Abstractions; using Microsoft.TemplateEngine.Abstractions.Constraints; using Microsoft.TemplateEngine.Abstractions.Installer; using Microsoft.TemplateEngine.Cli.PostActionProcessors; using Microsoft.TemplateEngine.Edge; using Microsoft.TemplateEngine.Edge.Settings; using Microsoft.TemplateEngine.Utils; using Command = System.CommandLine.Command; namespace Microsoft.TemplateEngine.Cli.Commands { internal class TemplateCommand : Command { private static readonly TimeSpan ConstraintEvaluationTimeout = TimeSpan.FromMilliseconds(1000); private static readonly string[] _helpAliases = ["-h", "/h", "--help", "-?", "/?"]; private readonly TemplatePackageManager _templatePackageManager; private readonly IEngineEnvironmentSettings _environmentSettings; private readonly Command _instantiateCommand; private readonly CliTemplateInfo _template; private Dictionary<string, TemplateOption> _templateSpecificOptions = new(); /// <summary> /// Create command for instantiation of specific template. /// </summary> /// <exception cref="InvalidTemplateParametersException">when <paramref name="template"/> has invalid template parameters.</exception> public TemplateCommand( Command instantiateCommand, IEngineEnvironmentSettings environmentSettings, TemplatePackageManager templatePackageManager, TemplateGroup templateGroup, CliTemplateInfo template, bool buildDefaultLanguageValidation = false) : base( templateGroup.ShortNames[0], template.Name + Environment.NewLine + template.Description) { _instantiateCommand = instantiateCommand; _environmentSettings = environmentSettings; _templatePackageManager = templatePackageManager; _template = template; foreach (var item in templateGroup.ShortNames.Skip(1)) { Aliases.Add(item); } OutputOption = SharedOptionsFactory.CreateOutputOption(); NameOption = SharedOptionsFactory.CreateNameOption(); DryRunOption = SharedOptionsFactory.CreateDryRunOption(); ForceOption = SharedOptionsFactory.CreateForceOption(); NoUpdateCheckOption = SharedOptionsFactory.CreateNoUpdateCheckOption(); Options.Add(OutputOption); Options.Add(NameOption); Options.Add(DryRunOption); Options.Add(ForceOption); Options.Add(NoUpdateCheckOption); string? templateLanguage = template.GetLanguage(); string? defaultLanguage = environmentSettings.GetDefaultLanguage(); if (!string.IsNullOrWhiteSpace(templateLanguage)) { LanguageOption = SharedOptionsFactory.CreateLanguageOption(); LanguageOption.Description = SymbolStrings.TemplateCommand_Option_Language; LanguageOption.FromAmongCaseInsensitive(new[] { templateLanguage }); if (!string.IsNullOrWhiteSpace(defaultLanguage) && buildDefaultLanguageValidation) { LanguageOption.DefaultValueFactory = (_) => defaultLanguage; LanguageOption.Validators.Add(optionResult => { var value = optionResult.GetValueOrDefault<string>(); if (value != template.GetLanguage()) { optionResult.AddError("Languages don't match"); } } ); } Options.Add(LanguageOption); } string? templateType = template.GetTemplateType(); if (!string.IsNullOrWhiteSpace(templateType)) { TypeOption = SharedOptionsFactory.CreateTypeOption(); TypeOption.Description = SymbolStrings.TemplateCommand_Option_Type; TypeOption.FromAmongCaseInsensitive(new[] { templateType }); Options.Add(TypeOption); } if (template.BaselineInfo.Any(b => !string.IsNullOrWhiteSpace(b.Key))) { BaselineOption = SharedOptionsFactory.CreateBaselineOption(); BaselineOption.Description = SymbolStrings.TemplateCommand_Option_Baseline; BaselineOption.FromAmongCaseInsensitive(template.BaselineInfo.Select(b => b.Key).Where(b => !string.IsNullOrWhiteSpace(b)).ToArray()); Options.Add(BaselineOption); } if (HasRunScriptPostActionDefined(template)) { AllowScriptsOption = new Option<AllowRunScripts>("--allow-scripts") { Description = SymbolStrings.TemplateCommand_Option_AllowScripts, Arity = new ArgumentArity(1, 1), DefaultValueFactory = (_) => AllowRunScripts.Prompt }; Options.Add(AllowScriptsOption); } AddTemplateOptionsToCommand(template); } internal static IReadOnlyList<string> KnownHelpAliases => _helpAliases; internal Option<FileInfo> OutputOption { get; } internal Option<string> NameOption { get; } internal Option<bool> DryRunOption { get; } internal Option<bool> ForceOption { get; } internal Option<bool> NoUpdateCheckOption { get; } internal Option<AllowRunScripts>? AllowScriptsOption { get; } internal Option<string>? LanguageOption { get; } internal Option<string>? TypeOption { get; } internal Option<string>? BaselineOption { get; } internal IReadOnlyDictionary<string, TemplateOption> TemplateOptions => _templateSpecificOptions; internal CliTemplateInfo Template => _template; internal static async Task<IReadOnlyList<TemplateConstraintResult>> ValidateConstraintsAsync(TemplateConstraintManager constraintManager, ITemplateInfo template, CancellationToken cancellationToken) { if (!template.Constraints.Any()) { return Array.Empty<TemplateConstraintResult>(); } IReadOnlyList<(ITemplateInfo Template, IReadOnlyList<TemplateConstraintResult> Result)> result = await constraintManager.EvaluateConstraintsAsync(new[] { template }, cancellationToken).ConfigureAwait(false); IReadOnlyList<TemplateConstraintResult> templateConstraints = result.Single().Result; if (templateConstraints.IsTemplateAllowed()) { return Array.Empty<TemplateConstraintResult>(); } return templateConstraints.Where(cr => cr.EvaluationStatus != TemplateConstraintResult.Status.Allowed).ToList(); } internal async Task<NewCommandStatus> InvokeAsync(ParseResult parseResult, CancellationToken cancellationToken) { using var templateInvocationActivity = Activities.Source.StartActivity("invoke-template"); TemplateCommandArgs args = new(this, _instantiateCommand, parseResult); TemplateInvoker invoker = new(_environmentSettings, () => Console.ReadLine() ?? string.Empty); TemplatePackageCoordinator packageCoordinator = new(_environmentSettings, _templatePackageManager); using TemplateConstraintManager constraintManager = new(_environmentSettings); TemplatePackageDisplay templatePackageDisplay = new(Reporter.Output, Reporter.Error); CancellationTokenSource cancellationTokenSource = new(); cancellationTokenSource.CancelAfter(ConstraintEvaluationTimeout); Task<IReadOnlyList<TemplateConstraintResult>> constraintsEvaluation = ValidateConstraintsAsync(constraintManager, args.Template, args.IsForceFlagSpecified ? cancellationTokenSource.Token : cancellationToken); if (!args.IsForceFlagSpecified) { using var constraintResultsActivity = Activities.Source.StartActivity("validate-constraints"); var constraintResults = await constraintsEvaluation.ConfigureAwait(false); if (constraintResults.Any()) { DisplayConstraintResults(constraintResults, args); return NewCommandStatus.CreateFailed; } } cancellationToken.ThrowIfCancellationRequested(); Task<NewCommandStatus> instantiateTask = invoker.InvokeTemplateAsync(args, cancellationToken); Task<(string Id, string Version, string Provider)> builtInPackageCheck = packageCoordinator.ValidateBuiltInPackageAvailabilityAsync(args.Template, cancellationToken); Task<CheckUpdateResult?> checkForUpdateTask = packageCoordinator.CheckUpdateForTemplate(args, cancellationToken); Task[] tasksToWait = [instantiateTask, builtInPackageCheck, checkForUpdateTask]; await Task.WhenAll(tasksToWait).ConfigureAwait(false); Reporter.Output.WriteLine(); cancellationToken.ThrowIfCancellationRequested(); if (checkForUpdateTask.Result != null) { // print if there is update for the template package containing the template templatePackageDisplay.DisplayUpdateCheckResult(checkForUpdateTask.Result, args); } if (builtInPackageCheck.Result != default) { // print if there is same or newer built-in package templatePackageDisplay.DisplayBuiltInPackagesCheckResult( builtInPackageCheck.Result.Id, builtInPackageCheck.Result.Version, builtInPackageCheck.Result.Provider, args); } if (args.IsForceFlagSpecified) { // print warning about the constraints that were not met. try { IReadOnlyList<TemplateConstraintResult> constraintResults = await constraintsEvaluation.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false); if (constraintResults.Any()) { DisplayConstraintResults(constraintResults, args); } } catch (TaskCanceledException) { // do nothing } } return instantiateTask.Result; } private void DisplayConstraintResults(IReadOnlyList<TemplateConstraintResult> constraintResults, TemplateCommandArgs templateArgs) { var reporter = templateArgs.IsForceFlagSpecified ? Reporter.Output : Reporter.Error; if (templateArgs.IsForceFlagSpecified) { reporter.WriteLine(LocalizableStrings.TemplateCommand_DisplayConstraintResults_Warning, templateArgs.Template.Name); } else { reporter.WriteLine(LocalizableStrings.TemplateCommand_DisplayConstraintResults_Error, templateArgs.Template.Name); } foreach (var constraint in constraintResults.Where(cr => cr.EvaluationStatus != TemplateConstraintResult.Status.Allowed)) { reporter.WriteLine(constraint.ToDisplayString().Indent()); } reporter.WriteLine(); if (!templateArgs.IsForceFlagSpecified) { reporter.WriteLine(LocalizableStrings.TemplateCommand_DisplayConstraintResults_Hint, ForceOption.Name); reporter.WriteCommand(Example.FromExistingTokens<TemplateCommand>(templateArgs.ParseResult).WithOption(c => c.ForceOption)); } else { reporter.WriteLine(LocalizableStrings.TemplateCommand_DisplayConstraintResults_Hint_TemplateNotUsable); } } private bool HasRunScriptPostActionDefined(CliTemplateInfo template) { return template.PostActions.Contains(ProcessStartPostActionProcessor.ActionProcessorId); } private HashSet<string> GetReservedAliases() { HashSet<string> reservedAliases = new(); AddReservedNamesAndAliases(reservedAliases, this); //add options of parent? - this covers debug: options AddReservedNamesAndAliases(reservedAliases, _instantiateCommand); //add restricted aliases: language, type, baseline (they may be optional) foreach (var option in new[] { SharedOptionsFactory.CreateLanguageOption(), SharedOptionsFactory.CreateTypeOption(), SharedOptionsFactory.CreateBaselineOption() }) { reservedAliases.Add(option.Name); foreach (string alias in option.Aliases) { reservedAliases.Add(alias); } } foreach (string helpAlias in KnownHelpAliases) { reservedAliases.Add(helpAlias); } return reservedAliases; static void AddReservedNamesAndAliases(HashSet<string> reservedAliases, Command command) { foreach (Option option in command.Options) { reservedAliases.Add(option.Name); foreach (string alias in option.Aliases) { reservedAliases.Add(alias); } } foreach (Command subCommand in command.Subcommands) { reservedAliases.Add(subCommand.Name); foreach (string alias in subCommand.Aliases) { reservedAliases.Add(alias); } } } } private void AddTemplateOptionsToCommand(CliTemplateInfo templateInfo) { HashSet<string> initiallyTakenAliases = GetReservedAliases(); var parametersWithAliasAssignments = AliasAssignmentCoordinator.AssignAliasesForParameter(templateInfo.CliParameters.Values, initiallyTakenAliases); if (parametersWithAliasAssignments.Any(p => p.Errors.Any())) { IReadOnlyDictionary<CliTemplateParameter, IReadOnlyList<string>> errors = parametersWithAliasAssignments .Where(p => p.Errors.Any()) .ToDictionary(p => p.Parameter, p => p.Errors); throw new InvalidTemplateParametersException(templateInfo, errors); } foreach ((CliTemplateParameter parameter, IReadOnlySet<string> aliases, IReadOnlyList<string> _) in parametersWithAliasAssignments) { TemplateOption option = new(parameter, aliases); Options.Add(option.Option); _templateSpecificOptions[parameter.Name] = option; } } } }