Skip to content

Commit 7836985

Browse files
authored
✨ Add -DestinationOnly switch (#50)
This pull request fixes an issue with the Install-ModuleFast function where it doesn't install modules in the specified destination if the same module is already installed in a different location on the system. To address this issue, I have added a new parameter -DestinationOnly to the Install-ModuleFast function. When this parameter is specified, the function will only consider the specified destination and not any other paths currently in the PSModulePath. This is useful for scenarios where you want to ensure that the modules are installed in a specific location, such as in a CI environment. I have also made changes to the Get-ModuleFastPlan and Find-LocalModule functions to support the new -DestinationOnly parameter.
1 parent 6fe4e90 commit 7836985

2 files changed

Lines changed: 55 additions & 13 deletions

File tree

ModuleFast.psm1

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,8 @@ function Install-ModuleFast {
216216
[Switch]$Prerelease,
217217
#Using the CI switch will write a lockfile to the current folder. If this file is present and -CI is specified in the future, ModuleFast will only install the versions specified in the lockfile, which is useful for reproducing CI builds even if newer versions of software come out.
218218
[Switch]$CI,
219+
#Only consider the specified destination and not any other paths currently in the PSModulePath. This is useful for CI scenarios where you want to ensure that the modules are installed in a specific location.
220+
[Switch]$DestinationOnly,
219221
#How many concurrent installation threads to run. Each installation thread, given sufficient bandwidth, will likely saturate a full CPU core with decompression work. This defaults to the number of logical cores on the system. If your system uses HyperThreading and presents more logical cores than physical cores available, you may want to set this to half your number of logical cores for best performance.
220222
[int]$ThrottleLimit = [Environment]::ProcessorCount,
221223
#The path to the lockfile. By default it is requires.lock.json in the current folder. This is ignored if CI is not present. It is generally not recommended to change this setting.
@@ -338,7 +340,7 @@ function Install-ModuleFast {
338340
$ModulesToInstall.ToArray()
339341
} else {
340342
Write-Progress -Id 1 -Activity 'Install-ModuleFast' -Status 'Plan' -PercentComplete 1
341-
Get-ModuleFastPlan -Specification $ModulesToInstall -HttpClient $httpClient -Source $Source -Update:$Update -PreRelease:$Prerelease.IsPresent
343+
Get-ModuleFastPlan -Specification $ModulesToInstall -HttpClient $httpClient -Source $Source -Update:$Update -PreRelease:$Prerelease.IsPresent -DestinationOnly:$DestinationOnly -Destination $Destination
342344
}
343345
}
344346

@@ -459,6 +461,8 @@ function Get-ModuleFastPlan {
459461
[PSCredential]$Credential,
460462
[HttpClient]$HttpClient = $(New-ModuleFastClient -Credential $Credential),
461463
[int]$ParentProgress,
464+
[string]$Destination,
465+
[switch]$DestinationOnly,
462466
[CancellationToken]$CancellationToken
463467
)
464468

@@ -506,7 +510,13 @@ function Get-ModuleFastPlan {
506510

507511
foreach ($moduleSpec in $ModulesToResolve) {
508512
Write-Verbose "${moduleSpec}: Evaluating Module Specification"
509-
[ModuleFastInfo]$localMatch = Find-LocalModule $moduleSpec -Update:$Update -BestCandidate:([ref]$bestLocalCandidate)
513+
$findLocalParams = @{
514+
Update = $Update
515+
BestCandidate = ([ref]$bestLocalCandidate)
516+
}
517+
if ($DestinationOnly) { $findLocalParams.ModulePaths = $Destination }
518+
519+
[ModuleFastInfo]$localMatch = Find-LocalModule @findLocalParams $moduleSpec
510520
if ($localMatch) {
511521
Write-Debug "${localMatch}: 🎯 FOUND satisfying version $($localMatch.ModuleVersion) at $($localMatch.Location). Skipping remote search."
512522
#TODO: Capture this somewhere that we can use it to report in the deploy plan
@@ -768,7 +778,13 @@ function Get-ModuleFastPlan {
768778
# We do this here rather than populate modulesToResolve because the tasks wont start until all the existing tasks complete
769779
# TODO: Figure out a way to dedupe this logic maybe recursively but I guess a function would be fine too
770780
foreach ($dependencySpec in $dependenciesToResolve) {
771-
[ModuleFastInfo]$localMatch = Find-LocalModule $dependencySpec -Update:$Update
781+
$findLocalParams = @{
782+
Update = $Update
783+
BestCandidate = ([ref]$bestLocalCandidate)
784+
}
785+
if ($DestinationOnly) { $findLocalParams.Destination = $Destination }
786+
787+
[ModuleFastInfo]$localMatch = Find-LocalModule @findLocalParams $dependencySpec
772788
if ($localMatch) {
773789
Write-Debug "FOUND local module $($localMatch.Name) $($localMatch.ModuleVersion) at $($localMatch.Location.AbsolutePath) that satisfies $moduleSpec. Skipping..."
774790
#TODO: Capture this somewhere that we can use it to report in the deploy plan
@@ -895,8 +911,7 @@ function Install-ModuleFastHelper {
895911
$streamTask
896912
}
897913

898-
#We are going to extract these straight out of memory, so we don't need to write the nupkg to disk
899-
Write-Verbose "$($context.Module): Extracting to $($context.installPath)"
914+
900915
[List[Job2]]$installJobs = while ($streamTasks.count -gt 0) {
901916
$noTasksYetCompleted = -1
902917
[int]$thisTaskIndex = [Task]::WaitAny($streamTasks, 500)
@@ -908,6 +923,7 @@ function Install-ModuleFastHelper {
908923
$streamTasks.RemoveAt($thisTaskIndex)
909924

910925
# This is a sync process and we want to do it in parallel, hence the threadjob
926+
Write-Verbose "$($context.Module): Extracting to $($context.installPath)"
911927
$installJob = Start-ThreadJob -ThrottleLimit $ThrottleLimit {
912928
param(
913929
[ValidateNotNullOrEmpty()]$stream = $USING:stream,
@@ -929,6 +945,7 @@ function Install-ModuleFastHelper {
929945

930946
New-Item -ItemType File -Path $installIndicatorPath -Force | Out-Null
931947

948+
#We are going to extract these straight out of memory, so we don't need to write the nupkg to disk
932949
$zip = [IO.Compression.ZipArchive]::new($stream, 'Read')
933950
[IO.Compression.ZipFileExtensions]::ExtractToDirectory($zip, $installPath)
934951

@@ -984,7 +1001,12 @@ function Install-ModuleFastHelper {
9841001
CLEAN {
9851002
$cancelTokenSource.Dispose()
9861003
if ($installJobs) {
987-
$installJobs | Remove-Job -Force
1004+
try {
1005+
$installJobs | Remove-Job -Force -ErrorAction SilentlyContinue
1006+
} catch {
1007+
#Suppress this error because it is likely that the job was already removed
1008+
if ($PSItem -notlike '*because it is a child job*') {throw}
1009+
}
9881010
}
9891011
}
9901012
}
@@ -1545,21 +1567,20 @@ function Find-LocalModule {
15451567
#>
15461568
param(
15471569
[Parameter(Mandatory)][ModuleFastSpec]$ModuleSpec,
1548-
[string[]]$ModulePath = $($env:PSModulePath -split [Path]::PathSeparator),
1570+
[string[]]$ModulePaths = $($env:PSModulePath.Split([Path]::PathSeparator, [StringSplitOptions]::RemoveEmptyEntries)),
15491571
[Switch]$Update,
15501572
[ref]$BestCandidate
15511573
)
15521574
$ErrorActionPreference = 'Stop'
15531575

1554-
$modulePaths = $env:PSModulePath.Split([Path]::PathSeparator, [StringSplitOptions]::RemoveEmptyEntries)
1555-
if (-not $modulePaths) {
1576+
if (-not $ModulePaths) {
15561577
Write-Warning 'No PSModulePaths found in $env:PSModulePath. If you are doing isolated testing you can disregard this.'
15571578
return
15581579
}
15591580

15601581
#We want to minimize reading the manifest files, so we will do a fast file-based search first and then do a more detailed inspection on high confidence candidate(s). Any module in any folder path that satisfies the spec will be sufficient, we don't care about finding the "latest" version, so we will return the first module that satisfies the spec. We will store potential candidates in this list, with their evaluated "guessed" version based on the folder name and the path. The first items added to the list should be the highest likelihood candidates in Path priority order, so no sorting should be necessary.
15611582

1562-
foreach ($modulePath in $modulePaths) {
1583+
foreach ($modulePath in $ModulePaths) {
15631584
[List[[Tuple[Version, string]]]]$candidatePaths = @()
15641585
if (-not [Directory]::Exists($modulePath)) {
15651586
Write-Debug "${ModuleSpec}: Skipping PSModulePath $modulePath - Configured but does not exist."
@@ -1673,9 +1694,11 @@ function Find-LocalModule {
16731694
if ($Update -and ($ModuleSpec.Max -ne $candidateVersion)) {
16741695
Write-Debug "${ModuleSpec}: Skipping $candidateVersion because -Update was specified and the version does not exactly meet the upper bound of the spec or no upper bound was specified at all, meaning there is a possible newer version remotely."
16751696
#We can use this ref later to find out if our best remote version matches what is installed without having to read the manifest again
1676-
if ($bestCandidate.Value[$moduleSpec] -and $manifestCandidate.ModuleVersion -gt $bestCandidate.Value[$moduleSpec]) {
1677-
Write-Debug "${ModuleSpec}: New Best Candidate Version $($manifestCandidate.ModuleVersion)"
1678-
$BestCandidate.Value.Add($moduleSpec, $manifestCandidate)
1697+
if (-not $bestCandidate.Value[$moduleSpec] -or
1698+
$manifestCandidate.ModuleVersion -gt $bestCandidate.Value[$moduleSpec].ModuleVersion
1699+
) {
1700+
Write-Debug "${ModuleSpec}: ⬆️ New Best Candidate Version $($manifestCandidate.ModuleVersion)"
1701+
$BestCandidate.Value[$moduleSpec] = $manifestCandidate
16791702
}
16801703
continue
16811704
}

ModuleFast.tests.ps1

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,7 @@ Describe 'Install-ModuleFast' -Tag 'E2E' {
478478
Install-ModuleFast @imfParams 'Az.Compute', 'Az.CosmosDB' -Update -Plan
479479
| Should -BeNullOrEmpty
480480
}
481+
481482
It 'Updates if multiple local versions installed' {
482483
Install-ModuleFast @imfParams 'Plaster=1.1.1'
483484
Install-ModuleFast @imfParams 'Plaster=1.1.3'
@@ -518,6 +519,24 @@ Describe 'Install-ModuleFast' -Tag 'E2E' {
518519
| Select-Object -First 1
519520
| Should -BeGreaterThan ([version]'5.0.0')
520521
}
522+
523+
It 'Detects module in other psmodulePath' {
524+
$installPath2 = Join-Path $testdrive $(New-Guid)
525+
New-Item -ItemType Directory $installPath2 | Out-Null
526+
$env:PSModulePath = "$installPath2"
527+
Install-ModuleFast @imfParams -Destination $installPath2 'PreReleaseTest'
528+
Install-ModuleFast @imfParams 'PreReleaseTest' -PassThru | Should -BeNullOrEmpty
529+
}
530+
531+
It 'Only considers destination modules if -DestinationOnly is specified' {
532+
$installPath2 = Join-Path $testdrive $(New-Guid)
533+
New-Item -ItemType Directory $installPath2 | Out-Null
534+
$env:PSModulePath = "$installPath2"
535+
Install-ModuleFast @imfParams -Destination $installPath2 'PreReleaseTest'
536+
Install-ModuleFast @imfParams 'PreReleaseTest' -DestinationOnly -PassThru | Should -HaveCount 1
537+
Install-ModuleFast @imfParams 'PreReleaseTest' -DestinationOnly -PassThru | Should -BeNullOrEmpty
538+
}
539+
521540
It 'Errors trying to install prerelease over regular module' {
522541
Install-ModuleFast @imfParams 'PrereleaseTest=0.0.1'
523542
{ Install-ModuleFast @imfParams 'PrereleaseTest=0.0.1-prerelease' }

0 commit comments

Comments
 (0)