Skip to content

Commit

Permalink
🎉 Initial SpecFile Support
Browse files Browse the repository at this point in the history
Adds support for SpecFiles, which are file based definitions of packages that should be installed. Initial support for psd1 and JSON based simple spec files.
  • Loading branch information
JustinGrote authored Dec 17, 2023
1 parent 60e9a0b commit 52d87ee
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 15 deletions.
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 'PrereleaseTest@0.0.1-bprerelease' -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",
"PnP.PowerShell@2.2.156-nightly",
"PSScriptAnalyzer<=1.21.0",
"Pester@5.4.0",
"Az.Accounts:[2.0.0, 2.13.2)"
]

0 comments on commit 52d87ee

Please sign in to comment.