Skip to content
Draft
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
38 changes: 38 additions & 0 deletions src/code/ArgumentCompleter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,44 @@
using System.Management.Automation;
using System.Management.Automation.Language;

internal class RuntimeIdentifierCompleter : IArgumentCompleter
{
private static readonly string[] s_knownRids =
{
"win-x64",
"win-x86",
"win-arm64",
"linux-x64",
"linux-arm64",
"linux-arm",
"linux-musl-x64",
"linux-musl-arm64",
"osx-x64",
"osx-arm64",
};

public IEnumerable<CompletionResult> CompleteArgument(
string commandName,
string parameterName,
string wordToComplete,
CommandAst commandAst,
IDictionary fakeBoundParameters)
{
wordToComplete = Utils.TrimQuotes(wordToComplete);
var wordToCompletePattern = WildcardPattern.Get(
pattern: string.IsNullOrWhiteSpace(wordToComplete) ? "*" : wordToComplete + "*",
options: WildcardOptions.IgnoreCase);

foreach (string rid in s_knownRids)
{
if (wordToCompletePattern.IsMatch(rid))
{
yield return new CompletionResult(rid);
}
}
}
}

internal class RepositoryNameCompleter : IArgumentCompleter
{
public IEnumerable<CompletionResult> CompleteArgument(
Expand Down
104 changes: 91 additions & 13 deletions src/code/InstallHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ internal class InstallHelper
private bool _noClobber;
private bool _authenticodeCheck;
private bool _savePkg;
private bool _filterRuntimeAssets;
private string _runtimeIdentifier;
List<string> _pathsToSearch;
List<string> _pkgNamesToInstall;
private string _tmpPath;
Expand Down Expand Up @@ -99,12 +101,14 @@ public IEnumerable<PSResourceInfo> BeginInstallPackages(
List<string> pathsToInstallPkg,
ScopeType? scope,
string tmpPath,
HashSet<string> pkgsInstalled)
HashSet<string> pkgsInstalled,
bool filterRuntimeAssets = false,
string runtimeIdentifier = null)
{
_cmdletPassedIn.WriteDebug("In InstallHelper::BeginInstallPackages()");
_cmdletPassedIn.WriteDebug(string.Format("Parameters passed in >>> Name: '{0}'; VersionRange: '{1}'; NuGetVersion: '{2}'; VersionType: '{3}'; Version: '{4}'; Prerelease: '{5}'; Repository: '{6}'; " +
"AcceptLicense: '{7}'; Quiet: '{8}'; Reinstall: '{9}'; TrustRepository: '{10}'; NoClobber: '{11}'; AsNupkg: '{12}'; IncludeXml '{13}'; SavePackage '{14}'; TemporaryPath '{15}'; SkipDependencyCheck: '{16}'; " +
"AuthenticodeCheck: '{17}'; PathsToInstallPkg: '{18}'; Scope '{19}'",
"AuthenticodeCheck: '{17}'; PathsToInstallPkg: '{18}'; Scope '{19}'; FilterRuntimeAssets: '{20}'; RuntimeIdentifier: '{21}'",
string.Join(",", names),
versionRange != null ? (versionRange.OriginalString != null ? versionRange.OriginalString : string.Empty) : string.Empty,
nugetVersion != null ? nugetVersion.ToString() : string.Empty,
Expand All @@ -124,7 +128,9 @@ public IEnumerable<PSResourceInfo> BeginInstallPackages(
skipDependencyCheck,
authenticodeCheck,
pathsToInstallPkg != null ? string.Join(",", pathsToInstallPkg) : string.Empty,
scope?.ToString() ?? string.Empty));
scope?.ToString() ?? string.Empty,
filterRuntimeAssets,
runtimeIdentifier ?? string.Empty));

_versionRange = versionRange;
_nugetVersion = nugetVersion;
Expand All @@ -141,6 +147,8 @@ public IEnumerable<PSResourceInfo> BeginInstallPackages(
_asNupkg = asNupkg;
_includeXml = includeXml;
_savePkg = savePkg;
_filterRuntimeAssets = filterRuntimeAssets;
_runtimeIdentifier = runtimeIdentifier;
_pathsToInstallPkg = pathsToInstallPkg;
_tmpPath = tmpPath ?? Path.GetTempPath();

Expand Down Expand Up @@ -402,7 +410,8 @@ private void MoveFilesIntoInstallPath(
string installPath,
string newVersion,
string moduleManifestVersion,
string scriptPath)
string scriptPath,
bool mergeRuntimeContent)
{
_cmdletPassedIn.WriteDebug("In InstallHelper::MoveFilesIntoInstallPath()");
// Creating the proper installation path depending on whether pkg is a module or script
Expand All @@ -425,6 +434,13 @@ private void MoveFilesIntoInstallPath(
Directory.CreateDirectory(newPathParent);
Utils.MoveDirectory(tempModuleVersionDir, finalModuleVersionDir);
}
else if (mergeRuntimeContent && Directory.Exists(finalModuleVersionDir))
{
_cmdletPassedIn.WriteVerbose(
$"Merging runtime content from '{tempModuleVersionDir}' into '{finalModuleVersionDir}'");
Utils.MergeDirContents(tempModuleVersionDir, finalModuleVersionDir);
Utils.DeleteDirectory(tempModuleVersionDir);
}
else
{
_cmdletPassedIn.WriteVerbose($"Temporary module version directory is: '{tempModuleVersionDir}'");
Expand Down Expand Up @@ -555,15 +571,27 @@ private List<PSResourceInfo> InstallPackages(
continue;
}

bool mergeRuntimeContent = false;

// Check to see if the pkg is already installed (unless -Reinstall was specified).
if (!_reinstall && _packagesOnMachine.Contains($"{pkgToInstall.Name}{pkgVersion}"))
{
_cmdletPassedIn.WriteWarning($"Resource '{pkgToInstall.Name}' with version '{pkgVersion}' is already installed. If you would like to reinstall, please run the cmdlet again with the -Reinstall parameter");
if (_filterRuntimeAssets && !string.IsNullOrEmpty(_runtimeIdentifier))
{
_cmdletPassedIn.WriteVerbose(
$"Resource '{pkgToInstall.Name}' with version '{pkgVersion}' is already installed. " +
$"Proceeding to merge runtime content for RuntimeIdentifier '{_runtimeIdentifier}'.");
mergeRuntimeContent = true;
}
else
{
_cmdletPassedIn.WriteWarning($"Resource '{pkgToInstall.Name}' with version '{pkgVersion}' is already installed. If you would like to reinstall, please run the cmdlet again with the -Reinstall parameter");

// Remove from tracking list of packages to install.
_pkgNamesToInstall.RemoveAll(x => x.Equals(pkgToInstall.Name, StringComparison.InvariantCultureIgnoreCase));
// Remove from tracking list of packages to install.
_pkgNamesToInstall.RemoveAll(x => x.Equals(pkgToInstall.Name, StringComparison.InvariantCultureIgnoreCase));

continue;
continue;
}
}

// ShouldProcess gate (handles -WhatIf / -Confirm) on the pipeline thread.
Expand All @@ -579,7 +607,8 @@ private List<PSResourceInfo> InstallPackages(
{
PkgToInstall = pkgToInstall,
PkgVersion = pkgVersion,
TempInstallPath = CreateInstallationTempPath()
TempInstallPath = CreateInstallationTempPath(),
MergeRuntimeContent = mergeRuntimeContent,
});
}

Expand Down Expand Up @@ -632,7 +661,14 @@ private List<PSResourceInfo> InstallPackages(

// Parent package and dependencies are now installed to temp directory.
// Try to move all package directories from temp directory to final destination.
if (!TryMoveInstallContent(workItem.TempInstallPath, scope, workItem.PackagesHash))
string mergePackageName = workItem.MergeRuntimeContent
? workItem.PkgToInstall.Name
: null;
if (!TryMoveInstallContent(
workItem.TempInstallPath,
scope,
workItem.PackagesHash,
mergePackageName))
{
_cmdletPassedIn.WriteError(new ErrorRecord(
new InvalidOperationException(),
Expand Down Expand Up @@ -683,6 +719,7 @@ private sealed class ParentInstallWorkItem
public string TempInstallPath;
public ConcurrentDictionary<string, Hashtable> PackagesHash;
public bool Succeeded;
public bool MergeRuntimeContent;
public readonly ConcurrentQueue<ErrorRecord> ErrorMsgs = new();
public readonly ConcurrentQueue<string> WarningMsgs = new();
public readonly ConcurrentQueue<string> DebugMsgs = new();
Expand Down Expand Up @@ -1272,9 +1309,10 @@ private bool TrySaveNupkgToTempPath(
}

/// <summary>
/// Extracts files from .nupkg
/// Extracts files from .nupkg.
/// Similar functionality as System.IO.Compression.ZipFile.ExtractToDirectory,
/// but while ExtractToDirectory cannot overwrite files, this method can.
/// Install-PSResource additionally filters root-level RID folders.
/// </summary>
/// <remarks>
/// This method may run on a background thread (via Parallel.ForEach), so it must not call
Expand Down Expand Up @@ -1304,6 +1342,22 @@ private bool TryExtractToDirectory(
{
foreach (ZipArchiveEntry entry in archive.Entries.Where(entry => entry.CompressedLength > 0))
{
if (_filterRuntimeAssets)
{
bool includeEntry = string.IsNullOrEmpty(_runtimeIdentifier)
? RuntimePackageHelper.ShouldIncludeEntry(entry.FullName)
: RuntimePackageHelper.ShouldIncludeEntry(entry.FullName, _runtimeIdentifier);

if (!includeEntry)
{
string targetRid = _runtimeIdentifier ??
RuntimeIdentifierHelper.GetCurrentRuntimeIdentifier();
debugMsgs.Enqueue(
$"Skipping runtime entry not compatible with RuntimeIdentifier '{targetRid}': {entry.FullName}");
continue;
}
}

// If a file has one or more parent directories.
if (entry.FullName.Contains(Path.DirectorySeparatorChar) || entry.FullName.Contains(Path.AltDirectorySeparatorChar))
{
Expand All @@ -1329,6 +1383,23 @@ private bool TryExtractToDirectory(
entry.ExtractToFile(destinationPath, overwrite: true);
}
}

if (_filterRuntimeAssets && !string.IsNullOrEmpty(_runtimeIdentifier))
{
IReadOnlyList<string> availableRids =
RuntimePackageHelper.GetAvailableRidsFromArchive(archive);
bool hasMatchingRid = availableRids.Any(
rid => RuntimeIdentifierHelper.IsCompatibleRid(rid, _runtimeIdentifier));

if (availableRids.Count > 0 && !hasMatchingRid)
{
string available = string.Join(", ", availableRids);
warningMsgs.Enqueue(
$"The specified RuntimeIdentifier '{_runtimeIdentifier}' was not found in this package. " +
$"No matching root-level runtime assets were installed. " +
$"Available root-level RIDs: {available}");
}
}
}
}
catch (Exception e)
Expand All @@ -1348,7 +1419,11 @@ private bool TryExtractToDirectory(
/// <summary>
/// Moves package files/directories from the temp install path into the final install path location.
/// </summary>
private bool TryMoveInstallContent(string tempInstallPath, ScopeType scope, ConcurrentDictionary<string, Hashtable> packagesHash)
private bool TryMoveInstallContent(
string tempInstallPath,
ScopeType scope,
ConcurrentDictionary<string, Hashtable> packagesHash,
string mergePackageName)
{
_cmdletPassedIn.WriteDebug("In InstallHelper::TryMoveInstallContent()");
foreach (string pkgName in packagesHash.Keys)
Expand All @@ -1361,6 +1436,8 @@ private bool TryMoveInstallContent(string tempInstallPath, ScopeType scope, Conc
string pkgVersion = pkgInfo["pkgVersion"] as string;
string scriptPath = pkgInfo["scriptPath"] as string;
string installPath = pkgInfo["installPath"] as string;
bool mergeRuntimeContent = !string.IsNullOrEmpty(mergePackageName) &&
string.Equals(pkgName, mergePackageName, StringComparison.OrdinalIgnoreCase);

try
{
Expand All @@ -1373,7 +1450,8 @@ private bool TryMoveInstallContent(string tempInstallPath, ScopeType scope, Conc
installPath,
newVersion: pkgVersion, // would not have prerelease label in this string
moduleManifestVersion: pkgVersion,
scriptPath);
scriptPath,
mergeRuntimeContent);

_cmdletPassedIn.WriteVerbose($"Successfully installed package '{pkgName}' to location '{installPath}'");

Expand Down
14 changes: 13 additions & 1 deletion src/code/InstallPSResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,16 @@ public string TemporaryPath
[Parameter]
public SwitchParameter AuthenticodeCheck { get; set; }

/// <summary>
/// Specifies the Runtime Identifier (RID) used to select root-level runtime asset folders.
/// When omitted, the RID for the current process is used. Specify a RID to prepare a
/// package for a different platform.
/// </summary>
[Parameter]
[ValidateNotNullOrEmpty]
[ArgumentCompleter(typeof(RuntimeIdentifierCompleter))]
public string RuntimeIdentifier { get; set; }

/// <summary>
/// Passes the resource installed to the console.
/// </summary>
Expand Down Expand Up @@ -597,7 +607,9 @@ private void ProcessInstallHelper(string[] pkgNames, string pkgVersion, bool pkg
pathsToInstallPkg: _pathsToInstallPkg,
scope: scope,
tmpPath: _tmpPath,
pkgsInstalled: _packagesOnMachine);
pkgsInstalled: _packagesOnMachine,
filterRuntimeAssets: true,
runtimeIdentifier: RuntimeIdentifier);

if (PassThru)
{
Expand Down
56 changes: 56 additions & 0 deletions src/code/InternalHooks.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Collections.Generic;
using System.Reflection;

namespace Microsoft.PowerShell.PSResourceGet.UtilClasses
Expand Down Expand Up @@ -43,5 +44,60 @@ public static string GetUserString()
{
return Microsoft.PowerShell.PSResourceGet.Cmdlets.UserAgentInfo.UserAgentString();
}

public static string GetCurrentRuntimeIdentifier()
{
return RuntimeIdentifierHelper.GetCurrentRuntimeIdentifier();
}

public static IReadOnlyList<string> GetCompatibleRuntimeIdentifiers()
{
return RuntimeIdentifierHelper.GetCompatibleRuntimeIdentifiers();
}

public static IReadOnlyList<string> GetCompatibleRuntimeIdentifiersFor(string rid)
{
return RuntimeIdentifierHelper.GetCompatibleRuntimeIdentifiers(rid);
}

public static bool IsCompatibleRid(string rid)
{
return RuntimeIdentifierHelper.IsCompatibleRid(rid);
}

public static bool IsCompatibleRidWith(string candidateRid, string targetRid)
{
return RuntimeIdentifierHelper.IsCompatibleRid(candidateRid, targetRid);
}

public static bool IsRidFolder(string folderName)
{
return RuntimePackageHelper.IsRidFolder(folderName);
}

public static bool IsRuntimeAssetEntry(string entryFullName)
{
return RuntimePackageHelper.IsRuntimeAssetEntry(entryFullName);
}

public static string GetRidFromRuntimeAssetEntry(string entryFullName)
{
return RuntimePackageHelper.GetRidFromRuntimeAssetEntry(entryFullName);
}

public static bool ShouldIncludeRuntimeAssetEntry(string entryFullName)
{
return RuntimePackageHelper.ShouldIncludeEntry(entryFullName);
}

public static bool ShouldIncludeRuntimeAssetEntryForRid(string entryFullName, string targetRid)
{
return RuntimePackageHelper.ShouldIncludeEntry(entryFullName, targetRid);
}

public static IReadOnlyList<string> GetAvailableRidsFromZipFile(string zipPath)
{
return RuntimePackageHelper.GetAvailableRidsFromZipFile(zipPath);
}
}
}
Loading
Loading