From fd1237c198780ef3f07e8b771bb00a172a0aad9a Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Sat, 16 Dec 2023 19:51:56 -0800 Subject: [PATCH 1/3] Update TaskSpecMap --- ModuleFast.psm1 | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/ModuleFast.psm1 b/ModuleFast.psm1 index 463220f..268c8ec 100644 --- a/ModuleFast.psm1 +++ b/ModuleFast.psm1 @@ -2,6 +2,7 @@ using namespace Microsoft.PowerShell.Commands using namespace System.Management.Automation using namespace NuGet.Versioning +using namespace System.Collections using namespace System.Collections.Concurrent using namespace System.Collections.Generic using namespace System.Collections.Specialized @@ -46,6 +47,8 @@ function Install-ModuleFast { [AllowEmptyCollection()] [Parameter(Mandatory, Position = 0, ValueFromPipeline, ParameterSetName = 'Specification')][ModuleFastSpec[]]$Specification, + #Provide a required module specification path to install from. This can be a local psd1/json file, or a remote URL with a psd1/json file in supported manifest formats. + [Parameter(Mandatory, ParameterSetName = 'Path')][string]$Path, #Where to install the modules. This defaults to the builtin module path on non-windows and a custom LOCALAPPDATA location on Windows. [string]$Destination, #The repository to scan for modules. TODO: Multi-repo support @@ -111,6 +114,7 @@ function Install-ModuleFast { process { #We initialize and type the container list here because there is a bug where the ParameterSet is not correct in the begin block if the pipeline is used. Null conditional keeps it from being reinitialized + [List[ModuleFastSpec]]$ModulesToInstall = @() switch ($PSCmdlet.ParameterSetName) { 'Specification' { [List[ModuleFastSpec]]$ModulesToInstall ??= @() @@ -125,6 +129,10 @@ function Install-ModuleFast { $ModulesToInstall.Add($ModuleToInstall) } break + + } + 'Path' { + $ModulesToInstall = ConvertFrom-RequiredSpec -RequiredSpecPath $Path } } } @@ -141,14 +149,11 @@ function Install-ModuleFast { #If we do not have an explicit implementation plan, fetch it #This is done so that Get-ModuleFastPlan | Install-ModuleFastPlan and Install-ModuleFastPlan have the same flow. - [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 -PreRelease:$Prerelease.IsPresent - } - 'ModuleFastInfo' { - $ModulesToInstall.ToArray() - } + [ModuleFastInfo[]]$plan = if ($PSCmdlet.ParameterSetName -eq 'ModuleFastInfo') { + $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 } $WhatIfPreference = $currentWhatIfPreference @@ -270,7 +275,7 @@ function Get-ModuleFastPlan { [HashSet[ModuleFastInfo]]$modulesToInstall = @{} # We use this as a fast lookup table for the context of the request - [Dictionary[Task[String], ModuleFastSpec]]$resolveTasks = @{} + [Dictionary[Task[String], ModuleFastSpec]]$taskSpecMap = @{} #We use this to track the tasks that are currently running #We dont need this to be ConcurrentList because we only manipulate it in the "main" runspace. @@ -288,7 +293,7 @@ function Get-ModuleFastPlan { } $task = Get-ModuleInfoAsync @httpContext -Endpoint $Source -Name $moduleSpec.Name - $resolveTasks[$task] = $moduleSpec + $taskSpecMap[$task] = $moduleSpec $currentTasks.Add($task) } @@ -309,7 +314,7 @@ function Get-ModuleFastPlan { #TODO: Perform a HEAD query to see if something has changed [Task[string]]$completedTask = $currentTasks[$thisTaskIndex] - [ModuleFastSpec]$currentModuleSpec = $resolveTasks[$completedTask] + [ModuleFastSpec]$currentModuleSpec = $taskSpecMap[$completedTask] Write-Debug "$currentModuleSpec`: Processing Response" # We use GetAwaiter so we get proper error messages back, as things such as network errors might occur here. @@ -450,7 +455,7 @@ function Get-ModuleFastPlan { if (-not $modulesToInstall.Add($selectedModule)) { Write-Debug "$selectedModule already exists in the install plan. Skipping..." #TODO: Fix the flow so this isn't stated twice - [void]$resolveTasks.Remove($completedTask) + [void]$taskSpecMap.Remove($completedTask) [void]$currentTasks.Remove($completedTask) $tasksCompleteCount++ continue @@ -521,7 +526,7 @@ function Get-ModuleFastPlan { Write-Debug "$currentModuleSpec`: Fetching dependency $dependencySpec" #TODO: Do a direct version lookup if the dependency is a required version $task = Get-ModuleInfoAsync @httpContext -Endpoint $Source -Name $dependencySpec.Name - $resolveTasks[$task] = $dependencySpec + $taskSpecMap[$task] = $dependencySpec #Used to track progress as tasks can get removed $resolveTaskCount++ @@ -531,7 +536,7 @@ function Get-ModuleFastPlan { #Putting .NET methods in a try/catch makes errors in them terminating try { - [void]$resolveTasks.Remove($completedTask) + [void]$taskSpecMap.Remove($completedTask) [void]$currentTasks.Remove($completedTask) $tasksCompleteCount++ } catch { From 69581bdc9d5ab9a5c80aac17eb3c5ad25d1589b6 Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Sat, 16 Dec 2023 19:54:25 -0800 Subject: [PATCH 2/3] Add Test Moks --- Test/Mocks/ModuleFast.requires.json | 7 +++++++ Test/Mocks/ModuleFast.requires.psd1 | 7 +++++++ Test/Mocks/ModuleFastArray.requires.json | 7 +++++++ 3 files changed, 21 insertions(+) create mode 100644 Test/Mocks/ModuleFast.requires.json create mode 100644 Test/Mocks/ModuleFast.requires.psd1 create mode 100644 Test/Mocks/ModuleFastArray.requires.json diff --git a/Test/Mocks/ModuleFast.requires.json b/Test/Mocks/ModuleFast.requires.json new file mode 100644 index 0000000..abffec9 --- /dev/null +++ b/Test/Mocks/ModuleFast.requires.json @@ -0,0 +1,7 @@ +{ + "PnP.PowerShell": "2.2.156-nightly", + "Pester": "@5.4.0", + "Az.Accounts": ":[2.0.0, 2.13.2)", + "ImportExcel": "latest", + "PSScriptAnalyzer": "<=1.21.0" +} diff --git a/Test/Mocks/ModuleFast.requires.psd1 b/Test/Mocks/ModuleFast.requires.psd1 new file mode 100644 index 0000000..0d4b3d2 --- /dev/null +++ b/Test/Mocks/ModuleFast.requires.psd1 @@ -0,0 +1,7 @@ +@{ + 'ImportExcel' = 'latest' + 'PnP.PowerShell' = '2.2.156-nightly' + 'PSScriptAnalyzer' = '<=1.21.0' + 'Pester' = '@5.4.0' + 'Az.Accounts' = ':[2.0.0, 2.13.2)' +} diff --git a/Test/Mocks/ModuleFastArray.requires.json b/Test/Mocks/ModuleFastArray.requires.json new file mode 100644 index 0000000..15cb139 --- /dev/null +++ b/Test/Mocks/ModuleFastArray.requires.json @@ -0,0 +1,7 @@ +[ + "ImportExcel", + "PnP.PowerShell@2.2.156-nightly", + "PSScriptAnalyzer<=1.21.0", + "Pester@5.4.0", + "Az.Accounts:[2.0.0, 2.13.2)" +] From bebf4348b707a3ce145baae81a05d17a328a1eff Mon Sep 17 00:00:00 2001 From: Justin Grote Date: Sat, 16 Dec 2023 20:59:34 -0800 Subject: [PATCH 3/3] Fixup specfile paths and add tests --- ModuleFast.psm1 | 20 +++++++++++++++++++- ModuleFast.tests.ps1 | 20 ++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/ModuleFast.psm1 b/ModuleFast.psm1 index 268c8ec..02bde2f 100644 --- a/ModuleFast.psm1 +++ b/ModuleFast.psm1 @@ -1340,6 +1340,7 @@ filter ConvertFrom-RequiredSpec { ) $ErrorActionPreference = 'Stop' + #Merge Required Data into spec path if ($RequiredSpecPath) { $uri = $RequiredSpecPath -as [Uri] @@ -1366,7 +1367,24 @@ filter ConvertFrom-RequiredSpec { } } - if ($RequiredData -is [IDictionary]) { + if ($RequiredData -is [PSCustomObject] -and $RequiredData.psobject.baseobject -isnot [IDictionary]) { + Write-Debug 'PSCustomObject-based Spec detected, converting to hashtable' + $requireHT = @{} + $RequiredData.psobject.Properties + | ForEach-Object { + $requireHT.Add($_.Name, $_.Value) + } + $RequiredData = $requireHT + } + + if ($RequiredData -is [Object[]] -and ($true -notin $RequiredData.GetEnumerator().Foreach{ $PSItem -isnot [string] })) { + Write-Debug 'RequiredData array detected and contains all string objects. Converting to string[]' + $requiredData = [string[]]$RequiredData + } + + if ($RequiredData -is [string[]]) { + return [ModuleFastSpec[]]$RequiredData + } elseif ($RequiredData -is [IDictionary]) { foreach ($kv in $RequiredData.GetEnumerator()) { if ($kv.Value -is [IDictionary]) { throw [NotImplementedException]'TODO: PSResourceGet/PSDepend full syntax' diff --git a/ModuleFast.tests.ps1 b/ModuleFast.tests.ps1 index 6d28ae6..646dea8 100644 --- a/ModuleFast.tests.ps1 +++ b/ModuleFast.tests.ps1 @@ -501,4 +501,24 @@ Describe 'Install-ModuleFast' -Tag 'E2E' { Install-ModuleFast @imfParams 'PrereleaseTest@0.0.1-bprerelease' -WarningVariable actual *>&1 | Out-Null $actual | Should -BeLike '*is newer than existing prerelease version*' } + + + It 'Installs from SpecFile' { + $SCRIPT:Mocks = Resolve-Path "$PSScriptRoot/Test/Mocks" + $specFilePath = Join-Path $Mocks $File + Install-ModuleFast @imfParams -Path $specFilePath + } -TestCases @( + @{ + Name = 'PowerShell Data File'; + File = 'ModuleFast.requires.psd1' + }, + @{ + Name = 'JSON'; + File = 'ModuleFast.requires.json' + }, + @{ + Name = 'JSONArray'; + File = 'ModuleFastArray.requires.json' + } + ) }