Skip to content

Commit 19da4cb

Browse files
author
Justin Chung
committed
Add merging for when same module, same version exists but different TFM or RID is specified
1 parent 9a44e6f commit 19da4cb

3 files changed

Lines changed: 270 additions & 5 deletions

File tree

src/code/InstallHelper.cs

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ internal class InstallHelper
5151
private bool _savePkg;
5252
private string _runtimeIdentifier;
5353
private string _targetFramework;
54+
private bool _mergeFilteredContent;
5455
List<string> _pathsToSearch;
5556
List<string> _pkgNamesToInstall;
5657
private string _tmpPath;
@@ -424,6 +425,13 @@ private void MoveFilesIntoInstallPath(
424425
Directory.CreateDirectory(newPathParent);
425426
Utils.MoveDirectory(tempModuleVersionDir, finalModuleVersionDir);
426427
}
428+
else if (_mergeFilteredContent && Directory.Exists(finalModuleVersionDir))
429+
{
430+
// Copy only new TFM/RID content into the existing install
431+
_cmdletPassedIn.WriteVerbose($"Merging additional platform content from '{tempModuleVersionDir}' into '{finalModuleVersionDir}'");
432+
Utils.MergeDirContents(tempModuleVersionDir, finalModuleVersionDir);
433+
Utils.DeleteDirectory(tempModuleVersionDir);
434+
}
427435
else
428436
{
429437
_cmdletPassedIn.WriteVerbose($"Temporary module version directory is: '{tempModuleVersionDir}'");
@@ -801,12 +809,24 @@ private Hashtable BeginPackageInstall(
801809
string currPkgNameVersion = $"{pkgToInstall.Name}{pkgToInstall.Version}";
802810
if (_packagesOnMachine.Contains(currPkgNameVersion))
803811
{
804-
_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");
812+
// When -TargetFramework or -RuntimeIdentifier is explicitly specified, allow re-download
813+
// to merge the additional TFM/RID content into the existing install directory.
814+
bool hasExplicitOverride = !string.IsNullOrEmpty(_targetFramework) || !string.IsNullOrEmpty(_runtimeIdentifier);
815+
if (hasExplicitOverride)
816+
{
817+
_cmdletPassedIn.WriteVerbose($"Resource '{pkgToInstall.Name}' with version '{pkgVersion}' is already installed. " +
818+
$"Proceeding to merge additional platform content (TargetFramework='{_targetFramework}', RuntimeIdentifier='{_runtimeIdentifier}').");
819+
_mergeFilteredContent = true;
820+
}
821+
else
822+
{
823+
_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");
805824

806-
// Remove from tracking list of packages to install.
807-
_pkgNamesToInstall.RemoveAll(x => x.Equals(pkgToInstall.Name, StringComparison.InvariantCultureIgnoreCase));
825+
// Remove from tracking list of packages to install.
826+
_pkgNamesToInstall.RemoveAll(x => x.Equals(pkgToInstall.Name, StringComparison.InvariantCultureIgnoreCase));
808827

809-
return packagesHash;
828+
return packagesHash;
829+
}
810830
}
811831
}
812832

@@ -1267,6 +1287,54 @@ private bool TryExtractToDirectory(string zipPath, string extractPath, out Error
12671287
entry.ExtractToFile(destinationPath, overwrite: true);
12681288
}
12691289
}
1290+
1291+
// Warn if an explicitly specified TFM or RID had no matching entries in the package
1292+
if (!string.IsNullOrEmpty(_targetFramework) && bestLibFramework != null)
1293+
{
1294+
bool hasMatchingLib = archive.Entries.Any(e =>
1295+
{
1296+
string n = e.FullName.Replace('\\', '/');
1297+
if (!n.StartsWith("lib/", StringComparison.OrdinalIgnoreCase)) return false;
1298+
string[] s = n.Split('/');
1299+
if (s.Length < 3 || string.IsNullOrEmpty(s[1])) return false;
1300+
NuGetFramework fw = NuGetFramework.ParseFolder(s[1]);
1301+
return fw != null && !fw.IsUnsupported && fw.Equals(bestLibFramework);
1302+
});
1303+
1304+
if (!hasMatchingLib)
1305+
{
1306+
var availableTfms = archive.Entries
1307+
.Select(e => e.FullName.Replace('\\', '/'))
1308+
.Where(n => n.StartsWith("lib/", StringComparison.OrdinalIgnoreCase))
1309+
.Select(n => n.Split('/'))
1310+
.Where(s => s.Length >= 3 && !string.IsNullOrEmpty(s[1]))
1311+
.Select(s => s[1])
1312+
.Distinct(StringComparer.OrdinalIgnoreCase);
1313+
string available = string.Join(", ", availableTfms);
1314+
_cmdletPassedIn.WriteWarning(
1315+
$"The specified TargetFramework '{_targetFramework}' was not found in this package. " +
1316+
$"No lib/ assemblies were installed. Available TFMs: {(string.IsNullOrEmpty(available) ? "none" : available)}");
1317+
}
1318+
}
1319+
1320+
if (!string.IsNullOrEmpty(_runtimeIdentifier))
1321+
{
1322+
bool hasMatchingRid = archive.Entries.Any(e =>
1323+
RuntimePackageHelper.IsRidFolder(e.FullName.Replace('\\', '/').Split('/')[0]) &&
1324+
RuntimePackageHelper.ShouldIncludeEntry(e.FullName, _runtimeIdentifier));
1325+
1326+
if (!hasMatchingRid)
1327+
{
1328+
var availableRids = archive.Entries
1329+
.Select(e => e.FullName.Replace('\\', '/').Split('/')[0])
1330+
.Where(f => RuntimePackageHelper.IsRidFolder(f))
1331+
.Distinct(StringComparer.OrdinalIgnoreCase);
1332+
string available = string.Join(", ", availableRids);
1333+
_cmdletPassedIn.WriteWarning(
1334+
$"The specified RuntimeIdentifier '{_runtimeIdentifier}' was not found in this package. " +
1335+
$"No platform-specific assets were installed. Available RIDs: {(string.IsNullOrEmpty(available) ? "none" : available)}");
1336+
}
1337+
}
12701338
}
12711339
}
12721340
catch (Exception e)
@@ -1487,10 +1555,11 @@ private void WarnIfCrossLineageTfmSkipped(ZipArchive archive, NuGetFramework bes
14871555
{
14881556
string skippedList = string.Join(", ", skippedLineageTfms);
14891557
string otherHost = bestIsNetCore ? "Windows PowerShell 5.1" : "PowerShell 7+";
1558+
string skippedTfm = skippedLineageTfms.First();
14901559
_cmdletPassedIn.WriteWarning(
14911560
$"This package contains assemblies for {skippedList} which were not installed because " +
14921561
$"the current runtime selected {bestFramework.GetShortFolderName()}. " +
1493-
$"If you also use this module on {otherHost}, install it separately from that host.");
1562+
$"If you also use this module on {otherHost}, run: Install-PSResource -Name <ModuleName> -TargetFramework '{skippedTfm}'");
14941563
}
14951564
}
14961565
catch

src/code/Utils.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1760,6 +1760,34 @@ public static void MoveDirectory(
17601760
DeleteDirectory(sourceDirPath);
17611761
}
17621762

1763+
/// <summary>
1764+
/// Merges the contents of a source directory into an existing destination directory.
1765+
/// Only adds new files and subdirectories; does not delete or overwrite existing content.
1766+
/// Used when merging additional TFM or RID content into an already-installed module.
1767+
/// </summary>
1768+
public static void MergeDirContents(string sourceDirPath, string destDirPath)
1769+
{
1770+
if (!Directory.Exists(destDirPath))
1771+
{
1772+
Directory.CreateDirectory(destDirPath);
1773+
}
1774+
1775+
foreach (var filePath in Directory.GetFiles(sourceDirPath))
1776+
{
1777+
var destFilePath = Path.Combine(destDirPath, Path.GetFileName(filePath));
1778+
if (!File.Exists(destFilePath))
1779+
{
1780+
File.Copy(filePath, destFilePath);
1781+
}
1782+
}
1783+
1784+
foreach (var srcSubDirPath in Directory.GetDirectories(sourceDirPath))
1785+
{
1786+
var destSubDirPath = Path.Combine(destDirPath, Path.GetFileName(srcSubDirPath));
1787+
MergeDirContents(srcSubDirPath, destSubDirPath);
1788+
}
1789+
}
1790+
17631791
private static void CopyDirContents(
17641792
string sourceDirPath,
17651793
string destDirPath,

test/PlatformFilteringTests/PlatformAwareInstall.Tests.ps1

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,12 @@ $depEntriesXml </group>
134134
$script:localRepoDir = Join-Path $TestDrive 'platformFilterRepo'
135135
$null = New-Item $localRepoDir -ItemType Directory -Force
136136

137+
# Import the local build of the module (handles PS 7.6+ where it ships built-in)
138+
$outModulePath = Join-Path $PSScriptRoot '../../out/Microsoft.PowerShell.PSResourceGet'
139+
if (Test-Path $outModulePath) {
140+
Import-Module $outModulePath -Force
141+
}
142+
137143
$script:localRepoName = 'PlatformFilterTestRepo'
138144
Register-PSResourceRepository -Name $localRepoName -Uri $localRepoDir -Trusted -Force -ErrorAction SilentlyContinue
139145

@@ -396,4 +402,166 @@ $depEntriesXml </group>
396402
}
397403
}
398404
}
405+
406+
407+
Context 'TFM Merge Install (already installed, explicit -TargetFramework)' {
408+
409+
BeforeAll {
410+
$script:tfmMergePkgName = 'TestTfmMergeModule'
411+
$script:tfmMergePkgVersion = '1.0.0'
412+
413+
New-TestNupkg -Name $tfmMergePkgName -Version $tfmMergePkgVersion `
414+
-OutputDir $localRepoDir `
415+
-LibTfms @('net472', 'net8.0') `
416+
-IncludeModuleManifest
417+
}
418+
419+
AfterEach {
420+
Uninstall-PSResource $tfmMergePkgName -Version "*" -ErrorAction SilentlyContinue
421+
}
422+
423+
It "Should merge net472 into an existing net8.0 install" -Skip:($PSVersionTable.PSVersion.Major -le 5) {
424+
# Step 1: Install normally — should select net8.0 on PS7+
425+
Install-PSResource -Name $tfmMergePkgName -Repository $localRepoName -TrustRepository -Version $tfmMergePkgVersion
426+
$installed = Get-InstalledPSResource -Name $tfmMergePkgName
427+
$installed | Should -Not -BeNullOrEmpty
428+
429+
$installPath = Get-VersionInstallPath $installed
430+
$libDir = Join-Path $installPath 'lib'
431+
432+
# Verify only net8.0 was installed
433+
Test-Path (Join-Path $libDir 'net8.0') | Should -BeTrue
434+
Test-Path (Join-Path $libDir 'net472') | Should -BeFalse
435+
436+
# Step 2: Merge net472 using -TargetFramework
437+
Install-PSResource -Name $tfmMergePkgName -Repository $localRepoName -TrustRepository -Version $tfmMergePkgVersion -TargetFramework 'net472'
438+
439+
# Verify both TFMs are now present
440+
Test-Path (Join-Path $libDir 'net8.0') | Should -BeTrue
441+
Test-Path (Join-Path $libDir 'net472') | Should -BeTrue
442+
443+
# Verify the assembly file exists in the merged folder
444+
Test-Path (Join-Path $libDir "net472/$tfmMergePkgName.dll") | Should -BeTrue
445+
}
446+
447+
It "Should merge net8.0 into an existing net472 install" -Skip:($PSVersionTable.PSVersion.Major -gt 5) {
448+
# Step 1: Install normally on WinPS — should select net472
449+
Install-PSResource -Name $tfmMergePkgName -Repository $localRepoName -TrustRepository -Version $tfmMergePkgVersion
450+
$installed = Get-InstalledPSResource -Name $tfmMergePkgName
451+
$installed | Should -Not -BeNullOrEmpty
452+
453+
$installPath = Get-VersionInstallPath $installed
454+
$libDir = Join-Path $installPath 'lib'
455+
456+
# Verify only net472 was installed
457+
Test-Path (Join-Path $libDir 'net472') | Should -BeTrue
458+
Test-Path (Join-Path $libDir 'net8.0') | Should -BeFalse
459+
460+
# Step 2: Merge net8.0 using -TargetFramework
461+
Install-PSResource -Name $tfmMergePkgName -Repository $localRepoName -TrustRepository -Version $tfmMergePkgVersion -TargetFramework 'net8.0'
462+
463+
# Verify both TFMs are now present
464+
Test-Path (Join-Path $libDir 'net472') | Should -BeTrue
465+
Test-Path (Join-Path $libDir 'net8.0') | Should -BeTrue
466+
}
467+
468+
It "Should not overwrite existing files during merge" -Skip:($PSVersionTable.PSVersion.Major -le 5) {
469+
# Step 1: Install normally — gets net8.0
470+
Install-PSResource -Name $tfmMergePkgName -Repository $localRepoName -TrustRepository -Version $tfmMergePkgVersion
471+
472+
$installed = Get-InstalledPSResource -Name $tfmMergePkgName
473+
$installPath = Get-VersionInstallPath $installed
474+
$psd1Path = Join-Path $installPath "$tfmMergePkgName.psd1"
475+
476+
# Record the original .psd1 write time
477+
$originalWriteTime = (Get-Item $psd1Path).LastWriteTime
478+
479+
# Small delay to ensure timestamp difference
480+
Start-Sleep -Milliseconds 100
481+
482+
# Step 2: Merge net472
483+
Install-PSResource -Name $tfmMergePkgName -Repository $localRepoName -TrustRepository -Version $tfmMergePkgVersion -TargetFramework 'net472'
484+
485+
# The .psd1 should NOT have been overwritten
486+
$newWriteTime = (Get-Item $psd1Path).LastWriteTime
487+
$newWriteTime | Should -Be $originalWriteTime
488+
}
489+
}
490+
491+
492+
Context 'RID Merge Install (already installed, explicit -RuntimeIdentifier)' {
493+
494+
BeforeAll {
495+
$script:ridMergePkgName = 'TestRidMergeModule'
496+
$script:ridMergePkgVersion = '1.0.0'
497+
498+
New-TestNupkg -Name $ridMergePkgName -Version $ridMergePkgVersion `
499+
-OutputDir $localRepoDir `
500+
-RuntimeIdentifiers @('win-x64', 'linux-x64', 'osx-arm64') `
501+
-LibTfms @('netstandard2.0') `
502+
-IncludeModuleManifest
503+
}
504+
505+
AfterEach {
506+
Uninstall-PSResource $ridMergePkgName -Version "*" -ErrorAction SilentlyContinue
507+
}
508+
509+
It "Should merge a foreign RID into an existing install" {
510+
# Step 1: Install normally — gets current platform RID
511+
Install-PSResource -Name $ridMergePkgName -Repository $localRepoName -TrustRepository -Version $ridMergePkgVersion
512+
$installed = Get-InstalledPSResource -Name $ridMergePkgName
513+
$installed | Should -Not -BeNullOrEmpty
514+
515+
$installPath = Get-VersionInstallPath $installed
516+
517+
# Pick a foreign RID
518+
$foreignRid = if ($IsWindows) { 'linux-x64' } elseif ($IsMacOS) { 'win-x64' } else { 'osx-arm64' }
519+
520+
# Verify foreign RID is not present yet
521+
Test-Path (Join-Path $installPath $foreignRid) | Should -BeFalse
522+
523+
# Step 2: Merge foreign RID
524+
Install-PSResource -Name $ridMergePkgName -Repository $localRepoName -TrustRepository -Version $ridMergePkgVersion -RuntimeIdentifier $foreignRid
525+
526+
# Verify both RIDs are now present
527+
$currentRidDir = Join-Path $installPath $script:currentRid
528+
$foreignRidDir = Join-Path $installPath $foreignRid
529+
Test-Path $currentRidDir | Should -BeTrue
530+
Test-Path $foreignRidDir | Should -BeTrue
531+
}
532+
}
533+
534+
535+
Context 'Missing TFM/RID Validation Warnings' {
536+
537+
BeforeAll {
538+
$script:warnPkgName = 'TestMissingTfmRidModule'
539+
$script:warnPkgVersion = '1.0.0'
540+
541+
# Package only has net8.0 and win-x64
542+
New-TestNupkg -Name $warnPkgName -Version $warnPkgVersion `
543+
-OutputDir $localRepoDir `
544+
-RuntimeIdentifiers @('win-x64') `
545+
-LibTfms @('net8.0') `
546+
-IncludeModuleManifest
547+
}
548+
549+
AfterEach {
550+
Uninstall-PSResource $warnPkgName -Version "*" -ErrorAction SilentlyContinue
551+
}
552+
553+
It "Should warn when -TargetFramework does not exist in the package" {
554+
$warnings = $null
555+
Install-PSResource -Name $warnPkgName -Repository $localRepoName -TrustRepository -Version $warnPkgVersion -TargetFramework 'net472' -WarningVariable warnings
556+
$warnings | Should -Not -BeNullOrEmpty
557+
($warnings | Out-String) | Should -BeLike "*net472*not found*"
558+
}
559+
560+
It "Should warn when -RuntimeIdentifier does not exist in the package" {
561+
$warnings = $null
562+
Install-PSResource -Name $warnPkgName -Repository $localRepoName -TrustRepository -Version $warnPkgVersion -RuntimeIdentifier 'linux-arm64' -WarningVariable warnings
563+
$warnings | Should -Not -BeNullOrEmpty
564+
($warnings | Out-String) | Should -BeLike "*linux-arm64*not found*"
565+
}
566+
}
399567
}

0 commit comments

Comments
 (0)