winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit e9521d35c254d50ebb15b69841960c74a2f87a6c
parent 0073a6e184ca4dd00facfe53236237627707e26a
Author: Ruben Guerrero <rubengu@microsoft.com>
Date:   Thu, 17 Aug 2023 18:00:09 -0700

PowerShellGet  (#3521)

Creates new IPowerShellGet and provide a 2.2.5 implementation. This will make it easier for us to move to v3 when released.
Use Import-CliXml instead of Get-InstalledModule. We will now get the information we need for modules that were installed via Install-Module and SaveModule.
Diffstat:
Msrc/Microsoft.Management.Configuration.Processor/Constants/PowerShellConstants.cs | 1+
Asrc/Microsoft.Management.Configuration.Processor/Helpers/IPowerShellGet.cs | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/Microsoft.Management.Configuration.Processor/Helpers/PowerShellGetV2.cs | 229+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/Microsoft.Management.Configuration.Processor/ProcessorEnvironments/HostedEnvironment.cs | 219++++++++++++++++++-------------------------------------------------------------
Msrc/Microsoft.Management.Configuration.Processor/ProcessorEnvironments/IProcessorEnvironment.cs | 7+++++++
5 files changed, 377 insertions(+), 170 deletions(-)

diff --git a/src/Microsoft.Management.Configuration.Processor/Constants/PowerShellConstants.cs b/src/Microsoft.Management.Configuration.Processor/Constants/PowerShellConstants.cs @@ -44,6 +44,7 @@ namespace Microsoft.Management.Configuration.Processor.Constants public const string InvokeDscResource = "Invoke-DscResource"; public const string SaveModule = "Save-Module"; public const string FindModule = "Find-Module"; + public const string ImportCliXml = "Import-CliXml"; } internal static class Parameters diff --git a/src/Microsoft.Management.Configuration.Processor/Helpers/IPowerShellGet.cs b/src/Microsoft.Management.Configuration.Processor/Helpers/IPowerShellGet.cs @@ -0,0 +1,91 @@ +// ----------------------------------------------------------------------------- +// <copyright file="IPowerShellGet.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.Helpers +{ + using System.Management.Automation; + using Microsoft.PowerShell.Commands; + + /// <summary> + /// Interface for PowerShellGet cmdlets. + /// </summary> + internal interface IPowerShellGet + { + /// <summary> + /// Calls Find-Module. + /// </summary> + /// <param name="pwsh">PowerShell instance.</param> + /// <param name="moduleName">Module name.</param> + /// <param name="semanticVersion">Optional version.</param> + /// <param name="semanticMinVersion">Optional min version.</param> + /// <param name="semanticMaxVersion">Optional max version.</param> + /// <param name="repository">Optional repository.</param> + /// <param name="allowPrerelease">Optional allow prerelease module.</param> + /// <returns>Module info, null if not found.</returns> + PSObject? FindModule( + PowerShell pwsh, + string moduleName, + SemanticVersion? semanticVersion, + SemanticVersion? semanticMinVersion, + SemanticVersion? semanticMaxVersion, + string? repository, + bool? allowPrerelease); + + /// <summary> + /// Calls Find-DscResource. + /// </summary> + /// <param name="pwsh">PowerShell instance.</param> + /// <param name="resourceName">resource name.</param> + /// <param name="moduleName">Optional module name.</param> + /// <param name="semanticVersion">Optional version.</param> + /// <param name="semanticMinVersion">Optional min version.</param> + /// <param name="semanticMaxVersion">Optional max version.</param> + /// <param name="repository">Optional repository.</param> + /// <param name="allowPrerelease">Optional allow prerelease module.</param> + /// <returns>Dsc Resource info, null if not found.</returns> + PSObject? FindDscResource( + PowerShell pwsh, + string resourceName, + string? moduleName, + SemanticVersion? semanticVersion, + SemanticVersion? semanticMinVersion, + SemanticVersion? semanticMaxVersion, + string? repository, + bool? allowPrerelease); + + /// <summary> + /// Calls Save-Module with module specification. + /// </summary> + /// <param name="pwsh">PowerShell instance.</param> + /// <param name="moduleSpecification">Module specification.</param> + /// <param name="location">Location to save module.</param> + void SaveModule(PowerShell pwsh, ModuleSpecification moduleSpecification, string location); + + /// <summary> + /// Calls Save-Module -InputObject object -Path location. + /// Input object must be the result of Find cmdlets of PowerShellGet. + /// </summary> + /// <param name="pwsh">PowerShell instance.</param> + /// <param name="inputObject">Input object.</param> + /// <param name="location">Location to save module.</param> + void SaveModule(PowerShell pwsh, PSObject inputObject, string location); + + /// <summary> + /// Calls Install-Module -InputObject object. + /// Input object must be the result of Find cmdlets of PowerShellGet. + /// </summary> + /// <param name="pwsh">PowerShell instance.</param> + /// <param name="inputObject">Input object.</param> + void InstallModule(PowerShell pwsh, PSObject inputObject); + + /// <summary> + /// Calls Install-Module with a module specification. + /// </summary> + /// <param name="pwsh">PowerShell instance.</param> + /// <param name="moduleSpecification">Module specification.</param> + void InstallModule(PowerShell pwsh, ModuleSpecification moduleSpecification); + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Helpers/PowerShellGetV2.cs b/src/Microsoft.Management.Configuration.Processor/Helpers/PowerShellGetV2.cs @@ -0,0 +1,229 @@ +// ----------------------------------------------------------------------------- +// <copyright file="PowerShellGetV2.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.Helpers +{ + using System.Collections.Generic; + using System.Linq; + using System.Management.Automation; + using Microsoft.PowerShell.Commands; + using static Microsoft.Management.Configuration.Processor.Constants.PowerShellConstants; + + /// <summary> + /// PowerShellGet implementation for 2.2.5 . + /// </summary> + internal class PowerShellGetV2 : IPowerShellGet + { + /// <inheritdoc/> + public PSObject? FindModule( + PowerShell pwsh, + string moduleName, + SemanticVersion? semanticVersion, + SemanticVersion? semanticMinVersion, + SemanticVersion? semanticMaxVersion, + string? repository, + bool? allowPrerelease) + { + bool implicitAllowPrerelease = false; + + var parameters = new Dictionary<string, object>() + { + { Parameters.Name, moduleName }, + }; + + if (semanticVersion != null) + { + implicitAllowPrerelease |= semanticVersion.IsPrerelease; + parameters.Add(Parameters.RequiredVersion, semanticVersion.ToString()); + } + + if (semanticMinVersion != null) + { + implicitAllowPrerelease |= semanticMinVersion.IsPrerelease; + parameters.Add(Parameters.MinimumVersion, semanticMinVersion.ToString()); + } + + if (semanticMaxVersion != null) + { + implicitAllowPrerelease |= semanticMaxVersion.IsPrerelease; + parameters.Add(Parameters.MaximumVersion, semanticMaxVersion.ToString()); + } + + if (!string.IsNullOrEmpty(repository)) + { + parameters.Add(Parameters.Repository, repository); + } + + if (allowPrerelease.HasValue || implicitAllowPrerelease) + { + // If explicit allowPrerelease = false don't use implicit. + bool allow = allowPrerelease ?? implicitAllowPrerelease; + parameters.Add(Parameters.AllowPrerelease, allow); + } + + pwsh.AddCommand(Commands.FindModule) + .AddParameters(parameters); + + return pwsh.Invoke().FirstOrDefault(); + } + + /// <inheritdoc/> + public PSObject? FindDscResource( + PowerShell pwsh, + string resourceName, + string? moduleName, + SemanticVersion? semanticVersion, + SemanticVersion? semanticMinVersion, + SemanticVersion? semanticMaxVersion, + string? repository, + bool? allowPrerelease) + { + var parameters = new Dictionary<string, object>() + { + { Parameters.Name, resourceName }, + }; + + bool implicitAllowPrerelease = false; + + if (!string.IsNullOrEmpty(moduleName)) + { + parameters.Add(Parameters.ModuleName, moduleName); + } + + if (semanticVersion != null) + { + implicitAllowPrerelease |= semanticVersion.IsPrerelease; + parameters.Add(Parameters.RequiredVersion, semanticVersion.ToString()); + } + + if (semanticMinVersion != null) + { + implicitAllowPrerelease |= semanticMinVersion.IsPrerelease; + parameters.Add(Parameters.MinimumVersion, semanticMinVersion.ToString()); + } + + if (semanticMaxVersion != null) + { + implicitAllowPrerelease |= semanticMaxVersion.IsPrerelease; + parameters.Add(Parameters.MaximumVersion, semanticMaxVersion.ToString()); + } + + if (!string.IsNullOrEmpty(repository)) + { + parameters.Add(Parameters.Repository, repository); + } + + if (allowPrerelease.HasValue || implicitAllowPrerelease) + { + // If explicit allowPrerelease = false don't use implicit. + bool allow = allowPrerelease ?? implicitAllowPrerelease; + parameters.Add(Parameters.AllowPrerelease, allow); + } + + pwsh.AddCommand(Commands.FindDscResource) + .AddParameters(parameters); + + // The result is just a PSCustomObject with a type name of Microsoft.PowerShell.Commands.PSGetDscResourceInfo. + // When no module is passed and a resource is not found, this will return an empty list. If a module + // is specified and no resource is found then it will fail earlier because of a Write-Error. + return pwsh.Invoke().FirstOrDefault(); + } + + /// <inheritdoc/> + public void SaveModule( + PowerShell pwsh, + ModuleSpecification moduleSpecification, + string location) + { + var parameters = new Dictionary<string, object>() + { + { Parameters.Name, moduleSpecification.Name }, + { Parameters.Path, location }, + }; + + if (moduleSpecification.Version is not null) + { + parameters.Add(Parameters.MinimumVersion, moduleSpecification.Version); + } + + if (moduleSpecification.MaximumVersion is not null) + { + parameters.Add(Parameters.MaximumVersion, moduleSpecification.MaximumVersion); + } + + if (moduleSpecification.RequiredVersion is not null) + { + parameters.Add(Parameters.RequiredVersion, moduleSpecification.RequiredVersion); + } + + _ = pwsh.AddCommand(Commands.SaveModule) + .AddParameters(parameters) + .AddParameter(Parameters.Force) + .Invoke(); + } + + /// <inheritdoc/> + public void SaveModule( + PowerShell pwsh, + PSObject inputObject, + string location) + { + _ = pwsh.AddCommand(Commands.SaveModule) + .AddParameter(Parameters.Path, location) + .AddParameter(Parameters.InputObject, inputObject) + .AddParameter(Parameters.Force) + .Invoke(); + } + + /// <inheritdoc/> + public void InstallModule( + PowerShell pwsh, + PSObject inputObject) + { + // If the repository is untrusted, it will fail with: + // Microsoft.PowerShell.Commands.WriteErrorException : Exception calling "ShouldContinue" with "5" + // argument(s): "A command that prompts the user failed because the host program or the command type + // does not support user interaction. + // If its trusted, PowerShellGets adds the Force parameter to the call to PackageManager\Install-Package. + // TODO: Once we have policies, we should remove Force. For hosted environments and depending + // on the policy we will trust PSGallery when we create the Runspace or add Force here. + _ = pwsh.AddCommand(Commands.InstallModule) + .AddParameter(Parameters.InputObject, inputObject) + .AddParameter(Parameters.Force) + .Invoke(); + } + + /// <inheritdoc/> + public void InstallModule( + PowerShell pwsh, + ModuleSpecification moduleSpecification) + { + var parameters = new Dictionary<string, object>() + { + { Parameters.Name, moduleSpecification.Name }, + }; + if (moduleSpecification.Version is not null) + { + parameters.Add(Parameters.MinimumVersion, moduleSpecification.Version); + } + + if (moduleSpecification.MaximumVersion is not null) + { + parameters.Add(Parameters.MaximumVersion, moduleSpecification.MaximumVersion); + } + + if (moduleSpecification.RequiredVersion is not null) + { + parameters.Add(Parameters.RequiredVersion, moduleSpecification.RequiredVersion); + } + + _ = pwsh.AddCommand(Commands.InstallModule) + .AddParameters(parameters) + .AddParameter(Parameters.Force) + .Invoke(); + } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/ProcessorEnvironments/HostedEnvironment.cs b/src/Microsoft.Management.Configuration.Processor/ProcessorEnvironments/HostedEnvironment.cs @@ -31,6 +31,7 @@ namespace Microsoft.Management.Configuration.Processor.Runspaces internal class HostedEnvironment : IProcessorEnvironment { private readonly PowerShellConfigurationProcessorType type; + private readonly IPowerShellGet powerShellGet; /// <summary> /// Initializes a new instance of the <see cref="HostedEnvironment"/> class. @@ -38,11 +39,17 @@ namespace Microsoft.Management.Configuration.Processor.Runspaces /// <param name="runspace">PowerShell Runspace.</param> /// <param name="type">Configuration processor type.</param> /// <param name="dscModule">IDscModule.</param> - public HostedEnvironment(Runspace runspace, PowerShellConfigurationProcessorType type, IDscModule dscModule) + public HostedEnvironment( + Runspace runspace, + PowerShellConfigurationProcessorType type, + IDscModule dscModule) { this.Runspace = runspace; this.type = type; this.DscModule = dscModule; + + // TODO: once v3 is release implement v3 version. + this.powerShellGet = new PowerShellGetV2(); } /// <inheritdoc/> @@ -229,35 +236,29 @@ namespace Microsoft.Management.Configuration.Processor.Runspaces /// <inheritdoc/> public PSObject? GetInstalledModule(ModuleSpecification moduleSpecification) { - var parameters = new Dictionary<string, object>() - { - { Parameters.Name, moduleSpecification.Name }, - }; - - if (moduleSpecification.Version is not null) - { - parameters.Add(Parameters.MinimumVersion, moduleSpecification.Version); - } - - if (moduleSpecification.MaximumVersion is not null) + // Instead of Get-InstalledModule, we look for PSGetModuleInfo.xml and serialize it + // if found. This allow us to get the information from Install-Module and Save-Module. + var module = this.GetAvailableModule(moduleSpecification); + if (module is null) { - parameters.Add(Parameters.MaximumVersion, moduleSpecification.MaximumVersion); + return null; } - if (moduleSpecification.RequiredVersion is not null) + var getModuleInfoFile = Path.Combine(module.ModuleBase, "PSGetModuleInfo.xml"); + if (!File.Exists(getModuleInfoFile)) { - parameters.Add(Parameters.RequiredVersion, moduleSpecification.RequiredVersion); + // Keep Get-InstalledModule behaviour. + return null; } using PowerShell pwsh = PowerShell.Create(this.Runspace); - - var result = pwsh.AddCommand(Commands.GetInstalledModule) - .AddParameters(parameters) - .Invoke() - .FirstOrDefault(); + var installedModule = pwsh.AddCommand(Commands.ImportCliXml) + .AddParameter(Parameters.Path, getModuleInfoFile) + .Invoke() + .FirstOrDefault(); this.OnDiagnostics(DiagnosticLevel.Verbose, pwsh); - return result; + return installedModule; } /// <inheritdoc/> @@ -272,56 +273,16 @@ namespace Microsoft.Management.Configuration.Processor.Runspaces return null; } - var semanticVersion = unitInternal.GetSemanticVersion(); - var semanticMinVersion = unitInternal.GetSemanticMinVersion(); - var semanticMaxVersion = unitInternal.GetSemanticMaxVersion(); - string? repository = unitInternal.GetDirective<string>(DirectiveConstants.Repository); - - bool? allowPrerelease = unitInternal.GetDirective(DirectiveConstants.AllowPrerelease); - bool implicitAllowPrerelease = false; - - var parameters = new Dictionary<string, object>() - { - { Parameters.Name, moduleName }, - }; - - if (semanticVersion != null) - { - implicitAllowPrerelease |= semanticVersion.IsPrerelease; - parameters.Add(Parameters.RequiredVersion, semanticVersion.ToString()); - } - - if (semanticMinVersion != null) - { - implicitAllowPrerelease |= semanticMinVersion.IsPrerelease; - parameters.Add(Parameters.MinimumVersion, semanticMinVersion.ToString()); - } - - if (semanticMaxVersion != null) - { - implicitAllowPrerelease |= semanticMaxVersion.IsPrerelease; - parameters.Add(Parameters.MaximumVersion, semanticMaxVersion.ToString()); - } - - if (!string.IsNullOrEmpty(repository)) - { - parameters.Add(Parameters.Repository, repository); - } - - if (allowPrerelease.HasValue || implicitAllowPrerelease) - { - // If explicit allowPrerelease = false don't use implicit. - bool allow = allowPrerelease.HasValue ? allowPrerelease.Value : implicitAllowPrerelease; - parameters.Add(Parameters.AllowPrerelease, allow); - } - using PowerShell pwsh = PowerShell.Create(this.Runspace); - pwsh.AddCommand(Commands.FindModule) - .AddParameters(parameters); - - var result = pwsh.Invoke() - .FirstOrDefault(); + var result = this.powerShellGet.FindModule( + pwsh, + moduleName, + unitInternal.GetSemanticVersion(), + unitInternal.GetSemanticMinVersion(), + unitInternal.GetSemanticMaxVersion(), + unitInternal.GetDirective<string>(DirectiveConstants.Repository), + unitInternal.GetDirective(DirectiveConstants.AllowPrerelease)); this.OnDiagnostics(DiagnosticLevel.Verbose, pwsh); return result; @@ -330,67 +291,17 @@ namespace Microsoft.Management.Configuration.Processor.Runspaces /// <inheritdoc/> public PSObject? FindDscResource(ConfigurationUnitInternal unitInternal) { - var parameters = new Dictionary<string, object>() - { - { Parameters.Name, unitInternal.Unit.UnitName }, - }; - - // Don't use ModuleSpecification here. Each parameter is independent and - // we need version even if a module was not specified. - string? moduleName = unitInternal.GetDirective<string>(DirectiveConstants.Module); - var semanticVersion = unitInternal.GetSemanticVersion(); - var semanticMinVersion = unitInternal.GetSemanticMinVersion(); - var semanticMaxVersion = unitInternal.GetSemanticMaxVersion(); - string? repository = unitInternal.GetDirective<string>(DirectiveConstants.Repository); - - bool? allowPrerelease = unitInternal.GetDirective(DirectiveConstants.AllowPrerelease); - bool implicitAllowPrerelease = false; - - if (!string.IsNullOrEmpty(moduleName)) - { - parameters.Add(Parameters.ModuleName, moduleName); - } - - if (semanticVersion != null) - { - implicitAllowPrerelease |= semanticVersion.IsPrerelease; - parameters.Add(Parameters.RequiredVersion, semanticVersion.ToString()); - } - - if (semanticMinVersion != null) - { - implicitAllowPrerelease |= semanticMinVersion.IsPrerelease; - parameters.Add(Parameters.MinimumVersion, semanticMinVersion.ToString()); - } - - if (semanticMaxVersion != null) - { - implicitAllowPrerelease |= semanticMaxVersion.IsPrerelease; - parameters.Add(Parameters.MaximumVersion, semanticMaxVersion.ToString()); - } - - if (!string.IsNullOrEmpty(repository)) - { - parameters.Add(Parameters.Repository, repository); - } - - if (allowPrerelease.HasValue || implicitAllowPrerelease) - { - // If explicit allowPrerelease = false don't use implicit. - bool allow = allowPrerelease.HasValue ? allowPrerelease.Value : implicitAllowPrerelease; - parameters.Add(Parameters.AllowPrerelease, allow); - } - using PowerShell pwsh = PowerShell.Create(this.Runspace); - pwsh.AddCommand(Commands.FindDscResource) - .AddParameters(parameters); - - // The result is just a PSCustomObject with a type name of Microsoft.PowerShell.Commands.PSGetDscResourceInfo. - // When no module is passed and a resource is not found, this will return an empty list. If a module - // is specified and no resource is found then it will fail earlier because of a Write-Error. - var result = pwsh.Invoke() - .FirstOrDefault(); + var result = this.powerShellGet.FindDscResource( + pwsh, + unitInternal.Unit.UnitName, + unitInternal.GetDirective<string>(DirectiveConstants.Module), + unitInternal.GetSemanticVersion(), + unitInternal.GetSemanticMinVersion(), + unitInternal.GetSemanticMaxVersion(), + unitInternal.GetDirective<string>(DirectiveConstants.Repository), + unitInternal.GetDirective(DirectiveConstants.AllowPrerelease)); this.OnDiagnostics(DiagnosticLevel.Verbose, pwsh); return result; @@ -400,12 +311,15 @@ namespace Microsoft.Management.Configuration.Processor.Runspaces public void SaveModule(PSObject inputObject, string location) { using PowerShell pwsh = PowerShell.Create(this.Runspace); + this.powerShellGet.SaveModule(pwsh, inputObject, location); + this.OnDiagnostics(DiagnosticLevel.Verbose, pwsh); + } - _ = pwsh.AddCommand(Commands.SaveModule) - .AddParameter(Parameters.Path, location) - .AddParameter(Parameters.InputObject, inputObject) - .Invoke(); - + /// <inheritdoc/> + public void SaveModule(ModuleSpecification moduleSpecification, string location) + { + using PowerShell pwsh = PowerShell.Create(this.Runspace); + this.powerShellGet.SaveModule(pwsh, moduleSpecification, location); this.OnDiagnostics(DiagnosticLevel.Verbose, pwsh); } @@ -413,19 +327,7 @@ namespace Microsoft.Management.Configuration.Processor.Runspaces public void InstallModule(PSObject inputObject) { using PowerShell pwsh = PowerShell.Create(this.Runspace); - - // If the repository is untrusted, it will fail with: - // Microsoft.PowerShell.Commands.WriteErrorException : Exception calling "ShouldContinue" with "5" - // argument(s): "A command that prompts the user failed because the host program or the command type - // does not support user interaction. - // If its trusted, PowerShellGets adds the Force parameter to the call to PackageManager\Install-Package. - // TODO: Once we have policies, we should remove Force. For hosted environments and depending - // on the policy we will trust PSGallery when we create the Runspace or add Force here. - _ = pwsh.AddCommand(Commands.InstallModule) - .AddParameter(Parameters.InputObject, inputObject) - .AddParameter(Parameters.Force) - .Invoke(); - + this.powerShellGet.InstallModule(pwsh, inputObject); this.OnDiagnostics(DiagnosticLevel.Verbose, pwsh); } @@ -436,31 +338,8 @@ namespace Microsoft.Management.Configuration.Processor.Runspaces if (!this.ValidateModule(moduleSpecification)) { // Ok, we have to get it. - var parameters = new Dictionary<string, object>() - { - { Parameters.Name, moduleSpecification.Name }, - }; - if (moduleSpecification.Version is not null) - { - parameters.Add(Parameters.MinimumVersion, moduleSpecification.Version); - } - - if (moduleSpecification.MaximumVersion is not null) - { - parameters.Add(Parameters.MaximumVersion, moduleSpecification.MaximumVersion); - } - - if (moduleSpecification.RequiredVersion is not null) - { - parameters.Add(Parameters.RequiredVersion, moduleSpecification.RequiredVersion); - } - using PowerShell pwsh = PowerShell.Create(this.Runspace); - _ = pwsh.AddCommand(Commands.InstallModule) - .AddParameters(parameters) - .AddParameter(Parameters.Force) - .Invoke(); - + this.powerShellGet.InstallModule(pwsh, moduleSpecification); this.OnDiagnostics(DiagnosticLevel.Verbose, pwsh); } } diff --git a/src/Microsoft.Management.Configuration.Processor/ProcessorEnvironments/IProcessorEnvironment.cs b/src/Microsoft.Management.Configuration.Processor/ProcessorEnvironments/IProcessorEnvironment.cs @@ -139,6 +139,13 @@ namespace Microsoft.Management.Configuration.Processor.ProcessorEnvironments void SaveModule(PSObject inputObject, string location); /// <summary> + /// Calls Save-Module. + /// </summary> + /// <param name="moduleSpecification">Module specification.</param> + /// <param name="location">Location to save module.</param> + void SaveModule(ModuleSpecification moduleSpecification, string location); + + /// <summary> /// Calls Install-Module -InputObject object. /// Input object must be the result of Find cmdlets of PowerShellGet. /// </summary>