@@ -21,7 +21,7 @@ THIS IS NOT FOR PRODUCTION, it should be considered "Fragile" and has very littl
2121It also doesn't generate the PowershellGet XML files currently, so PowershellGet will see them as "External" modules
2222#>
2323function Install-ModuleFast {
24- [CmdletBinding (SupportsShouldProcess )]
24+ [CmdletBinding (SupportsShouldProcess , ConfirmImpact = ' High ' )]
2525 param (
2626 $ModulesToInstall ,
2727 $Destination ,
@@ -30,7 +30,6 @@ function Install-ModuleFast {
3030 [string ]$Source = ' https://preview.pwsh.gallery/index.json' ,
3131 # The credential to use to authenticate. Only basic auth is supported
3232 [PSCredential ]$Credential ,
33- [Switch ]$Force ,
3433 # By default will modify your PSModulePath to use the builtin destination if not present. Setting this implicitly skips profile update as well.
3534 [Switch ]$NoPSModulePathUpdate ,
3635 # Setting this won't add the default destination to your profile.
@@ -46,7 +45,7 @@ function Install-ModuleFast {
4645 # Autocreate the default as a convenience, otherwise require the path to be present to avoid mistakes
4746 if ($Destination -eq $defaultRepoPath -and -not (Test-Path $Destination )) {
4847 if ($PSCmdlet.ShouldProcess (' Create Destination Folder' , $Destination )) {
49- New-Item - ItemType Directory - Path $Destination - Force
48+ New-Item - ItemType Directory - Path $Destination - Force | Out-Null
5049 }
5150 }
5251
@@ -63,9 +62,30 @@ function Install-ModuleFast {
6362 Add-DestinationToPSModulePath - Destination $Destination - NoProfileUpdate:$NoProfileUpdate
6463 }
6564
65+ $currentWhatIfPreference = $WhatIfPreference
66+ # We do some stuff here that doesn't affect the system but triggers whatif, so we disable it
67+ $WhatIfPreference = $false
6668 $httpClient = New-ModuleFastClient - Credential $Credential
67- Write-Progress - Id 1 - Activity ' Install-ModuleFast' - Status ' Preparing Plan' - PercentComplete 1
69+ Write-Progress - Id 1 - Activity ' Install-ModuleFast' - Status ' Plan' - PercentComplete 1
6870 $plan = Get-ModuleFastPlan $ModulesToInstall - HttpClient $httpClient - Source $Source
71+ $WhatIfPreference = $currentWhatIfPreference
72+
73+ if ($plan.Count -eq 0 ) {
74+ Write-Verbose ' No modules found to install or all modules are already installed. Exiting.'
75+ return
76+ }
77+
78+ if (-not $PSCmdlet.ShouldProcess ($Destination , " Install $ ( $plan.Count ) Modules" )) {
79+ Write-Host - fore DarkGreen ' 🚀 ModuleFast Install Plan BEGIN'
80+ $plan
81+ | Select-Object Name, @ {N = ' Version' ; E = { $_.Required } }
82+ | Format-Table - AutoSize
83+ | Out-String
84+ | Write-Host - ForegroundColor DarkGray
85+ Write-Host - fore DarkGreen ' 🚀 ModuleFast Install Plan END'
86+ return
87+ }
88+
6989
7090 Write-Progress - Id 1 - Activity ' Install-ModuleFast' - Status " Installing: $ ( $plan.count ) Modules" - PercentComplete 50
7191
@@ -136,7 +156,8 @@ function Get-ModuleFastPlan {
136156 # By default we use in-place modules if they satisfy the version requirements. This switch will force a search for all latest modules
137157 [Switch ]$Update ,
138158 [PSCredential ]$Credential ,
139- [HttpClient ]$HttpClient = $ (New-ModuleFastClient - Credential $Credential )
159+ [HttpClient ]$HttpClient = $ (New-ModuleFastClient - Credential $Credential ),
160+ [int ]$ParentProgress
140161 )
141162
142163 BEGIN {
@@ -151,7 +172,6 @@ function Get-ModuleFastPlan {
151172 HttpClient = $httpClient
152173 CancellationToken = $cancelToken.Token
153174 }
154- # Write-Progress -Id 1 -Activity 'Get-ModuleFast' -CurrentOperation 'Fetching module information from Powershell Gallery'
155175 }
156176 PROCESS {
157177 foreach ($spec in $Name ) {
@@ -186,13 +206,18 @@ function Get-ModuleFastPlan {
186206 $currentTasks.Add ($task )
187207 }
188208
209+ [int ]$tasksCompleteCount = 1
210+ [int ]$resolveTaskCount = $currentTasks.Count -as [Int ]
189211 while ($currentTasks.Count -gt 0 ) {
190212 # The timeout here allow ctrl-C to continue working in PowerShell
191213 # -1 is returned by WaitAny if we hit the timeout before any tasks completed
192214 $noTasksYetCompleted = -1
193215 [int ]$thisTaskIndex = [Task ]::WaitAny($currentTasks , 500 )
194216 if ($thisTaskIndex -eq $noTasksYetCompleted ) { continue }
195217
218+ # The Plan whitespace is intentional so that it lines up with install progress using the compact format
219+ Write-Progress - Id 1 - Activity ' Install-ModuleFast' - Status " Plan: Resolving $tasksCompleteCount /$resolveTaskCount Module Dependencies" - PercentComplete ((($tasksCompleteCount / $resolveTaskCount ) * 50 ) + 1 )
220+
196221 # TODO: This only indicates headers were received, content may still be downloading and we dont want to block on that.
197222 # For now the content is small but this could be faster if we have another inner loop that WaitAny's on content
198223 # TODO: Perform a HEAD query to see if something has changed
@@ -299,6 +324,7 @@ function Get-ModuleFastPlan {
299324 # This should be a relatively rare query that only happens when the latest package isn't being resolved.
300325 [Task [string ][]]$tasks = foreach ($page in $pages ) {
301326 Get-ModuleInfoAsync @httpContext - Uri $page .' @id'
327+ # Used to track progress as tasks can get removed
302328 # This loop is here to support ctrl-c cancellation again
303329 }
304330 while ($false -in $tasks.IsCompleted ) {
@@ -336,6 +362,7 @@ function Get-ModuleFastPlan {
336362 # TODO: Fix the flow so this isn't stated twice
337363 [void ]$resolveTasks.Remove ($completedTask )
338364 [void ]$currentTasks.Remove ($completedTask )
365+ $tasksCompleteCount ++
339366 continue
340367 }
341368
@@ -415,12 +442,16 @@ function Get-ModuleFastPlan {
415442 Write-Debug " $currentModuleSpec `: Fetching dependency $dependencySpec "
416443 $task = Get-ModuleInfoAsync @httpContext - Endpoint $Source - Name $dependencySpec.Name
417444 $resolveTasks [$task ] = $dependencySpec
445+ # Used to track progress as tasks can get removed
446+ $resolveTaskCount ++
447+
418448 $currentTasks.Add ($task )
419449 }
420450 }
421451 try {
422452 [void ]$resolveTasks.Remove ($completedTask )
423453 [void ]$currentTasks.Remove ($completedTask )
454+ $tasksCompleteCount ++
424455 } catch {
425456 throw
426457 }
@@ -504,53 +535,8 @@ function Install-ModuleFastHelper {
504535 if (-not $installJobs.Remove ($completedJob )) { throw ' Could not remove completed job from list. This is a bug, report it' }
505536 $installed ++
506537 Write-Verbose " $installedModule `: Successfuly installed to $installPath "
507- Write-Progress - Id $installProgressId - ParentId 1 - Activity ' Install' - Status " $installed /$ ( $ModuleToInstall.count ) Modules" - PercentComplete ($installed / $ModuleToInstall.count * 100 )
538+ Write-Progress - Id 1 - Activity ' Install-ModuleFast ' - Status " Install: $installed /$ ( $ModuleToInstall.count ) Modules" - PercentComplete ((( $installed / $ModuleToInstall.count ) * 50 ) + 50 )
508539 }
509-
510- # #Installation jobs are captured here, we will check them once all downloads have completed
511-
512-
513- # $downloaded = 0
514- # $downloadedProgressId = Get-Random
515- # #TODO: Filestreams should be disposed in a try/catch in case of cancellation. In PS 7.3+, should be a clean() block
516- # while ($downloadTasks.count -gt 0) {
517- # #TODO: Check on in jobs and if there's a failure, cancel the rest of the jobs
518- # $noTasksYetCompleted = -1
519- # [int]$thisTaskIndex = [Task]::WaitAny($downloadTasks, 500)
520- # if ($thisTaskIndex -eq $noTasksYetCompleted) { continue }
521- # $thisTask = $downloadTasks[$thisTaskIndex]
522- # $context = $taskMap[$thisTask]
523- # # We can close these streams now that it is downloaded.
524- # # This releases the lock on the file
525- # #TODO: Maybe can connect the stream to a zip decompressionstream. Should be in cache so performance would be negligible
526- # $context.fileStream.Dispose()
527- # $context.fetchStream.Dispose()
528- # #The file copy task is a void task that doesnt return anything, so we dont need to do GetResult()
529- # $downloadTasks.RemoveAt($thisTaskIndex)
530-
531- # #Start a new threadjob to handle the installation, because the zipfile API is not async. Also extraction is
532- # #CPU intensive so multiple threads will be helpful here and worth the startup cost of a runspace
533- # $installJobParams = @{
534- # ScriptBlock = (Get-Item Function:\Install-ModuleFastOperation).Scriptblock
535- # #Named parameters require a hack so we will just do these in order
536- # ArgumentList = @(
537- # $context.Module.Name,
538- # $context.Module.Version,
539- # $context.DownloadPath,
540- # $Destination
541- # )
542- # }
543- # Write-Debug "Starting Module Install Job for $($context.Module)"
544- # $installJob = Start-ThreadJob @installJobParams
545- # $installJobs.Add($installJob)
546- # $downloaded++
547- # Write-Progress -Id $downloadedProgressId -ParentId 1 -Activity 'Download' -Status "$downloaded/$($ModuleToInstall.count) Modules" -PercentComplete ($downloaded / $ModuleToInstall.count * 100)
548-
549- # }
550-
551- # #TODO: Correlate the installjobs to a dictionary so we can return the original modulespec maybe?
552- # #Or is that even needed?
553-
554540}
555541
556542# This will be run inside a threadjob. We separate this so that we can test it independently
@@ -1075,7 +1061,7 @@ function Get-ModuleInfoAsync {
10751061Adds an existing PowerShell Modules path to the current session as well as the profile
10761062#>
10771063function Add-DestinationToPSModulePath {
1078- [CmdletBinding (SupportsShouldProcess )]
1064+ [CmdletBinding (SupportsShouldProcess , ConfirmImpact = ' High ' )]
10791065 param (
10801066 [string ]$Destination ,
10811067 [switch ]$NoProfileUpdate
@@ -1087,21 +1073,25 @@ function Add-DestinationToPSModulePath {
10871073 [string []]$modulePaths = $env: PSModulePath -split [Path ]::PathSeparator
10881074
10891075 if ($Destination -notin $modulePaths ) {
1090- $pathUpdateMessage = " Update PSModulePath $ ( $NoProfileUpdate ? ' ' : ' and CurrentUserAllHosts profile ' ) to include $Destination "
1091- if (-not $PSCmdlet.ShouldProcess ($pathUpdateMessage , ' ' , ' ' )) { return }
1076+ Write-Verbose " Updating PSModulePath to include $Destination "
10921077 $modulePaths += $Destination
10931078 $env: PSModulePath = $modulePaths -join [Path ]::PathSeparator
10941079 }
10951080
10961081 if (-not $NoProfileUpdate ) {
1082+ # TODO: Support other profiles?
10971083 $myProfile = $profile.CurrentUserAllHosts
1084+
10981085 if (-not (Test-Path $myProfile )) {
1086+ if (-not $PSCmdlet.ShouldProcess ($myProfile , " Allow ModuleFast to work by creating a profile at $myProfile ." )) { return }
10991087 Write-Verbose ' User All Hosts profile not found, creating one.'
11001088 New-Item - ItemType File - Path $myProfile - Force | Out-Null
11011089 }
1102- $ProfileLine = " `$ env:PSModulePath += [System. IO.Path]::PathSeparator + $Destination #Added by ModuleFast. DO NOT EDIT THIS LINE. If you dont want this, add -NoProfileUpdate to your command ."
1090+ $ProfileLine = " `$ env:PSModulePath += ' $ ( [ IO.Path ]::PathSeparator + $Destination ) ' #Added by ModuleFast. DO NOT EDIT THIS LINE. If you do not want this, add -NoProfileUpdate to Install-ModuleFast ."
11031091 if ((Get-Content - Raw $myProfile ) -notmatch [Regex ]::Escape($ProfileLine )) {
1092+ if (-not $PSCmdlet.ShouldProcess ($myProfile , " Allow ModuleFast to work by adding $Destination to your PSModulePath on startup by appending to your CurrentUserAllHosts profile." )) { return }
11041093 Write-Verbose " Adding $Destination to profile $myProfile "
1094+ Add-Content - Path $myProfile - Value " `n`n "
11051095 Add-Content - Path $myProfile - Value $ProfileLine
11061096 } else {
11071097 Write-Verbose " PSModulePath $Destination already in profile, skipping..."
0 commit comments