PSDependScripts/PSResourceGet.ps1

<#
    .SYNOPSIS
        Installs a PowerShell resource from a PowerShell repository using PSResourceGet.
 
    .DESCRIPTION
        Installs a PowerShell module from a PowerShell repository (such as the PowerShell Gallery)
        using PSResourceGet (Microsoft.PowerShell.PSResourceGet), the successor to the deprecated
        PowerShellGet v2 module. PSResourceGet must be installed before using this provider.
 
        Prefer this provider over PSGalleryModule for new projects. PSGalleryModule targets
        PowerShellGet v2 (Install-Module); this provider targets PSResourceGet v3 (Install-PSResource).
 
        Relevant Dependency metadata:
            Name: The name of the module to install
            Version: Used to identify existing installs and as -Version for installation.
                        Supports NuGet range syntax (e.g. '[1.0.0, ]'). Defaults to 'latest'.
            Target: Used as -Scope for Install-PSResource (CurrentUser or AllUsers).
                        If this is a filesystem path, Save-PSResource is used instead.
                        Defaults to 'CurrentUser'.
            AddToPath: If Target is a filesystem path, prepend that path to $env:PSModulePath.
            Credential: A [PSCredential] for authenticating against a private repository.
                        Use Get-Credential or [PSCredential]::new() to construct one.
 
        This provider calls the following PSResourceGet cmdlets:
            - Find-PSResource
            - Install-PSResource
            - Save-PSResource
 
    .PARAMETER Dependency
        The PSDepend.Dependency object passed by Invoke-PSDepend. Not supplied directly by the caller.
 
    .PARAMETER Repository
        PSResource repository to download from.
        Defaults to PSGallery.
 
    .PARAMETER NoClobber
        Prevents installation if the module would overwrite commands already present on the system.
 
    .PARAMETER AcceptLicense
        Suppresses the license acceptance prompt during installation.
 
    .PARAMETER Prerelease
        If specified, allows installation of prerelease versions.
 
        If specified along with version 'latest', a prerelease will be selected
        if it is the most recent available version.
 
        Sorting assumes prereleases are named appropriately
        (e.g. alpha < beta < rc).
 
    .PARAMETER Import
        If specified, imports the module into the global scope after installation.
 
        Deprecated. Use PSDependAction = 'Import' instead. This parameter may be
        removed in a future release.
 
    .PARAMETER PSDependAction
        Test, Install, or Import the module.
        Defaults to Install.
 
        Test: Returns $true or $false depending on whether the dependency is present
        Install: Installs the dependency
        Import: Imports the dependency
 
    .EXAMPLE
        @{
            BuildHelpers = @{
                DependencyType = 'PSResourceGet'
                Version = 'latest'
            }
            InvokeBuild = @{
                DependencyType = 'PSResourceGet'
                Version = '3.2.1'
            }
        }
 
        # Install the latest BuildHelpers and version 3.2.1 of InvokeBuild from PSGallery.
        # Omitting Version, or setting it to '', also resolves to latest.
 
    .EXAMPLE
        @{
            BuildHelpers = @{
                DependencyType = 'PSResourceGet'
                Target = 'C:\Build'
            }
        }
 
        # Save the latest BuildHelpers module from PSGallery to C:\Build
        # (i.e. C:\Build\BuildHelpers will be the module folder)
 
    .EXAMPLE
        @{
            BuildHelpers = @{
                DependencyType = 'PSResourceGet'
                Parameters = @{
                    Repository = 'PSPrivateGallery'
                }
            }
        }
 
        # Install the latest BuildHelpers from a registered private repository.
        # Register the repository first with Register-PSResourceRepository.
        #
        # Examples of private repositories include:
        # - Artifactory
        # - ProGet
        # - GitLab Package Registry
 
    .EXAMPLE
        @{
            'vmware.powercli' = @{
                DependencyType = 'PSResourceGet'
                Parameters = @{
                    Prerelease = $true
                }
            }
        }
 
        # Install the latest version of PowerCLI, allowing prerelease versions.
#>


[CmdletBinding()]
param(
    [PSTypeName('PSDepend.Dependency')]
    [psobject[]]$Dependency,

    [AllowNull()]
    [string]$Repository = 'PSGallery',

    [switch]$NoClobber,

    [switch]$AcceptLicense,

    [switch]$Prerelease,

    [switch]$Import,

    [ValidateSet('Test', 'Install', 'Import')]
    [string[]]$PSDependAction = @('Install')
)

if (-not (Get-Command -Name Install-PSResource -ErrorAction SilentlyContinue)) {
    Write-Error "PSResourceGet (Microsoft.PowerShell.PSResourceGet) is required but not available. Install it before using the PSResourceGet dependency type."
    return
}

# Extract data from Dependency
$DependencyName = $Dependency.DependencyName
$Name = $Dependency.Name
if (-not $Name) {
    $Name = $DependencyName
}

$Version = $Dependency.Version
if (-not $Version) {
    $Version = 'latest'
}

# Target doubles as Scope: AllUsers/CurrentUser = install scope; any other value = filesystem path
if (-not $Dependency.Target) {
    $Scope = 'CurrentUser'
}
else {
    $Scope = $Dependency.Target
}

$Credential = $Dependency.Credential

if ('AllUsers', 'CurrentUser' -notcontains $Scope) {
    $command = 'save'
}
else {
    $command = 'install'
}

Write-Verbose -Message "Getting dependency [$Name] from PowerShell repository [$Repository]"

if ($Repository) {
    $validRepo = Get-PSResourceRepository -Name $Repository -Verbose:$false -ErrorAction SilentlyContinue
    if (-not $validRepo) {
        Write-Error "[$Repository] has not been set up as a valid PowerShell repository."
        return
    }
}

# TrustRepository defaults to $true so unattended / CI installs do not hang on a trust prompt
$params = @{
    Name            = $Name
    TrustRepository = $true
}

if ($PSBoundParameters.ContainsKey('NoClobber')) {
    $params.Add('NoClobber', $NoClobber)
}

if ($PSBoundParameters.ContainsKey('Prerelease')) {
    $params.Add('Prerelease', $Prerelease)
}

if ($PSBoundParameters.ContainsKey('AcceptLicense')) {
    $params.Add('AcceptLicense', $AcceptLicense)
}

if ($Repository) {
    $params.Add('Repository', $Repository)
}

if ($Version -and $Version -ne 'latest') {
    $params.Add('Version', $Version)
}

if ($Credential) {
    $params.Add('Credential', $Credential)
}

if ($command -eq 'save') {
    $ModuleName = Join-Path $Scope $Name
}
elseif ($command -eq 'install') {
    $ModuleName = $Name
}

# Filter params to only those accepted by the target command
$targetCmd = if ($command -eq 'save') {
    'Save-PSResource' 
}
else {
    'Install-PSResource' 
}
$availableParameters = (Get-Command $targetCmd).Parameters
$tempParams = $params.Clone()
foreach ($thisParameter in $params.Keys) {
    if (-not $availableParameters.ContainsKey($thisParameter)) {
        Write-Verbose -Message "Removing parameter [$thisParameter] from [$targetCmd] as it is not available"
        $tempParams.Remove($thisParameter)
    }
}
$params = $tempParams.Clone()

Add-ToPsModulePathIfRequired -Dependency $Dependency -Action $PSDependAction

$Existing = Get-Module -ListAvailable -Name $ModuleName -ErrorAction SilentlyContinue

if ($Existing) {
    Write-Verbose "Found existing module [$Name]"
    # Thanks to Brandon Padgett!
    $ExistingVersion = $Existing | Measure-Object -Property Version -Maximum | Select-Object -ExpandProperty Maximum
    $FindModuleParams = @{ Name = $Name }
    if ($Repository) {
        $FindModuleParams.Add('Repository', $Repository)
    }
    if ($Credential) {
        $FindModuleParams.Add('Credential', $Credential)
    }
    if ($Prerelease) {
        $FindModuleParams.Add('Prerelease', $true)
    }

    # Version string, and that version is already installed (may not be the maximum)
    $matchedExisting = if ($Version -and $Version -ne 'latest') {
        $Existing | Where-Object {
            Test-VersionEquality -ReferenceVersion $_.Version -DifferenceVersion $Version
        } | Select-Object -First 1
    }
    if ($matchedExisting) {
        Write-Verbose "You have the requested version [$Version] of [$Name]"
        Import-PSDependModule -Name $ModuleName -Action $PSDependAction -Version $matchedExisting.Version

        if ($PSDependAction -contains 'Test') {
            return $true
        }
        return $null
    }

    $GalleryVersion = Find-PSResource @FindModuleParams | Measure-Object -Property Version -Maximum | Select-Object -ExpandProperty Maximum
    # Compare using SemanticVersion first (PSResourceGet uses SemVer); fall back to System.Version
    [System.Version]$parsedVersion = $null
    [System.Version]$parsedGalleryVersion = $null
    [System.Management.Automation.SemanticVersion]$parsedSemanticVersion = $null
    [System.Management.Automation.SemanticVersion]$parsedTempSemanticVersion = $null
    $existingIsUpToDate = if (
        [System.Management.Automation.SemanticVersion]::TryParse([string]$ExistingVersion, [ref]$parsedSemanticVersion) -and
        [System.Management.Automation.SemanticVersion]::TryParse([string]$GalleryVersion, [ref]$parsedTempSemanticVersion)
    ) {
        $parsedTempSemanticVersion -le $parsedSemanticVersion
    }
    elseif (
        [System.Version]::TryParse([string]$ExistingVersion, [ref]$parsedVersion) -and
        [System.Version]::TryParse([string]$GalleryVersion, [ref]$parsedGalleryVersion)
    ) {
        $parsedGalleryVersion -le $parsedVersion
    }
    else {
        $false
    }

    # latest, and we have latest
    if ($Version -and ($Version -eq 'latest' -or $Version -eq '') -and $existingIsUpToDate) {
        Write-Verbose "You have the latest version of [$Name], with installed version [$ExistingVersion] and repository version [$GalleryVersion]"
        Import-PSDependModule -Name $ModuleName -Action $PSDependAction -Version $ExistingVersion

        if ($PSDependAction -contains 'Test') {
            return $true
        }
        return $null
    }
    Write-Verbose "Continuing to install [$Name]: Requested version [$Version], existing version [$ExistingVersion]"
}

# No dependency found, return false if we're testing alone...
if ($PSDependAction -contains 'Test' -and $PSDependAction.count -eq 1) {
    return $false
}

if ($PSDependAction -contains 'Install') {
    if ('AllUsers', 'CurrentUser' -contains $Scope) {
        Write-Verbose "Installing [$Name] with scope [$Scope]"
        Install-PSResource @params -Scope $Scope
    }
    else {
        Write-Verbose "Saving [$Name] to path [$Scope]"
        Write-Verbose "Creating directory path to [$Scope]"
        if (-not (Test-Path $Scope -ErrorAction SilentlyContinue)) {
            $null = New-Item -ItemType Directory -Path $Scope -Force -ErrorAction SilentlyContinue
        }
        Save-PSResource @params -Path $Scope
    }
}

# Conditional import — params['Version'] may be a NuGet range; resolve to a concrete installed version
$importVs = $params['Version']
if ($importVs -and $importVs -match '[\[\](,]') {
    $importVs = Get-Module -ListAvailable -Name $ModuleName -ErrorAction SilentlyContinue |
        Measure-Object -Property Version -Maximum |
        Select-Object -ExpandProperty Maximum
}
Import-PSDependModule -Name $ModuleName -Action $PSDependAction -Version $importVs