Skip to content

Commit

Permalink
Fix for finding and installing modules microsoft#1860 (microsoft#1879)
Browse files Browse the repository at this point in the history
  • Loading branch information
BernieWhite authored Jul 28, 2024
1 parent c07daa0 commit 3856d75
Show file tree
Hide file tree
Showing 12 changed files with 262 additions and 61 deletions.
3 changes: 3 additions & 0 deletions docs/CHANGELOG-v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ What's changed since pre-release v3.0.0-B0203:
[#1872](https://github.com/microsoft/PSRule/pull/1872)
- Bump PSScriptAnalyzer to v1.22.0.
[#1858](https://github.com/microsoft/PSRule/pull/1858)
- Bug fixes:
- Fixed CLI exception the term Find-Module is not recognized by @BernieWhite.
[#1860](https://github.com/microsoft/PSRule/issues/1860)

## v3.0.0-B0203 (pre-release)

Expand Down
10 changes: 8 additions & 2 deletions src/PSRule.CommandLine/ClientContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public ClientContext(InvocationContext invocation, string? option, bool verbose,
Debug = debug;
Host = new ClientHost(this, verbose, debug);
Option = GetOption(Host, option);
CachePath = Path;
}

/// <summary>
Expand All @@ -50,15 +51,20 @@ public ClientContext(InvocationContext invocation, string? option, bool verbose,
public PSRuleOption Option { get; }

/// <summary>
///
/// Determines if verbose level diagnostic information should be displayed.
/// </summary>
public bool Verbose { get; }

/// <summary>
///
/// Determines if debug level diagnostic information should be displayed.
/// </summary>
public bool Debug { get; }

/// <summary>
/// Configures the path to use for caching rules modules.
/// </summary>
public string CachePath { get; }

private static PSRuleOption GetOption(ClientHost host, string? path)
{
PSRuleOption.UseHostContext(host);
Expand Down
120 changes: 86 additions & 34 deletions src/PSRule.CommandLine/Commands/ModuleCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,19 @@
using System.Diagnostics.CodeAnalysis;
using System.Management.Automation;
using Newtonsoft.Json;
using NuGet.Configuration;
using NuGet.Protocol;
using NuGet.Protocol.Core.Types;
using NuGet.Versioning;
using PSRule.CommandLine.Models;
using PSRule.CommandLine.Resources;
using PSRule.Configuration;
using PSRule.Data;
using PSRule.Pipeline.Dependencies;
using SemanticVersion = PSRule.Data.SemanticVersion;
using NuGet.Packaging;
using NuGet.Common;
using PSRule.Pipeline;

namespace PSRule.CommandLine.Commands;

Expand All @@ -29,16 +36,16 @@ public sealed class ModuleCommand
private const int ERROR_MODULE_ADD_VIOLATES_CONSTRAINT = 503;

private const string PARAM_NAME = "Name";
private const string PARAM_VERSION = "Version";

private const string FIELD_PRERELEASE = "Prerelease";
private const string FIELD_PSDATA = "PSData";
private const string PRERELEASE_SEPARATOR = "-";
private const string POWERSHELL_GALLERY_SOURCE = "https://www.powershellgallery.com/api/v2/";
private const string MODULES_PATH = "Modules";

/// <summary>
/// Call <c>module restore</c>.
/// </summary>
public static int ModuleRestore(RestoreOptions operationOptions, ClientContext clientContext)
public static async Task<int> ModuleRestoreAsync(RestoreOptions operationOptions, ClientContext clientContext, CancellationToken cancellationToken = default)
{
var exitCode = 0;
var requires = clientContext.Option.Requires.ToDictionary();
Expand All @@ -61,9 +68,9 @@ public static int ModuleRestore(RestoreOptions operationOptions, ClientContext c
continue;
}

var idealVersion = FindVersion(pwsh, module, null, targetVersion, null);
var idealVersion = await FindVersionAsync(module, null, targetVersion, null, cancellationToken);
if (idealVersion != null)
InstallVersion(clientContext, pwsh, module, idealVersion.ToString());
await InstallVersionAsync(clientContext, module, idealVersion.ToString(), cancellationToken);

if (pwsh.HadErrors || (idealVersion == null && installedVersion == null))
{
Expand Down Expand Up @@ -99,10 +106,10 @@ public static int ModuleRestore(RestoreOptions operationOptions, ClientContext c
}

// Find the ideal version.
var idealVersion = FindVersion(pwsh, includeModule, moduleConstraint, null, null);
var idealVersion = await FindVersionAsync(includeModule, moduleConstraint, null, null, cancellationToken);
if (idealVersion != null)
{
InstallVersion(clientContext, pwsh, includeModule, idealVersion.ToString());
await InstallVersionAsync(clientContext, includeModule, idealVersion.ToString(), cancellationToken);
}
else if (idealVersion == null)
{
Expand Down Expand Up @@ -131,7 +138,7 @@ public static int ModuleRestore(RestoreOptions operationOptions, ClientContext c
/// <summary>
/// Initialize a new lock file based on existing options.
/// </summary>
public static int ModuleInit(ModuleOptions operationOptions, ClientContext clientContext)
public static async Task<int> ModuleInitAsync(ModuleOptions operationOptions, ClientContext clientContext, CancellationToken cancellationToken = default)
{
var exitCode = 0;
var requires = clientContext.Option.Requires.ToDictionary();
Expand All @@ -152,7 +159,7 @@ public static int ModuleInit(ModuleOptions operationOptions, ClientContext clien
var moduleConstraint = requires.TryGetValue(includeModule, out var c) ? c : null;

// Find the ideal version.
var idealVersion = FindVersion(pwsh, includeModule, moduleConstraint, null, null);
var idealVersion = await FindVersionAsync(includeModule, moduleConstraint, null, null, cancellationToken);
if (idealVersion == null)
{
clientContext.LogError(Messages.Error_502, includeModule);
Expand Down Expand Up @@ -183,7 +190,9 @@ public static int ModuleInit(ModuleOptions operationOptions, ClientContext clien
/// <summary>
/// List any module and the installed versions from the lock file.
/// </summary>
public static int ModuleList(ModuleOptions operationOptions, ClientContext clientContext)
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
public static async Task<int> ModuleListAsync(ModuleOptions operationOptions, ClientContext clientContext, CancellationToken cancellationToken = default)
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
{
var exitCode = 0;
var requires = clientContext.Option.Requires.ToDictionary();
Expand All @@ -201,7 +210,7 @@ public static int ModuleList(ModuleOptions operationOptions, ClientContext clien
/// <summary>
/// Add a module to the lock file.
/// </summary>
public static int ModuleAdd(ModuleOptions operationOptions, ClientContext clientContext)
public static async Task<int> ModuleAddAsync(ModuleOptions operationOptions, ClientContext clientContext, CancellationToken cancellationToken = default)
{
var exitCode = 0;
if (operationOptions.Module == null || operationOptions.Module.Length == 0) return exitCode;
Expand All @@ -228,7 +237,7 @@ public static int ModuleAdd(ModuleOptions operationOptions, ClientContext client
}

// Find the ideal version.
var idealVersion = FindVersion(pwsh, module, moduleConstraint, targetVersion, null);
var idealVersion = await FindVersionAsync(module, moduleConstraint, targetVersion, null, cancellationToken);
if (idealVersion == null && targetVersion != null && operationOptions.SkipVerification)
idealVersion = targetVersion;

Expand Down Expand Up @@ -263,7 +272,9 @@ public static int ModuleAdd(ModuleOptions operationOptions, ClientContext client
/// <summary>
/// Remove a module from the lock file.
/// </summary>
public static int ModuleRemove(ModuleOptions operationOptions, ClientContext clientContext)
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
public static async Task<int> ModuleRemoveAsync(ModuleOptions operationOptions, ClientContext clientContext, CancellationToken cancellationToken = default)
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
{
var exitCode = 0;
if (operationOptions.Module == null || operationOptions.Module.Length == 0) return exitCode;
Expand Down Expand Up @@ -295,7 +306,7 @@ public static int ModuleRemove(ModuleOptions operationOptions, ClientContext cli
/// <summary>
/// Upgrade a module within the lock file.
/// </summary>
public static int ModuleUpgrade(ModuleOptions operationOptions, ClientContext clientContext)
public static async Task<int> ModuleUpgradeAsync(ModuleOptions operationOptions, ClientContext clientContext, CancellationToken cancellationToken = default)
{
var exitCode = 0;
var requires = clientContext.Option.Requires.ToDictionary();
Expand All @@ -308,7 +319,7 @@ public static int ModuleUpgrade(ModuleOptions operationOptions, ClientContext cl
var moduleConstraint = requires.TryGetValue(kv.Key, out var c) ? c : null;

// Find the ideal version.
var idealVersion = FindVersion(pwsh, kv.Key, moduleConstraint, null, null);
var idealVersion = await FindVersionAsync(kv.Key, moduleConstraint, null, null, cancellationToken);
if (idealVersion == null)
{
clientContext.LogError(Messages.Error_502, kv.Key);
Expand Down Expand Up @@ -428,19 +439,17 @@ private static bool TryPrivateData(PSModuleInfo info, string propertyName, out H
return false;
}

private static SemanticVersion.Version? FindVersion(PowerShell pwsh, string module, ModuleConstraint? constraint, SemanticVersion.Version? targetVersion, SemanticVersion.Version? installedVersion)
private static async Task<SemanticVersion.Version?> FindVersionAsync(string module, ModuleConstraint? constraint, SemanticVersion.Version? targetVersion, SemanticVersion.Version? installedVersion, CancellationToken cancellationToken)
{
pwsh.Commands.Clear();
pwsh.Streams.ClearStreams();
pwsh.AddCommand("Find-Module")
.AddParameter(PARAM_NAME, module)
.AddParameter("AllVersions");
var cache = new SourceCacheContext();
var logger = new NullLogger();
var resource = await GetSourceRepositoryAsync();
var versions = await resource.GetAllVersionsAsync(module, cache, logger, cancellationToken);

var versions = pwsh.Invoke();
SemanticVersion.Version? result = null;
foreach (var version in versions)
{
if (version.Properties[PARAM_VERSION].Value is string versionString &&
if (version.ToFullString() is string versionString &&
SemanticVersion.TryParseVersion(versionString, out var v) &&
v != null &&
(constraint == null || constraint.Constraint.Equals(v)) &&
Expand All @@ -452,20 +461,63 @@ private static bool TryPrivateData(PSModuleInfo info, string propertyName, out H
return result;
}

private static void InstallVersion([DisallowNull] ClientContext context, [DisallowNull] PowerShell pwsh, [DisallowNull] string name, [DisallowNull] string version)
private static async Task InstallVersionAsync([DisallowNull] ClientContext context, [DisallowNull] string name, [DisallowNull] string version, CancellationToken cancellationToken)
{
context.LogVerbose(Messages.RestoringModule, name, version);

pwsh.Commands.Clear();
pwsh.Streams.ClearStreams();
pwsh.AddCommand("Install-Module")
.AddParameter(PARAM_NAME, name)
.AddParameter("RequiredVersion", version)
.AddParameter("Scope", "CurrentUser")
.AddParameter("AllowPrerelease")
.AddParameter("Force");

pwsh.Invoke();
var cache = new SourceCacheContext();
var logger = new NullLogger();
var resource = await GetSourceRepositoryAsync();

var packageVersion = new NuGetVersion(version);
using var packageStream = new MemoryStream();

await resource.CopyNupkgToStreamAsync(
name,
packageVersion,
packageStream,
cache,
logger,
cancellationToken);

using var packageReader = new PackageArchiveReader(packageStream);
var nuspecReader = await packageReader.GetNuspecReaderAsync(cancellationToken);

var modulePath = GetModulePath(context, name, version);

// Remove existing module.
if (Directory.Exists(modulePath))
Directory.Delete(modulePath, true);

var files = packageReader.GetFiles();
packageReader.CopyFiles(modulePath, files, (name, targetPath, s) =>
{
if (ShouldIgnorePackageFile(name))
return null;

s.CopyToFile(targetPath);

return targetPath;

}, logger, cancellationToken);
}

private static async Task<FindPackageByIdResource> GetSourceRepositoryAsync()
{
var source = new PackageSource(POWERSHELL_GALLERY_SOURCE);
var repository = Repository.Factory.GetCoreV2(source);
return await repository.GetResourceAsync<FindPackageByIdResource>();
}

private static string GetModulePath(ClientContext context, string name, string version)
{
return Path.Combine(context.CachePath, MODULES_PATH, name, version);
}

private static bool ShouldIgnorePackageFile(string name)
{
return string.Equals(name, "[Content_Types].xml", StringComparison.OrdinalIgnoreCase) ||
string.Equals(name, "_rels/.rels", StringComparison.OrdinalIgnoreCase);
}

#endregion Helper methods
Expand Down
1 change: 1 addition & 0 deletions src/PSRule.CommandLine/PSRule.CommandLine.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="NuGet.Protocol" Version="6.10.1" />
<PackageReference Include="Microsoft.PowerShell.SDK" Version="7.4.4" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="8.0.0">
Expand Down
47 changes: 47 additions & 0 deletions src/PSRule.CommandLine/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@
"Microsoft.SourceLink.Common": "8.0.0"
}
},
"NuGet.Protocol": {
"type": "Direct",
"requested": "[6.10.1, )",
"resolved": "6.10.1",
"contentHash": "ZWZKeFBtxBOI4MyM9DbL47APRrLzIvg6vP5YcHHuryHb4W9A0bnAPjpmR5BF6qB5KpGBioqJ0GftxWrZ+W7pUA==",
"dependencies": {
"NuGet.Packaging": "6.10.1"
}
},
"System.CommandLine": {
"type": "Direct",
"requested": "[2.0.0-beta4.22272.1, )",
Expand Down Expand Up @@ -346,6 +355,44 @@
"resolved": "13.0.3",
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
},
"NuGet.Common": {
"type": "Transitive",
"resolved": "6.10.1",
"contentHash": "djIP7OOdQYamFFtLFxbGTotFhlCkRs42Nc2lYS7E1uGLq91zwLdfOUCsWLfFBMT5EA8iqQnyRYrMD/sihgTkiQ==",
"dependencies": {
"NuGet.Frameworks": "6.10.1"
}
},
"NuGet.Configuration": {
"type": "Transitive",
"resolved": "6.10.1",
"contentHash": "SlPy3mxtMZRb281WUDA5Q+8SPR5objjFfXXOGjk5vs60/f7KeIdORJOj2UgxJal9YFFU58sJv3tVkaF51bGoyA==",
"dependencies": {
"NuGet.Common": "6.10.1",
"System.Security.Cryptography.ProtectedData": "4.4.0"
}
},
"NuGet.Frameworks": {
"type": "Transitive",
"resolved": "6.10.1",
"contentHash": "DtppveEBKkGwLoY5fk2DLNxtVbx0iw8r7s/RjYdm2AkK7RwnfJGe+j7DriYSEuxHrvSOU7n3ELKmlnn9jbZYfQ=="
},
"NuGet.Packaging": {
"type": "Transitive",
"resolved": "6.10.1",
"contentHash": "0YiFuHfPty9XOZXEZTj8KPjhBZhr7q91vmANttay+3IsO3ri40sMyGDoTRhFYH/A8dJzwmnD7ZNDJLFTiChwNA==",
"dependencies": {
"Newtonsoft.Json": "13.0.3",
"NuGet.Configuration": "6.10.1",
"NuGet.Versioning": "6.10.1",
"System.Security.Cryptography.Pkcs": "6.0.4"
}
},
"NuGet.Versioning": {
"type": "Transitive",
"resolved": "6.10.1",
"contentHash": "tovHZ3OlMVmsTdhv2z5nwnnhoA1ryhfJMyVQ9/+iv6d3h78fp230XaGy3K/iVcLwB50DdfNfIsitW97KSOWDFg=="
},
"runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": {
"type": "Transitive",
"resolved": "4.3.2",
Expand Down
Loading

0 comments on commit 3856d75

Please sign in to comment.