Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 36 additions & 13 deletions ModuleFast.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,8 @@ function Install-ModuleFast {
[Switch]$Prerelease,
#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.
[Switch]$CI,
#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.
[Switch]$DestinationOnly,
#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.
[int]$ThrottleLimit = [Environment]::ProcessorCount,
#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.
Expand Down Expand Up @@ -338,7 +340,7 @@ function Install-ModuleFast {
$ModulesToInstall.ToArray()
} else {
Write-Progress -Id 1 -Activity 'Install-ModuleFast' -Status 'Plan' -PercentComplete 1
Get-ModuleFastPlan -Specification $ModulesToInstall -HttpClient $httpClient -Source $Source -Update:$Update -PreRelease:$Prerelease.IsPresent
Get-ModuleFastPlan -Specification $ModulesToInstall -HttpClient $httpClient -Source $Source -Update:$Update -PreRelease:$Prerelease.IsPresent -DestinationOnly:$DestinationOnly -Destination $Destination
}
}

Expand Down Expand Up @@ -459,6 +461,8 @@ function Get-ModuleFastPlan {
[PSCredential]$Credential,
[HttpClient]$HttpClient = $(New-ModuleFastClient -Credential $Credential),
[int]$ParentProgress,
[string]$Destination,
[switch]$DestinationOnly,
[CancellationToken]$CancellationToken
)

Expand Down Expand Up @@ -506,7 +510,13 @@ function Get-ModuleFastPlan {

foreach ($moduleSpec in $ModulesToResolve) {
Write-Verbose "${moduleSpec}: Evaluating Module Specification"
[ModuleFastInfo]$localMatch = Find-LocalModule $moduleSpec -Update:$Update -BestCandidate:([ref]$bestLocalCandidate)
$findLocalParams = @{
Update = $Update
BestCandidate = ([ref]$bestLocalCandidate)
}
if ($DestinationOnly) { $findLocalParams.ModulePaths = $Destination }

[ModuleFastInfo]$localMatch = Find-LocalModule @findLocalParams $moduleSpec
if ($localMatch) {
Write-Debug "${localMatch}: 🎯 FOUND satisfying version $($localMatch.ModuleVersion) at $($localMatch.Location). Skipping remote search."
#TODO: Capture this somewhere that we can use it to report in the deploy plan
Expand Down Expand Up @@ -768,7 +778,13 @@ function Get-ModuleFastPlan {
# We do this here rather than populate modulesToResolve because the tasks wont start until all the existing tasks complete
# TODO: Figure out a way to dedupe this logic maybe recursively but I guess a function would be fine too
foreach ($dependencySpec in $dependenciesToResolve) {
[ModuleFastInfo]$localMatch = Find-LocalModule $dependencySpec -Update:$Update
$findLocalParams = @{
Update = $Update
BestCandidate = ([ref]$bestLocalCandidate)
}
if ($DestinationOnly) { $findLocalParams.Destination = $Destination }

[ModuleFastInfo]$localMatch = Find-LocalModule @findLocalParams $dependencySpec
if ($localMatch) {
Write-Debug "FOUND local module $($localMatch.Name) $($localMatch.ModuleVersion) at $($localMatch.Location.AbsolutePath) that satisfies $moduleSpec. Skipping..."
#TODO: Capture this somewhere that we can use it to report in the deploy plan
Expand Down Expand Up @@ -895,8 +911,7 @@ function Install-ModuleFastHelper {
$streamTask
}

#We are going to extract these straight out of memory, so we don't need to write the nupkg to disk
Write-Verbose "$($context.Module): Extracting to $($context.installPath)"

[List[Job2]]$installJobs = while ($streamTasks.count -gt 0) {
$noTasksYetCompleted = -1
[int]$thisTaskIndex = [Task]::WaitAny($streamTasks, 500)
Expand All @@ -908,6 +923,7 @@ function Install-ModuleFastHelper {
$streamTasks.RemoveAt($thisTaskIndex)

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

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

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

Expand Down Expand Up @@ -984,7 +1001,12 @@ function Install-ModuleFastHelper {
CLEAN {
$cancelTokenSource.Dispose()
if ($installJobs) {
$installJobs | Remove-Job -Force
try {
$installJobs | Remove-Job -Force -ErrorAction SilentlyContinue
} catch {
#Suppress this error because it is likely that the job was already removed
if ($PSItem -notlike '*because it is a child job*') {throw}
}
}
}
}
Expand Down Expand Up @@ -1545,21 +1567,20 @@ function Find-LocalModule {
#>
param(
[Parameter(Mandatory)][ModuleFastSpec]$ModuleSpec,
[string[]]$ModulePath = $($env:PSModulePath -split [Path]::PathSeparator),
[string[]]$ModulePaths = $($env:PSModulePath.Split([Path]::PathSeparator, [StringSplitOptions]::RemoveEmptyEntries)),
[Switch]$Update,
[ref]$BestCandidate
)
$ErrorActionPreference = 'Stop'

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

#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.

foreach ($modulePath in $modulePaths) {
foreach ($modulePath in $ModulePaths) {
[List[[Tuple[Version, string]]]]$candidatePaths = @()
if (-not [Directory]::Exists($modulePath)) {
Write-Debug "${ModuleSpec}: Skipping PSModulePath $modulePath - Configured but does not exist."
Expand Down Expand Up @@ -1673,9 +1694,11 @@ function Find-LocalModule {
if ($Update -and ($ModuleSpec.Max -ne $candidateVersion)) {
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."
#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
if ($bestCandidate.Value[$moduleSpec] -and $manifestCandidate.ModuleVersion -gt $bestCandidate.Value[$moduleSpec]) {
Write-Debug "${ModuleSpec}: New Best Candidate Version $($manifestCandidate.ModuleVersion)"
$BestCandidate.Value.Add($moduleSpec, $manifestCandidate)
if (-not $bestCandidate.Value[$moduleSpec] -or
$manifestCandidate.ModuleVersion -gt $bestCandidate.Value[$moduleSpec].ModuleVersion
) {
Write-Debug "${ModuleSpec}: ⬆️ New Best Candidate Version $($manifestCandidate.ModuleVersion)"
$BestCandidate.Value[$moduleSpec] = $manifestCandidate
}
continue
}
Expand Down
19 changes: 19 additions & 0 deletions ModuleFast.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,7 @@ Describe 'Install-ModuleFast' -Tag 'E2E' {
Install-ModuleFast @imfParams 'Az.Compute', 'Az.CosmosDB' -Update -Plan
| Should -BeNullOrEmpty
}

It 'Updates if multiple local versions installed' {
Install-ModuleFast @imfParams 'Plaster=1.1.1'
Install-ModuleFast @imfParams 'Plaster=1.1.3'
Expand Down Expand Up @@ -518,6 +519,24 @@ Describe 'Install-ModuleFast' -Tag 'E2E' {
| Select-Object -First 1
| Should -BeGreaterThan ([version]'5.0.0')
}

It 'Detects module in other psmodulePath' {
$installPath2 = Join-Path $testdrive $(New-Guid)
New-Item -ItemType Directory $installPath2 | Out-Null
$env:PSModulePath = "$installPath2"
Install-ModuleFast @imfParams -Destination $installPath2 'PreReleaseTest'
Install-ModuleFast @imfParams 'PreReleaseTest' -PassThru | Should -BeNullOrEmpty
}

It 'Only considers destination modules if -DestinationOnly is specified' {
$installPath2 = Join-Path $testdrive $(New-Guid)
New-Item -ItemType Directory $installPath2 | Out-Null
$env:PSModulePath = "$installPath2"
Install-ModuleFast @imfParams -Destination $installPath2 'PreReleaseTest'
Install-ModuleFast @imfParams 'PreReleaseTest' -DestinationOnly -PassThru | Should -HaveCount 1
Install-ModuleFast @imfParams 'PreReleaseTest' -DestinationOnly -PassThru | Should -BeNullOrEmpty
}

It 'Errors trying to install prerelease over regular module' {
Install-ModuleFast @imfParams 'PrereleaseTest=0.0.1'
{ Install-ModuleFast @imfParams 'PrereleaseTest=0.0.1-prerelease' }
Expand Down