Skip to content

Commit bbda212

Browse files
author
Justin Chung
committed
Add RID and TFM awareness to Install-PSResource
1 parent 097cfc2 commit bbda212

File tree

9 files changed

+2147
-23
lines changed

9 files changed

+2147
-23
lines changed

src/code/InstallHelper.cs

Lines changed: 212 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
using Microsoft.PowerShell.PSResourceGet.UtilClasses;
5+
using NuGet.Frameworks;
56
using NuGet.Versioning;
67
using System;
78
using System.Collections;
@@ -48,6 +49,9 @@ internal class InstallHelper
4849
private bool _noClobber;
4950
private bool _authenticodeCheck;
5051
private bool _savePkg;
52+
private bool _skipRuntimeFiltering;
53+
private string _runtimeIdentifier;
54+
private string _targetFramework;
5155
List<string> _pathsToSearch;
5256
List<string> _pkgNamesToInstall;
5357
private string _tmpPath;
@@ -91,7 +95,10 @@ public IEnumerable<PSResourceInfo> BeginInstallPackages(
9195
List<string> pathsToInstallPkg,
9296
ScopeType? scope,
9397
string tmpPath,
94-
HashSet<string> pkgsInstalled)
98+
HashSet<string> pkgsInstalled,
99+
bool skipRuntimeFiltering = false,
100+
string runtimeIdentifier = null,
101+
string targetFramework = null)
95102
{
96103
_cmdletPassedIn.WriteDebug("In InstallHelper::BeginInstallPackages()");
97104
_cmdletPassedIn.WriteDebug(string.Format("Parameters passed in >>> Name: '{0}'; VersionRange: '{1}'; NuGetVersion: '{2}'; VersionType: '{3}'; Version: '{4}'; Prerelease: '{5}'; Repository: '{6}'; " +
@@ -133,6 +140,9 @@ public IEnumerable<PSResourceInfo> BeginInstallPackages(
133140
_asNupkg = asNupkg;
134141
_includeXml = includeXml;
135142
_savePkg = savePkg;
143+
_skipRuntimeFiltering = skipRuntimeFiltering;
144+
_runtimeIdentifier = runtimeIdentifier;
145+
_targetFramework = targetFramework;
136146
_pathsToInstallPkg = pathsToInstallPkg;
137147
_tmpPath = tmpPath ?? Path.GetTempPath();
138148

@@ -1161,9 +1171,12 @@ private bool TrySaveNupkgToTempPath(
11611171
}
11621172

11631173
/// <summary>
1164-
/// Extracts files from .nupkg
1174+
/// Extracts files from .nupkg with platform-aware filtering.
11651175
/// Similar functionality as System.IO.Compression.ZipFile.ExtractToDirectory,
11661176
/// but while ExtractToDirectory cannot overwrite files, this method can.
1177+
/// Additionally filters:
1178+
/// - runtimes/{rid}/ entries based on the current platform's RID (unless _skipRuntimeFiltering is true)
1179+
/// - lib/{tfm}/ entries to only extract the best matching Target Framework Moniker
11671180
/// </summary>
11681181
private bool TryExtractToDirectory(string zipPath, string extractPath, out ErrorRecord error)
11691182
{
@@ -1182,8 +1195,50 @@ private bool TryExtractToDirectory(string zipPath, string extractPath, out Error
11821195
{
11831196
using (ZipArchive archive = ZipFile.OpenRead(zipPath))
11841197
{
1198+
// Determine best TFM for lib/ folder filtering
1199+
// If user specified -TargetFramework, use that; otherwise auto-detect
1200+
NuGetFramework bestLibFramework;
1201+
if (!string.IsNullOrEmpty(_targetFramework))
1202+
{
1203+
bestLibFramework = NuGetFramework.ParseFolder(_targetFramework);
1204+
if (bestLibFramework == null || bestLibFramework.IsUnsupported)
1205+
{
1206+
_cmdletPassedIn.WriteDebug($"Could not parse specified TargetFramework '{_targetFramework}', falling back to auto-detection.");
1207+
bestLibFramework = GetBestLibFramework(archive);
1208+
}
1209+
else
1210+
{
1211+
_cmdletPassedIn.WriteDebug($"Using user-specified TargetFramework: {bestLibFramework.GetShortFolderName()}");
1212+
}
1213+
}
1214+
else
1215+
{
1216+
bestLibFramework = GetBestLibFramework(archive);
1217+
}
1218+
11851219
foreach (ZipArchiveEntry entry in archive.Entries.Where(entry => entry.CompressedLength > 0))
11861220
{
1221+
// RID filtering: skip runtimes/ entries for incompatible platforms
1222+
if (!_skipRuntimeFiltering)
1223+
{
1224+
bool includeEntry = !string.IsNullOrEmpty(_runtimeIdentifier)
1225+
? RuntimePackageHelper.ShouldIncludeEntry(entry.FullName, _runtimeIdentifier)
1226+
: RuntimePackageHelper.ShouldIncludeEntry(entry.FullName);
1227+
1228+
if (!includeEntry)
1229+
{
1230+
_cmdletPassedIn.WriteDebug($"Skipping runtime entry not matching target platform: {entry.FullName}");
1231+
continue;
1232+
}
1233+
}
1234+
1235+
// TFM filtering: for lib/ entries, only extract the best matching TFM
1236+
if (bestLibFramework != null && !ShouldIncludeLibEntry(entry.FullName, bestLibFramework))
1237+
{
1238+
_cmdletPassedIn.WriteDebug($"Skipping lib entry not matching target framework: {entry.FullName}");
1239+
continue;
1240+
}
1241+
11871242
// If a file has one or more parent directories.
11881243
if (entry.FullName.Contains(Path.DirectorySeparatorChar) || entry.FullName.Contains(Path.AltDirectorySeparatorChar))
11891244
{
@@ -1225,6 +1280,161 @@ private bool TryExtractToDirectory(string zipPath, string extractPath, out Error
12251280
return true;
12261281
}
12271282

1283+
/// <summary>
1284+
/// Determines the best matching Target Framework Moniker (TFM) from the lib/ folder entries in a zip archive.
1285+
/// Uses NuGet.Frameworks.FrameworkReducer to select the nearest compatible framework.
1286+
/// </summary>
1287+
/// <param name="archive">The zip archive to analyze.</param>
1288+
/// <returns>The best matching NuGetFramework, or null if no lib/ folders exist or no match is found.</returns>
1289+
private NuGetFramework GetBestLibFramework(ZipArchive archive)
1290+
{
1291+
// Collect all TFMs from lib/ folder entries
1292+
var libFrameworks = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
1293+
foreach (ZipArchiveEntry entry in archive.Entries)
1294+
{
1295+
string normalizedName = entry.FullName.Replace('\\', '/');
1296+
if (normalizedName.StartsWith("lib/", StringComparison.OrdinalIgnoreCase))
1297+
{
1298+
string[] segments = normalizedName.Split('/');
1299+
if (segments.Length >= 3 && !string.IsNullOrEmpty(segments[1]))
1300+
{
1301+
libFrameworks.Add(segments[1]);
1302+
}
1303+
}
1304+
}
1305+
1306+
if (libFrameworks.Count <= 1)
1307+
{
1308+
// Zero or one TFM — no filtering needed
1309+
return null;
1310+
}
1311+
1312+
try
1313+
{
1314+
// Detect the current runtime's target framework
1315+
NuGetFramework currentFramework = GetCurrentFramework();
1316+
1317+
// Parse all discovered TFMs
1318+
var parsedFrameworks = new List<NuGetFramework>();
1319+
foreach (string tfm in libFrameworks)
1320+
{
1321+
NuGetFramework parsed = NuGetFramework.ParseFolder(tfm);
1322+
if (parsed != null && !parsed.IsUnsupported)
1323+
{
1324+
parsedFrameworks.Add(parsed);
1325+
}
1326+
}
1327+
1328+
if (parsedFrameworks.Count == 0)
1329+
{
1330+
return null;
1331+
}
1332+
1333+
// Use FrameworkReducer to find the best match
1334+
var reducer = new FrameworkReducer();
1335+
NuGetFramework bestMatch = reducer.GetNearest(currentFramework, parsedFrameworks);
1336+
1337+
if (bestMatch != null)
1338+
{
1339+
_cmdletPassedIn.WriteDebug($"Selected best matching TFM: {bestMatch.GetShortFolderName()} (from {string.Join(", ", libFrameworks)})");
1340+
}
1341+
1342+
return bestMatch;
1343+
}
1344+
catch (Exception e)
1345+
{
1346+
_cmdletPassedIn.WriteDebug($"TFM selection failed, extracting all lib/ folders: {e.Message}");
1347+
return null;
1348+
}
1349+
}
1350+
1351+
/// <summary>
1352+
/// Determines if a zip entry from the lib/ folder should be included based on the best matching TFM.
1353+
/// Non-lib entries are always included.
1354+
/// </summary>
1355+
/// <param name="entryFullName">The full name of the zip entry.</param>
1356+
/// <param name="bestFramework">The best matching framework from GetBestLibFramework.</param>
1357+
/// <returns>True if the entry should be extracted.</returns>
1358+
private static bool ShouldIncludeLibEntry(string entryFullName, NuGetFramework bestFramework)
1359+
{
1360+
string normalizedName = entryFullName.Replace('\\', '/');
1361+
1362+
// Only filter entries inside lib/
1363+
if (!normalizedName.StartsWith("lib/", StringComparison.OrdinalIgnoreCase))
1364+
{
1365+
return true;
1366+
}
1367+
1368+
string[] segments = normalizedName.Split('/');
1369+
if (segments.Length < 3 || string.IsNullOrEmpty(segments[1]))
1370+
{
1371+
// lib/ root files (uncommon) — include them
1372+
return true;
1373+
}
1374+
1375+
string entryTfm = segments[1];
1376+
NuGetFramework entryFramework = NuGetFramework.ParseFolder(entryTfm);
1377+
1378+
if (entryFramework == null || entryFramework.IsUnsupported)
1379+
{
1380+
// Can't parse TFM, include to be safe
1381+
return true;
1382+
}
1383+
1384+
// Only include entries matching the best framework
1385+
return entryFramework.Equals(bestFramework);
1386+
}
1387+
1388+
/// <summary>
1389+
/// Gets the NuGetFramework for the current runtime environment.
1390+
/// Since this assembly is compiled as net472, it must detect the actual host runtime
1391+
/// by parsing RuntimeInformation.FrameworkDescription rather than using Environment.Version
1392+
/// (which returns 4.0.30319.x even when running on .NET 8+ via compatibility shims).
1393+
/// </summary>
1394+
private static NuGetFramework GetCurrentFramework()
1395+
{
1396+
string runtimeDescription = RuntimeInformation.FrameworkDescription;
1397+
1398+
if (runtimeDescription.StartsWith(".NET Framework", StringComparison.OrdinalIgnoreCase))
1399+
{
1400+
// Windows PowerShell 5.1 — .NET Framework 4.x
1401+
return NuGetFramework.ParseFolder("net472");
1402+
}
1403+
1404+
// PowerShell 7+ on .NET Core/.NET 5+
1405+
// RuntimeInformation.FrameworkDescription format examples:
1406+
// ".NET Core 3.1.0" -> netcoreapp3.1
1407+
// ".NET 6.0.5" -> net6.0
1408+
// ".NET 8.0.1" -> net8.0
1409+
// ".NET 9.0.0" -> net9.0
1410+
try
1411+
{
1412+
string versionPart = runtimeDescription;
1413+
1414+
// Strip prefix to get just the version number
1415+
if (versionPart.StartsWith(".NET Core ", StringComparison.OrdinalIgnoreCase))
1416+
{
1417+
versionPart = versionPart.Substring(".NET Core ".Length);
1418+
}
1419+
else if (versionPart.StartsWith(".NET ", StringComparison.OrdinalIgnoreCase))
1420+
{
1421+
versionPart = versionPart.Substring(".NET ".Length);
1422+
}
1423+
1424+
if (Version.TryParse(versionPart, out Version parsedVersion))
1425+
{
1426+
return new NuGetFramework(".NETCoreApp", new Version(parsedVersion.Major, parsedVersion.Minor));
1427+
}
1428+
}
1429+
catch
1430+
{
1431+
// Fall through to default
1432+
}
1433+
1434+
// Fallback: default to netstandard2.0 which is broadly compatible
1435+
return NuGetFramework.ParseFolder("netstandard2.0");
1436+
}
1437+
12281438
/// <summary>
12291439
/// Moves package files/directories from the temp install path into the final install path location.
12301440
/// </summary>

src/code/InstallPSResource.cs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,34 @@ public string TemporaryPath
138138
[Parameter]
139139
public SwitchParameter AuthenticodeCheck { get; set; }
140140

141+
/// <summary>
142+
/// Skips platform-specific runtime asset filtering during installation.
143+
/// When specified, all runtime assets for all platforms will be installed (original behavior).
144+
/// By default, only runtime assets compatible with the current platform are installed.
145+
/// </summary>
146+
[Parameter]
147+
public SwitchParameter SkipRuntimeFiltering { get; set; }
148+
149+
/// <summary>
150+
/// Specifies the Runtime Identifier (RID) to filter platform-specific assets for.
151+
/// When specified, only runtime assets matching this RID are installed instead of the auto-detected platform.
152+
/// Use this for cross-platform deployment scenarios (e.g., preparing a Linux package from Windows).
153+
/// Valid values follow the .NET RID catalog: win-x64, linux-x64, osx-arm64, etc.
154+
/// </summary>
155+
[Parameter]
156+
[ValidateNotNullOrEmpty]
157+
public string RuntimeIdentifier { get; set; }
158+
159+
/// <summary>
160+
/// Specifies the Target Framework Moniker (TFM) to select for lib/ folder filtering.
161+
/// When specified, only lib/ assets matching this TFM are installed instead of the auto-detected framework.
162+
/// Use this for cross-platform deployment scenarios (e.g., preparing a .NET 6 package from a .NET 8 host).
163+
/// Valid values follow NuGet TFM format: net472, netstandard2.0, net6.0, net8.0, etc.
164+
/// </summary>
165+
[Parameter]
166+
[ValidateNotNullOrEmpty]
167+
public string TargetFramework { get; set; }
168+
141169
/// <summary>
142170
/// Passes the resource installed to the console.
143171
/// </summary>
@@ -597,7 +625,10 @@ private void ProcessInstallHelper(string[] pkgNames, string pkgVersion, bool pkg
597625
pathsToInstallPkg: _pathsToInstallPkg,
598626
scope: scope,
599627
tmpPath: _tmpPath,
600-
pkgsInstalled: _packagesOnMachine);
628+
pkgsInstalled: _packagesOnMachine,
629+
skipRuntimeFiltering: SkipRuntimeFiltering,
630+
runtimeIdentifier: RuntimeIdentifier,
631+
targetFramework: TargetFramework);
601632

602633
if (PassThru)
603634
{

0 commit comments

Comments
 (0)