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
136 changes: 89 additions & 47 deletions ModuleFast.psm1
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#requires -version 7.2
using namespace Microsoft.PowerShell.Commands
using namespace System.Management.Automation
using namespace System.Management.Automation.Language
using namespace NuGet.Versioning
using namespace System.Collections
using namespace System.Collections.Concurrent
Expand Down Expand Up @@ -51,7 +52,7 @@ 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.
#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, or a .ps1/.psm1 file with a #Requires statement.
[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,
Expand Down Expand Up @@ -972,9 +973,11 @@ class ModuleFastSpec {
#BUG: We cannot implement IEquatable directly because we need to self-reference ModuleFastSpec before it exists.
#We can however just add Equals() method



#Implementation of https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1.equals
[bool]Equals($other) {
return $this.GetHashCode() -eq $other.GetHashCode()
}
#end IEquatable

[string] ToString() {
$guid = $this._Guid -ne [Guid]::Empty ? " [$($this._Guid)]" : ''
Expand All @@ -983,10 +986,6 @@ class ModuleFastSpec {
[int] GetHashCode() {
return $this.ToString().GetHashCode()
}
[bool]Equals($other) {
return $this.GetHashCode() -eq $other.GetHashCode()
}
#end IEquatable

#IComparable
#Implementation of https://learn.microsoft.com/en-us/dotnet/api/system.icomparable-1.equals
Expand Down Expand Up @@ -1355,56 +1354,38 @@ filter ConvertFrom-RequiredSpec {
[OutputType([ModuleFastSpec[]])]
param(
[Parameter(Mandatory, ParameterSetName = 'File')][string]$RequiredSpecPath,
[Parameter(Mandatory, ParameterSetName = 'Object')][object]$RequiredSpec
[Parameter(Mandatory, ParameterSetName = 'Object')]$RequiredSpec
)
$ErrorActionPreference = 'Stop'

#Merge Required Data into spec path
#If a spec path was specified, resolve it into RequiredSpec
if ($RequiredSpecPath) {
$uri = $RequiredSpecPath -as [Uri]

$RequiredData = if ($uri.scheme -in 'http', 'https') {
[string]$content = (Invoke-WebRequest -Uri $uri).Content
if ($content.StartsWith('@{')) {
$tempFile = [io.path]::GetTempFileName()
$content > $tempFile
Import-PowerShellDataFile -Path $tempFile
} else {
ConvertFrom-Json $content -Depth 5
}
} else {
#Assume this is a local if a URL above didn't match
$resolvedPath = Resolve-Path $RequiredSpecPath
$extension = [Path]::GetExtension($resolvedPath)
if ($extension -eq '.psd1') {
Import-PowerShellDataFile -Path $resolvedPath
} elseif ($extension -in '.json', '.jsonc') {
Get-Content -Path $resolvedPath -Raw | ConvertFrom-Json -Depth 5
} else {
throw [NotSupportedException]'Only .psd1 and .json files are supported to import to this command'
}
}
$specFromUri = Read-RequiredSpecFile $RequiredSpecPath
$RequiredSpec = $specFromUri
}

if ($RequiredData -is [PSCustomObject] -and $RequiredData.psobject.baseobject -isnot [IDictionary]) {
$PassThruTypes = [string],
[string[]],
[ModuleFastSpec],
[ModuleFastSpec[]],
[ModuleSpecification[]],
[ModuleSpecification]

if ($RequiredSpec.GetType() -in $PassThruTypes) { return [ModuleFastSpec[]]$requiredSpec }

if ($RequiredSpec -is [PSCustomObject] -and $RequiredSpec.psobject.baseobject -isnot [IDictionary]) {
Write-Debug 'PSCustomObject-based Spec detected, converting to hashtable'
$requireHT = @{}
$RequiredData.psobject.Properties
| ForEach-Object {
$RequiredSpec.psobject.Properties
| ForEach-Object {
$requireHT.Add($_.Name, $_.Value)
}
$RequiredData = $requireHT
#Will be process by IDictionary case below
$RequiredSpec = $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 ($RequiredSpec -is [IDictionary]) {
foreach ($kv in $RequiredSpec.GetEnumerator()) {
if ($kv.Value -is [IDictionary]) {
throw [NotImplementedException]'TODO: PSResourceGet/PSDepend full syntax'
}
Expand All @@ -1423,9 +1404,70 @@ filter ConvertFrom-RequiredSpec {
#All other potential options (<=, @, :, etc.) are a direct merge
[ModuleFastSpec]"$($kv.Name)$($kv.Value)"
}
} else {
throw [NotImplementedException]'TODO: Support simple array based json strings'
return
}

if ($RequiredSpec -is [Object[]] -and ($true -notin $RequiredSpec.GetEnumerator().Foreach{ $PSItem -isnot [string] })) {
Write-Debug 'RequiredData array detected and contains all string objects. Converting to string[]'
$RequiredSpec = [string[]]$RequiredSpec
}

throw [InvalidDataException]'Could not evaluate the Required Specification to a known format.'
}

function Read-RequiredSpecFile ($RequiredSpecPath) {
if ($uri.scheme -in 'http', 'https') {
[string]$content = (Invoke-WebRequest -Uri $uri).Content
if ($content.StartsWith('@{')) {
$tempFile = [io.path]::GetTempFileName()
$content > $tempFile
return Import-PowerShellDataFile -Path $tempFile
} else {
$json = ConvertFrom-Json $content -Depth 5
return $json
}
}

#Assume this is a local if a URL above didn't match
$resolvedPath = Resolve-Path $RequiredSpecPath
$extension = [Path]::GetExtension($resolvedPath)

if ($extension -eq '.psd1') {
$manifestData = Import-PowerShellDataFile -Path $resolvedPath
if ($manifestData.ModuleVersion) {
[ModuleSpecification[]]$requiredModules = $manifestData.RequiredModules
Write-Debug 'Detected a Module Manifest, evaluating RequiredModules'
if ($requiredModules.count -eq 0) {
throw [InvalidDataException]'The manifest does not have a RequiredModules key so ModuleFast does not know what this module requires. See Get-Help about_module_manifests for more.'
}
return , $requiredModules
} else {
Write-Debug 'Did not detect a module manifest, passing through as-is'
return $manifestData
}
}

if ($extension -in '.ps1', '.psm1') {
Write-Debug 'PowerShell Script/Module file detected, checking for #Requires'
$ast = [Parser]::ParseFile($resolvedPath, [ref]$null, [ref]$null)
[ModuleSpecification[]]$requiredModules = $ast.ScriptRequirements.RequiredModules

if ($RequiredModules.count -eq 0) {
throw [NotSupportedException]'The script does not have a #Requires -Module statement so ModuleFast does not know what this module requires. See Get-Help about_requires for more.'
}
return , $requiredModules
}

if ($extension -in '.json', '.jsonc') {
$json = Get-Content -Path $resolvedPath -Raw | ConvertFrom-Json -Depth 5
if ($json -is [Object[]] -and $false -notin $json.getenumerator().foreach{ $_ -is [string] }) {
Write-Debug 'Detected a JSON array of strings, converting to string[]'
return , [string[]]$json
}
return $json
}

throw [NotSupportedException]'Only .ps1, psm1, .psd1, and .json files are supported to import to this command'
}

filter Resolve-FolderVersion([NuGetVersion]$version) {
Expand Down
60 changes: 56 additions & 4 deletions ModuleFast.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -505,19 +505,71 @@ Describe 'Install-ModuleFast' -Tag 'E2E' {
It 'Installs from <Name> SpecFile' {
$SCRIPT:Mocks = Resolve-Path "$PSScriptRoot/Test/Mocks"
$specFilePath = Join-Path $Mocks $File
Install-ModuleFast @imfParams -Path $specFilePath
$modulesToInstall = Install-ModuleFast @imfParams -Path $specFilePath -WhatIf
#TODO: Verify individual modules and versions
$modulesToInstall | Should -Not -BeNullOrEmpty
} -TestCases @(
@{
Name = 'PowerShell Data File';
Name = 'PowerShell Data File'
File = 'ModuleFast.requires.psd1'
},
@{
Name = 'JSON';
Name = 'JSON'
File = 'ModuleFast.requires.json'
},
@{
Name = 'JSONArray';
Name = 'JSONArray'
File = 'ModuleFastArray.requires.json'
},
@{
Name = 'ScriptRequires'
File = 'RequiresScript.ps1'
},
@{
Name = 'ScriptModule'
File = 'RequiresModule.psm1'
}
)

It 'Fails for script if #Requires is not Present' {
$scriptPath = Join-Path $testDrive 'norequires.ps1'
{
'There is no requires here!'
} | Out-File $scriptPath

{ Install-ModuleFast @imfParams -Path $scriptPath }
| Should -Throw 'The script does not have a #Requires*'
}
It 'Fails for module if #Requires is not Present' {
$scriptPath = Join-Path $testDrive 'norequires.psm1'
{
'There is no requires here!'
} | Out-File $scriptPath

{ Install-ModuleFast @imfParams -Path $scriptPath }
| Should -Throw 'The script does not have a #Requires*'
}
It 'Fails if Module Manifest and RequiredModules is missing' {
$scriptPath = Join-Path $testDrive 'testmanifestnorequires.psd1'
"@{
'ModuleVersion' = '1.0.0'
}" | Out-File $scriptPath
{ Install-ModuleFast @imfParams -Path $scriptPath }
| Should -Throw 'The manifest does not have a RequiredMOdules key*'
}
It 'Resolves Module Manifest RequiredModules' {
$scriptPath = Join-Path $testDrive 'testmanifest.psd1'
"@{
'ModuleVersion' = '1.0.0'
'RequiredModules' = @(
'PreReleaseTest'
@{
ModuleName = 'Az.Accounts'
ModuleVersion = '2.7.0'
}
)
}" | Out-File $scriptPath
$modules = Install-ModuleFast @imfParams -Path $scriptPath -WhatIf
$modules.count | Should -Be 2
}
}
1 change: 1 addition & 0 deletions Test/Mocks/RequiresModule.psm1
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#requires -Module PreReleaseTest
1 change: 1 addition & 0 deletions Test/Mocks/RequiresScript.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#requires -Module PreReleaseTest