diff --git a/ModuleFast.ps1 b/ModuleFast.ps1 index 75fa210..838710d 100644 --- a/ModuleFast.ps1 +++ b/ModuleFast.ps1 @@ -28,6 +28,7 @@ param ( #All additional arguments passed to this script will be passed to Install-ModuleFast [Parameter(ValueFromRemainingArguments)]$installArgs, #Used for testing + [Parameter(DontShow)] [switch]$ImportNugetVersioning ) $ErrorActionPreference = 'Stop' diff --git a/ModuleFast.psm1 b/ModuleFast.psm1 index 08972fe..3b9bba0 100644 --- a/ModuleFast.psm1 +++ b/ModuleFast.psm1 @@ -58,6 +58,8 @@ function Install-ModuleFast { [Switch]$NoProfileUpdate, #Setting this will check for newer modules if your installed modules are not already at the upper bound of the required version range. [Switch]$Update, + #Consider prerelease packages in the evaluation. Note that if a non-prerelease package has a prerelease dependency, that dependency will be included regardless of this setting. + [Switch]$Prerelease, [Parameter(Mandatory, ValueFromPipeline, ParameterSetName = 'ModuleFastInfo')][ModuleFastInfo]$ModuleFastInfo ) begin { @@ -142,7 +144,7 @@ function Install-ModuleFast { [ModuleFastInfo[]]$plan = switch ($PSCmdlet.ParameterSetName) { 'Specification' { Write-Progress -Id 1 -Activity 'Install-ModuleFast' -Status 'Plan' -PercentComplete 1 - Get-ModuleFastPlan -Specification $ModulesToInstall -HttpClient $httpClient -Source $Source -Update:$Update + Get-ModuleFastPlan -Specification $ModulesToInstall -HttpClient $httpClient -Source $Source -Update:$Update -PreRelease:$Prerelease.IsPresent } 'ModuleFastInfo' { $ModulesToInstall.ToArray() @@ -235,7 +237,7 @@ function Get-ModuleFastPlan { #The repository to scan for modules. TODO: Multi-repo support [string]$Source = 'https://pwsh.gallery/index.json', #Whether to include prerelease modules in the request - [Switch]$PreRelease, + [Switch]$Prerelease, #By default we use in-place modules if they satisfy the version requirements. This switch will force a search for all latest modules [Switch]$Update, [PSCredential]$Credential, @@ -264,6 +266,14 @@ function Get-ModuleFastPlan { } } END { + foreach ($version in $modulesToResolve.VersionRange.MinVersion, $modulesToResolve.VersionRange.MaxVersion) { + if ($version.IsPreRelease -or $version.HasMetadata) { + Write-Warning 'A specification with a prerelease version was detected, forcing -Prerelease switch. Specify -Prerelease in the future to avoid this warning.' + $Prerelease = $true + break + } + } + # A deduplicated list of modules to install [HashSet[ModuleFastInfo]]$modulesToInstall = @{} @@ -348,18 +358,24 @@ function Get-ModuleFastPlan { $entries = $pageLeaves.catalogEntry #Get the highest version that satisfies the requirement in the inlined index, if possible - [Object]$selectedEntry = if ($entries) { + $selectedEntry = if ($entries) { [SortedSet[NuGetVersion]]$inlinedVersions = $entries.version - $inlinedVersions.Reverse() | ForEach-Object { - if ($currentModuleSpec.SatisfiedBy($PSItem)) { - Write-Debug "$currentModuleSpec`: Found satisfying version $PSItem in the inlined index." - [object]$matchingEntry = $entries | Where-Object version -EQ $PSItem - if (-not $matchingEntry) { throw 'Multiple matching Entries found for a specific version. This is a bug and should not happen' } - return $matchingEntry + + foreach ($candidate in $inlinedVersions.Reverse()) { + #Skip Prereleases unless explicitly requested + if (($candidate.IsPrerelease -or $candidate.HasMetadata) -and -not $Prerelease) { continue } + + if ($currentModuleSpec.SatisfiedBy($candidate)) { + Write-Debug "$currentModuleSpec`: Found satisfying version $candidate in the inlined index." + $matchingEntry = $entries | Where-Object version -EQ $candidate + if ($matchingEntry.count -gt 1) { throw 'Multiple matching Entries found for a specific version. This is a bug and should not happen' } + $matchingEntry + break } } } + if ($selectedEntry.count -gt 1) { throw 'Multiple Entries Selected. This is a bug.' } #Search additional pages if we didn't find it in the inlined ones $selectedEntry ??= $( Write-Debug "$currentModuleSpec`: not found in inlined index. Determining appropriate page(s) to query" @@ -386,14 +402,7 @@ function Get-ModuleFastPlan { #Start with the highest potentially matching page and work our way down until we find a match. foreach ($page in $pages) { - [Task[string][]]$getPagesTasks = Get-ModuleInfoAsync @httpContext -Uri $page.'@id' - - #This timeout loop enables ctrl-c to still function while waiting for the results - while ($false -in $getPagesTasks.IsCompleted) { - [void][Task]::WaitAll($getPagesTasks, 500) - } - - $response = $getPagesTasks.GetAwaiter().GetResult() | ConvertFrom-Json + $response = (Get-ModuleInfoAsync @httpContext -Uri $page.'@id').GetAwaiter().GetResult() | ConvertFrom-Json $pageLeaves = $response.items | ForEach-Object { if ($PSItem.packageContent -and -not $PSItem.catalogEntry.packagecontent) { @@ -408,16 +417,22 @@ function Get-ModuleFastPlan { #TODO: Dedupe as a function with above if ($entries) { [SortedSet[NuGetVersion]]$inlinedVersions = $entries.version - $inlinedVersions.Reverse() | ForEach-Object { - if ($currentModuleSpec.SatisfiedBy($PSItem)) { - Write-Debug "$currentModuleSpec`: Found satisfying version $PSItem in the additional pages." - [object]$matchingEntry = $entries | Where-Object version -EQ $PSItem + + foreach ($candidate in $inlinedVersions.Reverse()) { + #Skip Prereleases unless explicitly requested + if (($candidate.IsPrerelease -or $candidate.HasMetadata) -and -not $Prerelease) { continue } + if ($currentModuleSpec.SatisfiedBy($candidate)) { + Write-Debug "$currentModuleSpec`: Found satisfying version $candidate in the additional pages." + $matchingEntry = $entries | Where-Object version -EQ $candidate if (-not $matchingEntry) { throw 'Multiple matching Entries found for a specific version. This is a bug and should not happen' } $matchingEntry break } } } + + #Candidate found, no need to process additional pages + if ($matchingEntry) { break } } ) @@ -685,6 +700,10 @@ class ModuleFastInfo { [bool] Equals($other) { return $this.GetHashCode() -eq $other.GetHashCode() } + + static hidden [Version]Get_Prerelease([bool]$i) { + return $i.ModuleVersion.IsPrerelease -or $i.ModuleVersion.HasMetadata + } } $ModuleFastInfoTypeData = @{ @@ -694,6 +713,7 @@ $ModuleFastInfoTypeData = @{ PropertySerializationSet = 'Name', 'ModuleVersion', 'Location' SerializationDepth = 0 } +[ModuleFastInfo] | Add-Getters Update-TypeData -TypeName ModuleFastInfo @ModuleFastInfoTypeData -Force Update-TypeData -TypeName Nuget.Versioning.NugetVersion -SerializationMethod String -Force @@ -989,7 +1009,7 @@ function Get-ModuleInfoAsync { $registrationBase = $SCRIPT:__registrationIndex | ConvertFrom-Json - | Select-Object -ExpandProperty Resources + | Select-Object -ExpandProperty resources | Where-Object { $_.'@type' -match 'RegistrationsBaseUrl' } @@ -1099,7 +1119,8 @@ function Find-LocalModule { [List[[Tuple[Version, string]]]]$candidateModules = foreach ($modulePath in $modulePaths) { if (-not [Directory]::Exists($modulePath)) { - Write-Debug "PSModulePath $modulePath is configured but does not exist, skipping..." + Write-Debug "$($ModuleSpec.Name): PSModulePath $modulePath is configured but does not exist, skipping..." + $modulePaths = $modulePaths | Where-Object { $_ -ne $modulePath } continue } @@ -1200,9 +1221,9 @@ function Find-LocalModule { continue } - [string]$prerelease = $manifestData.PreRelease + [string]$prereleaseData = $manifestData.PreRelease - [NuGetVersion]$manifestVersion = [NuGetVersion]::new($manifestVersionData, $prerelease, $null) + [NuGetVersion]$manifestVersion = [NuGetVersion]::new($manifestVersionData, $prereleaseData, $null) #Re-Test against the manifest loaded version to be sure if (-not $ModuleSpec.SatisfiedBy($manifestVersion)) { @@ -1238,6 +1259,8 @@ function ConvertTo-AuthenticationHeaderValue ([PSCredential]$Credential) { function Get-StringHash ([string]$String, [string]$Algorithm = 'SHA256') { (Get-FileHash -InputStream ([MemoryStream]::new([Encoding]::UTF8.GetBytes($String))) -Algorithm $algorithm).Hash } + + #endregion Helpers ### ISSUES diff --git a/ModuleFast.tests.ps1 b/ModuleFast.tests.ps1 index d8d3786..ef38f8a 100644 --- a/ModuleFast.tests.ps1 +++ b/ModuleFast.tests.ps1 @@ -295,6 +295,20 @@ Describe 'Get-ModuleFastPlan' -Tag 'E2E' { $actual.Name | Should -Be 'Az.Accounts' $actual.RequiredVersion | Should -BeGreaterThan '2.7.3' } + + It 'Filters Prerelease Modules by Default' { + $actual = Get-ModuleFastPlan 'PrereleaseTest' + $actual.ModuleVersion | Should -Be '0.0.1' + } + It 'Shows Prerelease Modules if Prerelease is specified' { + $actual = Get-ModuleFastPlan 'PrereleaseTest' -PreRelease + $actual.ModuleVersion | Should -Be '0.0.2-newerversion' + } + It 'Detects Prerelease even if Prerelease not specified' { + $actual = Get-ModuleFastPlan 'PrereleaseTest@0.0.2-newerversion' + $actual.ModuleVersion | Should -Be '0.0.2-newerversion' + } + } Describe 'Install-ModuleFast' -Tag 'E2E' {