22// Licensed under the MIT License.
33
44using Microsoft . PowerShell . PSResourceGet . UtilClasses ;
5+ using NuGet . Frameworks ;
56using NuGet . Versioning ;
67using System ;
78using 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>
0 commit comments