@@ -874,8 +874,27 @@ private ConcurrentDictionary<string, Hashtable> DownloadParentAndDeps(
874874 if ( ! skipDependencyCheck )
875875 {
876876 // List returned only includes dependencies, so we'll add the parent pkg to this list to pass on to installation method.
877- parentAndDeps . AddRange ( _findHelper . FindDependencyPackages ( currentServer , currentResponseUtil , pkgToInstall , repository , errorMsgs , warningMsgs , debugMsgs , verboseMsgs ) ) ;
877+ IEnumerable < PSResourceInfo > depPkgs = _findHelper . FindDependencyPackages ( currentServer , currentResponseUtil , pkgToInstall , repository , errorMsgs , warningMsgs , debugMsgs , verboseMsgs ) ;
878878 debugMsgs . Enqueue ( "In InstallHelper::DownloadParentAndDeps(), found all dependencies" ) ;
879+
880+ // Do not reinstall dependencies that are already installed, unless -Reinstall was specified or an explicit
881+ // -TargetFramework/-RuntimeIdentifier requires re-downloading to merge additional platform content.
882+ // _packagesOnMachine is only mutated on the pipeline thread after this parallel phase, so reading it here is safe.
883+ bool skipInstalledDeps = ! _reinstall && string . IsNullOrEmpty ( _targetFramework ) && string . IsNullOrEmpty ( _runtimeIdentifier ) ;
884+ foreach ( PSResourceInfo depPkg in depPkgs )
885+ {
886+ if ( skipInstalledDeps && _packagesOnMachine != null )
887+ {
888+ string depPkgVersion = string . IsNullOrEmpty ( depPkg . Prerelease ) ? depPkg . Version . ToString ( ) : $ "{ depPkg . Version } -{ depPkg . Prerelease } ";
889+ if ( _packagesOnMachine . Contains ( $ "{ depPkg . Name } { depPkgVersion } ") )
890+ {
891+ verboseMsgs . Enqueue ( $ "Dependency '{ depPkg . Name } ' with version '{ depPkgVersion } ' is already installed.") ;
892+ continue ;
893+ }
894+ }
895+
896+ parentAndDeps . Add ( depPkg ) ;
897+ }
879898 }
880899
881900 parentAndDeps . Add ( pkgToInstall ) ;
@@ -1068,7 +1087,7 @@ private bool TryInstallToTempPath(
10681087 var tempDirNameVersion = Path . Combine ( tempInstallPath , pkgName , pkgVersion ) ;
10691088 Directory . CreateDirectory ( tempDirNameVersion ) ;
10701089
1071- if ( ! TryExtractToDirectory ( pathToFile , tempDirNameVersion , out ErrorRecord error ) )
1090+ if ( ! TryExtractToDirectory ( pathToFile , tempDirNameVersion , warningMsgs , debugMsgs , out ErrorRecord error ) )
10721091 {
10731092 Utils . EnqueueIfNotNull ( errorMsgs , error ) ;
10741093 return false ;
@@ -1296,7 +1315,16 @@ private bool TrySaveNupkgToTempPath(
12961315 /// - Root-level RID folder entries (e.g., win-x64/) based on the current platform's RID
12971316 /// - lib/{tfm}/ entries to only extract the best matching Target Framework Moniker
12981317 /// </summary>
1299- private bool TryExtractToDirectory ( string zipPath , string extractPath , out ErrorRecord error )
1318+ /// <remarks>
1319+ /// This method may run on a background thread (via Parallel.ForEach), so it must not call
1320+ /// cmdlet Write* methods directly. Messages are enqueued and written out by the pipeline thread.
1321+ /// </remarks>
1322+ private bool TryExtractToDirectory (
1323+ string zipPath ,
1324+ string extractPath ,
1325+ ConcurrentQueue < string > warningMsgs ,
1326+ ConcurrentQueue < string > debugMsgs ,
1327+ out ErrorRecord error )
13001328 {
13011329 error = null ;
13021330 // Normalize the path
@@ -1321,24 +1349,24 @@ private bool TryExtractToDirectory(string zipPath, string extractPath, out Error
13211349 bestLibFramework = NuGetFramework . ParseFolder ( _targetFramework ) ;
13221350 if ( bestLibFramework == null || bestLibFramework . IsUnsupported )
13231351 {
1324- _cmdletPassedIn . WriteDebug ( $ "Could not parse specified TargetFramework '{ _targetFramework } ', falling back to auto-detection.") ;
1325- bestLibFramework = GetBestLibFramework ( archive ) ;
1352+ debugMsgs . Enqueue ( $ "Could not parse specified TargetFramework '{ _targetFramework } ', falling back to auto-detection.") ;
1353+ bestLibFramework = GetBestLibFramework ( archive , debugMsgs ) ;
13261354 }
13271355 else
13281356 {
1329- _cmdletPassedIn . WriteDebug ( $ "Using user-specified TargetFramework: { bestLibFramework . GetShortFolderName ( ) } ") ;
1357+ debugMsgs . Enqueue ( $ "Using user-specified TargetFramework: { bestLibFramework . GetShortFolderName ( ) } ") ;
13301358 }
13311359 }
13321360 else
13331361 {
1334- bestLibFramework = GetBestLibFramework ( archive ) ;
1362+ bestLibFramework = GetBestLibFramework ( archive , debugMsgs ) ;
13351363 }
13361364
13371365 // Warn if TFM filtering is skipping assemblies for a different .NET lineage
13381366 // (e.g., installing on PS7/.NET 8 but package also has net472 for WinPS 5.1)
13391367 if ( bestLibFramework != null )
13401368 {
1341- WarnIfCrossLineageTfmSkipped ( archive , bestLibFramework ) ;
1369+ WarnIfCrossLineageTfmSkipped ( archive , bestLibFramework , warningMsgs ) ;
13421370 }
13431371
13441372 foreach ( ZipArchiveEntry entry in archive . Entries . Where ( entry => entry . CompressedLength > 0 ) )
@@ -1351,15 +1379,15 @@ private bool TryExtractToDirectory(string zipPath, string extractPath, out Error
13511379
13521380 if ( ! includeEntry )
13531381 {
1354- _cmdletPassedIn . WriteDebug ( $ "Skipping runtime entry not matching target platform: { entry . FullName } ") ;
1382+ debugMsgs . Enqueue ( $ "Skipping runtime entry not matching target platform: { entry . FullName } ") ;
13551383 continue ;
13561384 }
13571385 }
13581386
13591387 // TFM filtering: for lib/ entries, only extract the best matching TFM
13601388 if ( bestLibFramework != null && ! ShouldIncludeLibEntry ( entry . FullName , bestLibFramework ) )
13611389 {
1362- _cmdletPassedIn . WriteDebug ( $ "Skipping lib entry not matching target framework: { entry . FullName } ") ;
1390+ debugMsgs . Enqueue ( $ "Skipping lib entry not matching target framework: { entry . FullName } ") ;
13631391 continue ;
13641392 }
13651393
@@ -1412,7 +1440,7 @@ private bool TryExtractToDirectory(string zipPath, string extractPath, out Error
14121440 . Select ( s => s [ 1 ] )
14131441 . Distinct ( StringComparer . OrdinalIgnoreCase ) ;
14141442 string available = string . Join ( ", " , availableTfms ) ;
1415- _cmdletPassedIn . WriteWarning (
1443+ warningMsgs . Enqueue (
14161444 $ "The specified TargetFramework '{ _targetFramework } ' was not found in this package. " +
14171445 $ "No lib/ assemblies were installed. Available TFMs: { ( string . IsNullOrEmpty ( available ) ? "none" : available ) } ") ;
14181446 }
@@ -1427,7 +1455,7 @@ private bool TryExtractToDirectory(string zipPath, string extractPath, out Error
14271455 if ( ! hasMatchingRid )
14281456 {
14291457 string available = string . Join ( ", " , availableRids ) ;
1430- _cmdletPassedIn . WriteWarning (
1458+ warningMsgs . Enqueue (
14311459 $ "The specified RuntimeIdentifier '{ _runtimeIdentifier } ' was not found in this package. " +
14321460 $ "No platform-specific assets were installed. Available RIDs: { ( string . IsNullOrEmpty ( available ) ? "none" : available ) } ") ;
14331461 }
@@ -1454,7 +1482,7 @@ private bool TryExtractToDirectory(string zipPath, string extractPath, out Error
14541482 /// </summary>
14551483 /// <param name="archive">The zip archive to analyze.</param>
14561484 /// <returns>The best matching NuGetFramework, or null if no lib/ folders exist or no match is found.</returns>
1457- private NuGetFramework GetBestLibFramework ( ZipArchive archive )
1485+ private NuGetFramework GetBestLibFramework ( ZipArchive archive , ConcurrentQueue < string > debugMsgs )
14581486 {
14591487 // Collect all TFMs from lib/ folder entries
14601488 var libFrameworks = new HashSet < string > ( StringComparer . OrdinalIgnoreCase ) ;
@@ -1504,14 +1532,14 @@ private NuGetFramework GetBestLibFramework(ZipArchive archive)
15041532
15051533 if ( bestMatch != null )
15061534 {
1507- _cmdletPassedIn . WriteDebug ( $ "Selected best matching TFM: { bestMatch . GetShortFolderName ( ) } (from { string . Join ( ", " , libFrameworks ) } )") ;
1535+ debugMsgs . Enqueue ( $ "Selected best matching TFM: { bestMatch . GetShortFolderName ( ) } (from { string . Join ( ", " , libFrameworks ) } )") ;
15081536 }
15091537
15101538 return bestMatch ;
15111539 }
15121540 catch ( Exception e )
15131541 {
1514- _cmdletPassedIn . WriteDebug ( $ "TFM selection failed, extracting all lib/ folders: { e . Message } ") ;
1542+ debugMsgs . Enqueue ( $ "TFM selection failed, extracting all lib/ folders: { e . Message } ") ;
15151543 return null ;
15161544 }
15171545 }
@@ -1609,7 +1637,7 @@ private static NuGetFramework GetCurrentFramework()
16091637 /// .NET Framework assemblies (net472), those will be skipped — which means the module
16101638 /// won't work if later used on Windows PowerShell 5.1.
16111639 /// </summary>
1612- private void WarnIfCrossLineageTfmSkipped ( ZipArchive archive , NuGetFramework bestFramework )
1640+ private void WarnIfCrossLineageTfmSkipped ( ZipArchive archive , NuGetFramework bestFramework , ConcurrentQueue < string > warningMsgs )
16131641 {
16141642 try
16151643 {
@@ -1653,7 +1681,7 @@ private void WarnIfCrossLineageTfmSkipped(ZipArchive archive, NuGetFramework bes
16531681 string skippedList = string . Join ( ", " , skippedLineageTfms ) ;
16541682 string otherHost = bestIsNetCore ? "Windows PowerShell 5.1" : "PowerShell 7+" ;
16551683 string skippedTfm = skippedLineageTfms . First ( ) ;
1656- _cmdletPassedIn . WriteWarning (
1684+ warningMsgs . Enqueue (
16571685 $ "This package contains assemblies for { skippedList } which were not installed because " +
16581686 $ "the current runtime selected { bestFramework . GetShortFolderName ( ) } . " +
16591687 $ "If you also use this module on { otherHost } , run: Install-PSResource -Name <ModuleName> -TargetFramework '{ skippedTfm } '") ;
0 commit comments