Skip to content

Commit 98c7c6b

Browse files
author
Justin Chung
committed
Remove SkipRunTime parameter, modify runtime id filtering to match powershell dep resolution, add warning when filtering libs
1 parent d7e0c43 commit 98c7c6b

5 files changed

Lines changed: 184 additions & 89 deletions

File tree

src/code/InstallHelper.cs

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ internal class InstallHelper
4949
private bool _noClobber;
5050
private bool _authenticodeCheck;
5151
private bool _savePkg;
52-
private bool _skipRuntimeFiltering;
5352
private string _runtimeIdentifier;
5453
private string _targetFramework;
5554
List<string> _pathsToSearch;
@@ -96,7 +95,6 @@ public IEnumerable<PSResourceInfo> BeginInstallPackages(
9695
ScopeType? scope,
9796
string tmpPath,
9897
HashSet<string> pkgsInstalled,
99-
bool skipRuntimeFiltering = false,
10098
string runtimeIdentifier = null,
10199
string targetFramework = null)
102100
{
@@ -140,7 +138,6 @@ public IEnumerable<PSResourceInfo> BeginInstallPackages(
140138
_asNupkg = asNupkg;
141139
_includeXml = includeXml;
142140
_savePkg = savePkg;
143-
_skipRuntimeFiltering = skipRuntimeFiltering;
144141
_runtimeIdentifier = runtimeIdentifier;
145142
_targetFramework = targetFramework;
146143
_pathsToInstallPkg = pathsToInstallPkg;
@@ -1175,7 +1172,7 @@ private bool TrySaveNupkgToTempPath(
11751172
/// Similar functionality as System.IO.Compression.ZipFile.ExtractToDirectory,
11761173
/// but while ExtractToDirectory cannot overwrite files, this method can.
11771174
/// Additionally filters:
1178-
/// - runtimes/{rid}/ entries based on the current platform's RID (unless _skipRuntimeFiltering is true)
1175+
/// - Root-level RID folder entries (e.g., win-x64/) based on the current platform's RID
11791176
/// - lib/{tfm}/ entries to only extract the best matching Target Framework Moniker
11801177
/// </summary>
11811178
private bool TryExtractToDirectory(string zipPath, string extractPath, out ErrorRecord error)
@@ -1216,10 +1213,16 @@ private bool TryExtractToDirectory(string zipPath, string extractPath, out Error
12161213
bestLibFramework = GetBestLibFramework(archive);
12171214
}
12181215

1216+
// Warn if TFM filtering is skipping assemblies for a different .NET lineage
1217+
// (e.g., installing on PS7/.NET 8 but package also has net472 for WinPS 5.1)
1218+
if (bestLibFramework != null)
1219+
{
1220+
WarnIfCrossLineageTfmSkipped(archive, bestLibFramework);
1221+
}
1222+
12191223
foreach (ZipArchiveEntry entry in archive.Entries.Where(entry => entry.CompressedLength > 0))
12201224
{
1221-
// RID filtering: skip runtimes/ entries for incompatible platforms
1222-
if (!_skipRuntimeFiltering)
1225+
// RID filtering: skip entries under incompatible platform RID folders
12231226
{
12241227
bool includeEntry = !string.IsNullOrEmpty(_runtimeIdentifier)
12251228
? RuntimePackageHelper.ShouldIncludeEntry(entry.FullName, _runtimeIdentifier)
@@ -1435,6 +1438,67 @@ private static NuGetFramework GetCurrentFramework()
14351438
return NuGetFramework.ParseFolder("netstandard2.0");
14361439
}
14371440

1441+
/// <summary>
1442+
/// Emits a warning if TFM filtering will skip assemblies for a different .NET lineage.
1443+
/// For example, when installing on PowerShell 7 (.NET Core), if the package also contains
1444+
/// .NET Framework assemblies (net472), those will be skipped — which means the module
1445+
/// won't work if later used on Windows PowerShell 5.1.
1446+
/// </summary>
1447+
private void WarnIfCrossLineageTfmSkipped(ZipArchive archive, NuGetFramework bestFramework)
1448+
{
1449+
try
1450+
{
1451+
bool bestIsNetCore = bestFramework.Framework.Equals(".NETCoreApp", StringComparison.OrdinalIgnoreCase);
1452+
bool bestIsNetFramework = bestFramework.Framework.Equals(".NETFramework", StringComparison.OrdinalIgnoreCase);
1453+
1454+
// Only warn when we're on one lineage and skipping the other
1455+
if (!bestIsNetCore && !bestIsNetFramework)
1456+
{
1457+
return;
1458+
}
1459+
1460+
// Scan lib/ folders for a TFM from the other lineage
1461+
var skippedLineageTfms = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
1462+
foreach (ZipArchiveEntry entry in archive.Entries)
1463+
{
1464+
string normalizedName = entry.FullName.Replace('\\', '/');
1465+
if (normalizedName.StartsWith("lib/", StringComparison.OrdinalIgnoreCase))
1466+
{
1467+
string[] segments = normalizedName.Split('/');
1468+
if (segments.Length >= 3 && !string.IsNullOrEmpty(segments[1]))
1469+
{
1470+
NuGetFramework entryFw = NuGetFramework.ParseFolder(segments[1]);
1471+
if (entryFw != null && !entryFw.IsUnsupported && !entryFw.Equals(bestFramework))
1472+
{
1473+
bool entryIsNetCore = entryFw.Framework.Equals(".NETCoreApp", StringComparison.OrdinalIgnoreCase);
1474+
bool entryIsNetFramework = entryFw.Framework.Equals(".NETFramework", StringComparison.OrdinalIgnoreCase);
1475+
1476+
// Detect cross-lineage: we're on Core and skipping Framework, or vice versa
1477+
if ((bestIsNetCore && entryIsNetFramework) || (bestIsNetFramework && entryIsNetCore))
1478+
{
1479+
skippedLineageTfms.Add(segments[1]);
1480+
}
1481+
}
1482+
}
1483+
}
1484+
}
1485+
1486+
if (skippedLineageTfms.Count > 0)
1487+
{
1488+
string skippedList = string.Join(", ", skippedLineageTfms);
1489+
string otherHost = bestIsNetCore ? "Windows PowerShell 5.1" : "PowerShell 7+";
1490+
_cmdletPassedIn.WriteWarning(
1491+
$"This package contains assemblies for {skippedList} which were not installed because " +
1492+
$"the current runtime selected {bestFramework.GetShortFolderName()}. " +
1493+
$"If you also use this module on {otherHost}, install it separately from that host.");
1494+
}
1495+
}
1496+
catch
1497+
{
1498+
// Non-critical warning — don't let it fail the install
1499+
}
1500+
}
1501+
14381502
/// <summary>
14391503
/// Moves package files/directories from the temp install path into the final install path location.
14401504
/// </summary>

src/code/InstallPSResource.cs

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -138,14 +138,6 @@ 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-
149141
/// <summary>
150142
/// Specifies the Runtime Identifier (RID) to filter platform-specific assets for.
151143
/// When specified, only runtime assets matching this RID are installed instead of the auto-detected platform.
@@ -626,7 +618,6 @@ private void ProcessInstallHelper(string[] pkgNames, string pkgVersion, bool pkg
626618
scope: scope,
627619
tmpPath: _tmpPath,
628620
pkgsInstalled: _packagesOnMachine,
629-
skipRuntimeFiltering: SkipRuntimeFiltering,
630621
runtimeIdentifier: RuntimeIdentifier,
631622
targetFramework: TargetFramework);
632623

src/code/InternalHooks.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,15 @@ public static bool IsCompatibleRid(string rid)
7171
#region RuntimePackageHelper Test Hooks
7272

7373
/// <summary>
74-
/// Checks if a zip entry path is in the runtimes folder.
74+
/// Checks if a folder name looks like a .NET Runtime Identifier.
75+
/// </summary>
76+
public static bool IsRidFolder(string folderName)
77+
{
78+
return RuntimePackageHelper.IsRidFolder(folderName);
79+
}
80+
81+
/// <summary>
82+
/// Checks if a zip entry path contains runtime-specific assets.
7583
/// </summary>
7684
public static bool IsRuntimesEntry(string entryFullName)
7785
{

src/code/RuntimePackageHelper.cs

Lines changed: 79 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,64 +12,119 @@ namespace Microsoft.PowerShell.PSResourceGet.UtilClasses
1212
/// <summary>
1313
/// Helper class for parsing package runtime assets and filtering during extraction.
1414
/// Provides functionality to filter runtime-specific assets based on the current platform's RID.
15+
/// Detects root-level RID folders (e.g., win-x64/native.dll) used by PowerShell modules
16+
/// with platform-specific native dependencies.
1517
/// </summary>
1618
internal static class RuntimePackageHelper
1719
{
1820
#region Constants
1921

2022
/// <summary>
21-
/// The name of the runtimes folder in NuGet packages.
23+
/// Path separator used in zip archives.
2224
/// </summary>
23-
private const string RuntimesFolderName = "runtimes";
25+
private const char ZipPathSeparator = '/';
2426

2527
/// <summary>
26-
/// Path separator used in zip archives.
28+
/// Known OS prefixes used in .NET Runtime Identifiers.
2729
/// </summary>
28-
private const char ZipPathSeparator = '/';
30+
private static readonly string[] s_knownOsPrefixes = new[]
31+
{
32+
"win", "linux", "osx", "unix", "maccatalyst", "browser"
33+
};
34+
35+
/// <summary>
36+
/// Known architectures used in .NET Runtime Identifiers.
37+
/// </summary>
38+
private static readonly string[] s_knownArchitectures = new[]
39+
{
40+
"loongarch64", "ppc64le", "mips64", "s390x", "arm64", "armel", "wasm", "arm", "x64", "x86"
41+
};
2942

3043
#endregion
3144

3245
#region Public Methods
3346

3447
/// <summary>
35-
/// Checks if a zip entry path is within the runtimes folder.
48+
/// Checks if a folder name looks like a .NET Runtime Identifier.
49+
/// Matches patterns like: win-x64, linux-arm64, osx-arm64, linux-musl-x64, etc.
50+
/// </summary>
51+
/// <param name="folderName">The folder name to check.</param>
52+
/// <returns>True if the folder name matches a RID pattern; otherwise, false.</returns>
53+
public static bool IsRidFolder(string folderName)
54+
{
55+
if (string.IsNullOrEmpty(folderName) || !folderName.Contains("-"))
56+
{
57+
return false;
58+
}
59+
60+
// Must start with a known OS prefix
61+
bool startsWithKnownOs = false;
62+
foreach (string prefix in s_knownOsPrefixes)
63+
{
64+
if (folderName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
65+
{
66+
startsWithKnownOs = true;
67+
break;
68+
}
69+
}
70+
71+
if (!startsWithKnownOs)
72+
{
73+
return false;
74+
}
75+
76+
// Must end with a known architecture
77+
string[] parts = folderName.Split('-');
78+
string lastPart = parts[parts.Length - 1];
79+
foreach (string arch in s_knownArchitectures)
80+
{
81+
if (string.Equals(lastPart, arch, StringComparison.OrdinalIgnoreCase))
82+
{
83+
return true;
84+
}
85+
}
86+
87+
return false;
88+
}
89+
90+
/// <summary>
91+
/// Checks if a zip entry path is under a root-level RID folder.
92+
/// Detects entries like win-x64/native.dll, linux-arm64/libfoo.so, etc.
3693
/// </summary>
3794
/// <param name="entryFullName">The full path of the zip entry.</param>
38-
/// <returns>True if the entry is in the runtimes folder; otherwise, false.</returns>
95+
/// <returns>True if the entry is under a RID folder; otherwise, false.</returns>
3996
public static bool IsRuntimesEntry(string entryFullName)
4097
{
4198
if (string.IsNullOrEmpty(entryFullName))
4299
{
43100
return false;
44101
}
45102

46-
// Normalize path separators for comparison
47103
string normalizedPath = entryFullName.Replace('\\', ZipPathSeparator);
48-
49-
return normalizedPath.StartsWith(RuntimesFolderName + ZipPathSeparator, StringComparison.OrdinalIgnoreCase);
104+
string[] segments = normalizedPath.Split(ZipPathSeparator);
105+
106+
// Pattern: {rid}/... (root-level RID folders like win-x64/native.dll)
107+
return segments.Length >= 2 && IsRidFolder(segments[0]);
50108
}
51109

52110
/// <summary>
53-
/// Extracts the RID from a runtimes folder entry path.
111+
/// Extracts the RID from a root-level RID folder entry path.
54112
/// </summary>
55-
/// <param name="entryFullName">The full path of the zip entry (e.g., "runtimes/win-x64/native/file.dll").</param>
56-
/// <returns>The RID (e.g., "win-x64"), or null if not a runtimes entry.</returns>
113+
/// <param name="entryFullName">The full path of the zip entry (e.g., "win-x64/native.dll").</param>
114+
/// <returns>The RID (e.g., "win-x64"), or null if not under a RID folder.</returns>
57115
public static string GetRidFromRuntimesEntry(string entryFullName)
58116
{
59-
if (!IsRuntimesEntry(entryFullName))
117+
if (string.IsNullOrEmpty(entryFullName))
60118
{
61119
return null;
62120
}
63121

64-
// Normalize path separators
65122
string normalizedPath = entryFullName.Replace('\\', ZipPathSeparator);
66-
67-
// Path format: runtimes/{rid}/...
68123
string[] parts = normalizedPath.Split(ZipPathSeparator);
69-
70-
if (parts.Length >= 2)
124+
125+
if (parts.Length >= 2 && IsRidFolder(parts[0]))
71126
{
72-
return parts[1]; // The RID is the second segment
127+
return parts[0];
73128
}
74129

75130
return null;
@@ -82,21 +137,18 @@ public static string GetRidFromRuntimesEntry(string entryFullName)
82137
/// <returns>True if the entry should be included; otherwise, false.</returns>
83138
public static bool ShouldIncludeEntry(string entryFullName)
84139
{
85-
// Non-runtimes entries are always included
86140
if (!IsRuntimesEntry(entryFullName))
87141
{
88142
return true;
89143
}
90144

91145
string entryRid = GetRidFromRuntimesEntry(entryFullName);
92-
146+
93147
if (string.IsNullOrEmpty(entryRid))
94148
{
95-
// If we can't determine the RID, include the entry to be safe
96149
return true;
97150
}
98151

99-
// Check if this RID is compatible with the current platform
100152
return RuntimeIdentifierHelper.IsCompatibleRid(entryRid);
101153
}
102154

@@ -109,25 +161,24 @@ public static bool ShouldIncludeEntry(string entryFullName)
109161
/// <returns>True if the entry should be included; otherwise, false.</returns>
110162
public static bool ShouldIncludeEntry(string entryFullName, string targetRid)
111163
{
112-
// Non-runtimes entries are always included
113164
if (!IsRuntimesEntry(entryFullName))
114165
{
115166
return true;
116167
}
117168

118169
string entryRid = GetRidFromRuntimesEntry(entryFullName);
119-
170+
120171
if (string.IsNullOrEmpty(entryRid))
121172
{
122173
return true;
123174
}
124175

125-
// Check if this RID is compatible with the specified target
126176
return RuntimeIdentifierHelper.IsCompatibleRid(entryRid, targetRid);
127177
}
128178

129179
/// <summary>
130-
/// Gets a list of all unique RIDs present in a zip archive's runtimes folder.
180+
/// Gets a list of all unique RIDs present in a zip archive.
181+
/// Detects both runtimes/{rid}/ and root-level {rid}/ patterns.
131182
/// </summary>
132183
/// <param name="archive">The zip archive to scan.</param>
133184
/// <returns>A list of unique RIDs found in the archive.</returns>
@@ -153,7 +204,7 @@ public static IReadOnlyList<string> GetAvailableRidsFromArchive(ZipArchive archi
153204
}
154205

155206
/// <summary>
156-
/// Gets a list of all unique RIDs present in a zip file's runtimes folder.
207+
/// Gets a list of all unique RIDs present in a zip file.
157208
/// </summary>
158209
/// <param name="zipPath">The path to the zip file.</param>
159210
/// <returns>A list of unique RIDs found in the archive.</returns>

0 commit comments

Comments
 (0)