Skip to content

🎉 Initial SpecFile Support #17

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Dec 17, 2023
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
53 changes: 38 additions & 15 deletions ModuleFast.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 ??= @()
Expand All @@ -125,6 +129,10 @@ function Install-ModuleFast {
$ModulesToInstall.Add($ModuleToInstall)
}
break

}
'Path' {
$ModulesToInstall = ConvertFrom-RequiredSpec -RequiredSpecPath $Path
}
}
}
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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)
}

Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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++

Expand All @@ -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 {
Expand Down Expand Up @@ -1335,6 +1340,7 @@ filter ConvertFrom-RequiredSpec {
)
$ErrorActionPreference = 'Stop'

#Merge Required Data into spec path
if ($RequiredSpecPath) {
$uri = $RequiredSpecPath -as [Uri]

Expand All @@ -1361,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'
Expand Down
20 changes: 20 additions & 0 deletions ModuleFast.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -501,4 +501,24 @@ Describe 'Install-ModuleFast' -Tag 'E2E' {
Install-ModuleFast @imfParams '[email protected]' -WarningVariable actual *>&1 | Out-Null
$actual | Should -BeLike '*is newer than existing prerelease version*'
}


It 'Installs from <Name> 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'
}
)
}
7 changes: 7 additions & 0 deletions Test/Mocks/ModuleFast.requires.json
Original file line number Diff line number Diff line change
@@ -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"
}
7 changes: 7 additions & 0 deletions Test/Mocks/ModuleFast.requires.psd1
Original file line number Diff line number Diff line change
@@ -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)'
}
7 changes: 7 additions & 0 deletions Test/Mocks/ModuleFastArray.requires.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
"ImportExcel",
"[email protected]",
"PSScriptAnalyzer<=1.21.0",
"[email protected]",
"Az.Accounts:[2.0.0, 2.13.2)"
]