Skip to content

Commit d2cee50

Browse files
Justin ChungCopilot
andcommitted
Fix platform-aware install regressions
Support root and NuGet RID layouts, scope filtered-content merging per install work item, make the platform tests cross-edition and architecture-safe, and fall back to unauthenticated DSC release lookup for fork builds. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 7c397b2 commit d2cee50

5 files changed

Lines changed: 78 additions & 38 deletions

File tree

‎.ci/test.yml‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,13 @@ jobs:
107107
"Authorization" = "Bearer $env:GITHUB_TOKEN"
108108
}
109109
110+
# Secret variables are not exposed to fork PR builds. Use the public API without
111+
# authentication in that case instead of sending an invalid Authorization header.
112+
if ([string]::IsNullOrWhiteSpace($env:GITHUB_TOKEN) -or
113+
$env:GITHUB_TOKEN -match '^\$\([^)]+\)$') {
114+
$null = $headers.Remove("Authorization")
115+
}
116+
110117
# Get releases
111118
$releases = Invoke-RestMethod -Uri $api -Headers $headers -RetryIntervalSec 30 -MaximumRetryCount 10
112119

‎src/code/InstallHelper.cs‎

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ internal class InstallHelper
5656
private bool _savePkg;
5757
private string _runtimeIdentifier;
5858
private string _targetFramework;
59-
private bool _mergeFilteredContent;
6059
List<string> _pathsToSearch;
6160
List<string> _pkgNamesToInstall;
6261
private string _tmpPath;
@@ -410,7 +409,8 @@ private void MoveFilesIntoInstallPath(
410409
string installPath,
411410
string newVersion,
412411
string moduleManifestVersion,
413-
string scriptPath)
412+
string scriptPath,
413+
bool mergeFilteredContent)
414414
{
415415
_cmdletPassedIn.WriteDebug("In InstallHelper::MoveFilesIntoInstallPath()");
416416
// Creating the proper installation path depending on whether pkg is a module or script
@@ -433,7 +433,7 @@ private void MoveFilesIntoInstallPath(
433433
Directory.CreateDirectory(newPathParent);
434434
Utils.MoveDirectory(tempModuleVersionDir, finalModuleVersionDir);
435435
}
436-
else if (_mergeFilteredContent && Directory.Exists(finalModuleVersionDir))
436+
else if (mergeFilteredContent && Directory.Exists(finalModuleVersionDir))
437437
{
438438
// Copy only new TFM/RID content into the existing install
439439
_cmdletPassedIn.WriteVerbose($"Merging additional platform content from '{tempModuleVersionDir}' into '{finalModuleVersionDir}'");
@@ -570,6 +570,8 @@ private List<PSResourceInfo> InstallPackages(
570570
continue;
571571
}
572572

573+
bool mergeFilteredContent = false;
574+
573575
// Check to see if the pkg is already installed (unless -Reinstall was specified).
574576
if (!_reinstall && _packagesOnMachine.Contains($"{pkgToInstall.Name}{pkgVersion}"))
575577
{
@@ -580,7 +582,7 @@ private List<PSResourceInfo> InstallPackages(
580582
{
581583
_cmdletPassedIn.WriteVerbose($"Resource '{pkgToInstall.Name}' with version '{pkgVersion}' is already installed. " +
582584
$"Proceeding to merge additional platform content (TargetFramework='{_targetFramework}', RuntimeIdentifier='{_runtimeIdentifier}').");
583-
_mergeFilteredContent = true;
585+
mergeFilteredContent = true;
584586
}
585587
else
586588
{
@@ -606,7 +608,8 @@ private List<PSResourceInfo> InstallPackages(
606608
{
607609
PkgToInstall = pkgToInstall,
608610
PkgVersion = pkgVersion,
609-
TempInstallPath = CreateInstallationTempPath()
611+
TempInstallPath = CreateInstallationTempPath(),
612+
MergeFilteredContent = mergeFilteredContent
610613
});
611614
}
612615

@@ -659,7 +662,11 @@ private List<PSResourceInfo> InstallPackages(
659662

660663
// Parent package and dependencies are now installed to temp directory.
661664
// Try to move all package directories from temp directory to final destination.
662-
if (!TryMoveInstallContent(workItem.TempInstallPath, scope, workItem.PackagesHash))
665+
if (!TryMoveInstallContent(
666+
workItem.TempInstallPath,
667+
scope,
668+
workItem.PackagesHash,
669+
workItem.MergeFilteredContent))
663670
{
664671
_cmdletPassedIn.WriteError(new ErrorRecord(
665672
new InvalidOperationException(),
@@ -710,6 +717,7 @@ private sealed class ParentInstallWorkItem
710717
public string TempInstallPath;
711718
public ConcurrentDictionary<string, Hashtable> PackagesHash;
712719
public bool Succeeded;
720+
public bool MergeFilteredContent;
713721
public readonly ConcurrentQueue<ErrorRecord> ErrorMsgs = new();
714722
public readonly ConcurrentQueue<string> WarningMsgs = new();
715723
public readonly ConcurrentQueue<string> DebugMsgs = new();
@@ -1410,16 +1418,12 @@ private bool TryExtractToDirectory(string zipPath, string extractPath, out Error
14101418

14111419
if (!string.IsNullOrEmpty(_runtimeIdentifier))
14121420
{
1413-
bool hasMatchingRid = archive.Entries.Any(e =>
1414-
RuntimePackageHelper.IsRidFolder(e.FullName.Replace('\\', '/').Split('/')[0]) &&
1415-
RuntimePackageHelper.ShouldIncludeEntry(e.FullName, _runtimeIdentifier));
1421+
IReadOnlyList<string> availableRids = RuntimePackageHelper.GetAvailableRidsFromArchive(archive);
1422+
bool hasMatchingRid = availableRids.Any(rid =>
1423+
RuntimeIdentifierHelper.IsCompatibleRid(rid, _runtimeIdentifier));
14161424

14171425
if (!hasMatchingRid)
14181426
{
1419-
var availableRids = archive.Entries
1420-
.Select(e => e.FullName.Replace('\\', '/').Split('/')[0])
1421-
.Where(f => RuntimePackageHelper.IsRidFolder(f))
1422-
.Distinct(StringComparer.OrdinalIgnoreCase);
14231427
string available = string.Join(", ", availableRids);
14241428
_cmdletPassedIn.WriteWarning(
14251429
$"The specified RuntimeIdentifier '{_runtimeIdentifier}' was not found in this package. " +
@@ -1662,7 +1666,11 @@ private void WarnIfCrossLineageTfmSkipped(ZipArchive archive, NuGetFramework bes
16621666
/// <summary>
16631667
/// Moves package files/directories from the temp install path into the final install path location.
16641668
/// </summary>
1665-
private bool TryMoveInstallContent(string tempInstallPath, ScopeType scope, ConcurrentDictionary<string, Hashtable> packagesHash)
1669+
private bool TryMoveInstallContent(
1670+
string tempInstallPath,
1671+
ScopeType scope,
1672+
ConcurrentDictionary<string, Hashtable> packagesHash,
1673+
bool mergeFilteredContent)
16661674
{
16671675
_cmdletPassedIn.WriteDebug("In InstallHelper::TryMoveInstallContent()");
16681676
foreach (string pkgName in packagesHash.Keys)
@@ -1687,7 +1695,8 @@ private bool TryMoveInstallContent(string tempInstallPath, ScopeType scope, Conc
16871695
installPath,
16881696
newVersion: pkgVersion, // would not have prerelease label in this string
16891697
moduleManifestVersion: pkgVersion,
1690-
scriptPath);
1698+
scriptPath,
1699+
mergeFilteredContent);
16911700

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

‎src/code/RuntimePackageHelper.cs‎

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ namespace Microsoft.PowerShell.PSResourceGet.UtilClasses
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.
1515
/// Detects root-level RID folders (e.g., win-x64/native.dll) used by PowerShell modules
16-
/// with platform-specific native dependencies.
16+
/// and standard NuGet runtime folders (e.g., runtimes/win-x64/native.dll).
1717
/// </summary>
1818
internal static class RuntimePackageHelper
1919
{
@@ -88,30 +88,21 @@ public static bool IsRidFolder(string folderName)
8888
}
8989

9090
/// <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.
91+
/// Checks if a zip entry path is under a root-level RID folder or a standard NuGet runtimes folder.
92+
/// Detects entries like win-x64/native.dll and runtimes/linux-arm64/native/libfoo.so.
9393
/// </summary>
9494
/// <param name="entryFullName">The full path of the zip entry.</param>
9595
/// <returns>True if the entry is under a RID folder; otherwise, false.</returns>
9696
public static bool IsRuntimesEntry(string entryFullName)
9797
{
98-
if (string.IsNullOrEmpty(entryFullName))
99-
{
100-
return false;
101-
}
102-
103-
string normalizedPath = entryFullName.Replace('\\', ZipPathSeparator);
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]);
98+
return !string.IsNullOrEmpty(GetRidFromRuntimesEntry(entryFullName));
10899
}
109100

110101
/// <summary>
111-
/// Extracts the RID from a root-level RID folder entry path.
102+
/// Extracts the RID from a root-level RID folder or standard NuGet runtimes folder entry path.
112103
/// </summary>
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>
104+
/// <param name="entryFullName">The full path of the zip entry (e.g., "win-x64/native.dll" or "runtimes/win-x64/native.dll").</param>
105+
/// <returns>The RID (e.g., "win-x64"), or null if not under a supported RID folder layout.</returns>
115106
public static string GetRidFromRuntimesEntry(string entryFullName)
116107
{
117108
if (string.IsNullOrEmpty(entryFullName))
@@ -122,6 +113,15 @@ public static string GetRidFromRuntimesEntry(string entryFullName)
122113
string normalizedPath = entryFullName.Replace('\\', ZipPathSeparator);
123114
string[] parts = normalizedPath.Split(ZipPathSeparator);
124115

116+
// Pattern: runtimes/{rid}/... (standard NuGet runtime asset layout)
117+
if (parts.Length >= 3 &&
118+
string.Equals(parts[0], "runtimes", StringComparison.OrdinalIgnoreCase) &&
119+
!string.IsNullOrEmpty(parts[1]))
120+
{
121+
return parts[1];
122+
}
123+
124+
// Pattern: {rid}/... (root-level RID folders used by PowerShell modules)
125125
if (parts.Length >= 2 && IsRidFolder(parts[0]))
126126
{
127127
return parts[0];

‎test/PlatformFilteringTests/PlatformAwareInstall.Tests.ps1‎

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,13 @@ $depEntriesXml </group>
120120
function Get-VersionInstallPath {
121121
param([object]$PkgInfo)
122122
$base = $PkgInfo.InstalledLocation
123-
$versionPath = Join-Path $base $PkgInfo.Name $PkgInfo.Version.ToString()
123+
$version = $PkgInfo.Version.ToString()
124+
$namePath = Join-Path -Path $base -ChildPath $PkgInfo.Name
125+
$versionPath = Join-Path -Path $namePath -ChildPath $version
124126
if (Test-Path $versionPath) { return $versionPath }
125127
# Maybe InstalledLocation already points to name/version
126-
if ($base -match "$([regex]::Escape($PkgInfo.Name))[\\/]$([regex]::Escape($PkgInfo.Version.ToString()))$") { return $base }
128+
if ($base -match "$([regex]::Escape($PkgInfo.Name))[\\/]$([regex]::Escape($version))$") { return $base }
127129
# Try name folder only
128-
$namePath = Join-Path $base $PkgInfo.Name
129130
if (Test-Path $namePath) { return $namePath }
130131
return $base
131132
}
@@ -494,10 +495,19 @@ $depEntriesXml </group>
494495
BeforeAll {
495496
$script:ridMergePkgName = 'TestRidMergeModule'
496497
$script:ridMergePkgVersion = '1.0.0'
498+
$script:ridMergeForeignRid = if ($script:currentRid -match '^win') {
499+
'linux-x64'
500+
}
501+
elseif ($script:currentRid -match '^osx') {
502+
'win-x64'
503+
}
504+
else {
505+
'osx-arm64'
506+
}
497507

498508
New-TestNupkg -Name $ridMergePkgName -Version $ridMergePkgVersion `
499509
-OutputDir $localRepoDir `
500-
-RuntimeIdentifiers @('win-x64', 'linux-x64', 'osx-arm64') `
510+
-RuntimeIdentifiers @($script:currentRid, $script:ridMergeForeignRid) `
501511
-LibTfms @('netstandard2.0') `
502512
-IncludeModuleManifest
503513
}
@@ -514,8 +524,7 @@ $depEntriesXml </group>
514524

515525
$installPath = Get-VersionInstallPath $installed
516526

517-
# Pick a foreign RID
518-
$foreignRid = if ($IsWindows) { 'linux-x64' } elseif ($IsMacOS) { 'win-x64' } else { 'osx-arm64' }
527+
$foreignRid = $script:ridMergeForeignRid
519528

520529
# Verify foreign RID is not present yet
521530
Test-Path (Join-Path $installPath $foreignRid) | Should -BeFalse

‎test/PlatformFilteringTests/RuntimePackageHelper.Tests.ps1‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ Describe 'RuntimePackageHelper Tests' -tags 'CI' {
2020
$InternalHooks::IsRuntimesEntry('runtimes/win-x64/native/file.dll') | Should -BeTrue
2121
}
2222

23+
It "Should return true for a root-level RID path" {
24+
$InternalHooks::IsRuntimesEntry('win-x64/native/file.dll') | Should -BeTrue
25+
}
26+
2327
It "Should return true for runtimes/ path with backslashes" {
2428
$InternalHooks::IsRuntimesEntry('runtimes\win-x64\native\file.dll') | Should -BeTrue
2529
}
@@ -52,6 +56,10 @@ Describe 'RuntimePackageHelper Tests' -tags 'CI' {
5256
$InternalHooks::GetRidFromRuntimesEntry('runtimes/win-x64/native/file.dll') | Should -Be 'win-x64'
5357
}
5458

59+
It "Should extract RID from a root-level RID path" {
60+
$InternalHooks::GetRidFromRuntimesEntry('win-x64/native/file.dll') | Should -Be 'win-x64'
61+
}
62+
5563
It "Should extract RID for linux-musl-x64" {
5664
$InternalHooks::GetRidFromRuntimesEntry('runtimes/linux-musl-x64/lib/file.dll') | Should -Be 'linux-musl-x64'
5765
}
@@ -80,6 +88,7 @@ Describe 'RuntimePackageHelper Tests' -tags 'CI' {
8088
It "Should include runtimes entry for current platform" {
8189
$currentRid = $InternalHooks::GetCurrentRuntimeIdentifier()
8290
$InternalHooks::ShouldIncludeEntry("runtimes/$currentRid/native/file.dll") | Should -BeTrue
91+
$InternalHooks::ShouldIncludeEntry("$currentRid/native/file.dll") | Should -BeTrue
8392
}
8493

8594
It "Should include runtimes entry for 'any' RID" {
@@ -119,6 +128,11 @@ Describe 'RuntimePackageHelper Tests' -tags 'CI' {
119128
Set-Content -Path (Join-Path $ridNativeDir "test.dll") -Value "dummy"
120129
}
121130

131+
# Add a root-level RID folder to verify both supported layouts are discovered.
132+
$rootRidDir = Join-Path $testZipDir 'linux-s390x'
133+
$null = New-Item $rootRidDir -ItemType Directory -Force
134+
Set-Content -Path (Join-Path $rootRidDir 'test.dll') -Value 'dummy'
135+
122136
# Also create a non-runtimes file
123137
Set-Content -Path (Join-Path $testZipDir "MyModule.psd1") -Value "dummy"
124138

@@ -129,10 +143,11 @@ Describe 'RuntimePackageHelper Tests' -tags 'CI' {
129143

130144
It "Should list all RIDs present in the zip" {
131145
$availableRids = $InternalHooks::GetAvailableRidsFromZipFile($testZipPath)
132-
$availableRids.Count | Should -Be 6
146+
$availableRids.Count | Should -Be 7
133147
$availableRids | Should -Contain 'win-x64'
134148
$availableRids | Should -Contain 'linux-x64'
135149
$availableRids | Should -Contain 'osx-arm64'
150+
$availableRids | Should -Contain 'linux-s390x'
136151
}
137152

138153
It "Should throw for non-existent file" {

0 commit comments

Comments
 (0)