PSDependScripts/Chocolatey.ps1

<#
    .SYNOPSIS
    Installs a package from a Chocolatey repository.
 
    .DESCRIPTION
    Installs a package from a Chocolatey repository like the Chocolatey community repository.
 
    Relevant Dependency metadata:
        Name: The name of the package
        Version: Used to identify existing installs meeting this criteria. Defaults to 'latest'
        Source: Source Uri. Defaults to https://community.chocolatey.org/api/v2/
 
    .PARAMETER Dependency
    Dependency to process
 
    .PARAMETER Force
    If specified and the package is already installed, force the install again.
 
    .PARAMETER ChocoInstallScriptUrl
    Url to the script used to bootstrap Chocolatey when choco.exe is not found.
    Defaults to https://community.chocolatey.org/install.ps1
 
    .PARAMETER PSDependAction
    Test, or Install the package. Defaults to Install
 
    Test: Return true or false on whether the dependency is in place
    Install: Install the dependency
 
    .EXAMPLE
    @{
        'git' = @{
            DependencyType = 'Chocolatey'
            Version = '2.0.2'
        }
    }
 
    # Install version 2.0.2 of git from the Chocolatey community repository
 
    .EXAMPLE
    @{
        'git' = @{
            DependencyType = 'Chocolatey'
            Source = 'https://feed.mycompany.com'
        }
    }
 
    # Install the latest version of git from the Chocolatey feed at https://feed.mycompany.com
 
    .EXAMPLE
    @{
        PSDependOptions = @{
            DependencyType = 'Chocolatey'
        }
        'git.portable' = @{
            Version = 'latest'
            Parameters = @{
                Force = $true
            }
        }
        'lessmsi' = 'latest'
        'putty' = 'latest'
    }
 
    # Installs the list of Chocolatey packages from the Chocolatey community repository using the Global PSDependOptions to limit repetition.
 
#>

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

    [switch]$Force,

    [string]$ChocoInstallScriptUrl = 'https://community.chocolatey.org/install.ps1',

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

function Get-ChocoVersion {
    [CmdletBinding()]
    param ()

    $invokeExternalCommandSplat = @{
        Command   = 'choco.exe'
        Arguments = @('--version')
        PassThru  = $true
    }
    $rawVersion = [string](Invoke-ExternalCommand @invokeExternalCommandSplat | Select-Object -First 1)
    [System.Version]$parsedVersion = $null
    # Strip prerelease/build metadata (e.g. 2.2.2-beta) before parsing
    if ([System.Version]::TryParse(($rawVersion -replace '[-+].*$'), [ref]$parsedVersion)) {
        $parsedVersion
    }
    else {
        # Assume a modern CLI when the version cannot be determined
        [System.Version]'2.0'
    }
}

function Get-ChocoInstalledPackage {
    [CmdletBinding()]
    param (
        [string]$Name
    )

    $chocoParams = @(
        'list',
        "$Name",
        '--limit-output',
        '--exact'
    )
    # Chocolatey 2.0 removed --local-only ('choco list' is now local-only by default);
    # before 2.0, 'choco list' queried remote sources unless the flag was passed
    if ((Get-ChocoVersion).Major -lt 2) {
        $chocoParams += '--local-only'
    }
    $invokeExternalCommandSplat = @{
        Command   = 'choco.exe'
        Arguments = $chocoParams
        PassThru  = $true
    }
    $convertFromCsvSplat = @{
        Header    = 'Name', 'Version'
        Delimiter = "|"
    }
    Invoke-ExternalCommand @invokeExternalCommandSplat | ConvertFrom-Csv @convertFromCsvSplat
}

function Get-ChocoLatestPackage {
    [CmdletBinding()]
    param (
        [string]$Name,

        [string]$Source,

        [Management.Automation.PSCredential]$Credential
    )

    # 'choco search' queries remote sources on both 1.x and 2.x; 'choco list' stopped
    # querying remote sources in Chocolatey 2.0 and rejects URL sources (issue #187)
    $chocoParams = @('search', "$Name", '--limit-output', '--exact')
    if ($Source) {
        $chocoParams += "--source='$Source'"
    }

    if ($Credential) {
        $username = $credential.UserName
        $password = $credential.GetNetworkCredential().Password
        $chocoParams += "--username='$username'"
        $chocoParams += "--password='$password'"
    }

    $invokeExternalCommandSplat = @{
        Command   = 'choco.exe'
        Arguments = $chocoParams
        PassThru  = $true
    }
    $convertFromCsvSplat = @{
        Header    = 'Name', 'Version'
        Delimiter = "|"
    }
    Invoke-ExternalCommand @invokeExternalCommandSplat | ConvertFrom-Csv @convertFromCsvSplat
}

function Invoke-ChocoInstallPackage {
    [CmdletBinding()]
    param (
        [string]$Name,

        [string]$Version,

        [string]$Source,

        [switch]$Force,

        [Management.Automation.PSCredential]$Credential
    )

    $chocoParams = @(
        'upgrade',
        "$Name",
        '--limit-output',
        '--exact',
        '--no-progress',
        '--allow-downgrade',
        '--yes' # Ensure that we do not get prompted to confirm the install
    )
    if ($Force.IsPresent) {
        $chocoParams += "--force"
    }

    if ($Source) {
        $chocoParams += "--source='$Source'"
    }

    if ($Version -and $Version -ne 'latest' -and $Version -ne '') {
        $chocoParams += "--version='$Version'"
    }

    if ($Credential) {
        $username = $credential.UserName
        $password = $credential.GetNetworkCredential().Password
        $chocoParams += "--username='$username'"
        $chocoParams += "--password='$password'"
    }

    $invokeExternalCommandSplat = @{
        Command   = 'choco.exe'
        Arguments = $chocoParams
    }
    Invoke-ExternalCommand @invokeExternalCommandSplat
}

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

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

$Source = $Dependency.Source
if (-not $Dependency.Source -or $Source -eq '') {
    $Source = 'https://community.chocolatey.org/api/v2/'
}

$Credential = $Dependency.Credential

if (-not (Get-Command -Name 'choco.exe' -ErrorAction SilentlyContinue)) {
    Write-Verbose "Chocolatey is not installed. Installing from [$ChocoInstallScriptUrl]"
    # download and run the Chocolatey script
    # Add TLS 1.2 support
    [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12

    do {
        $scriptPath = Join-Path -Path $env:TEMP -ChildPath ("{0}.ps1" -f [GUID]::NewGuid().ToString())
    } while (Test-Path -Path $scriptPath)

    try {
        Invoke-WebRequest -UseBasicParsing -Uri $ChocoInstallScriptUrl -OutFile $scriptPath
        & $scriptPath
    }
    catch {
        throw "Unable to install Chocolatey from '$ChocoInstallScriptUrl'."
    }
}

# If this is a forced install we don't need to check anything,
# just install the package version requested
if ($Force.IsPresent -and $PSDependAction -contains 'Install') {
    $params = @{
        Name    = $Name
        Version = $Version
        Source  = $Source
        Force   = $Force.IsPresent
    }

    if ($Credential) {
        $params.Credential = $Credential
    }

    Write-Verbose "Forced install of Chocolatey package [$Name] from Chocolatey source [$Source] with Version [$Version]"
    Invoke-ChocoInstallPackage @params

    return
}

# get the package if it is installed
Write-Verbose "Getting package [$Name] version, if it is installed."
$existingVersion = (Get-ChocoInstalledPackage -Name $Name).Version
if ($existingVersion) {
    Write-Verbose "Found package [$Name] installed with version [$existingVersion]."
}
else {
    Write-Verbose "Package [$Name] not installed."
}

# Specific version requested, and equal to current
if ($Version -ne 'latest' -and (Test-VersionEquality -ReferenceVersion $Version -DifferenceVersion $existingVersion)) {
    Write-Verbose "You have the requested version [$Version] of [$Name]"
    if ($PSDependAction -contains 'Test') {
        return $true
    }

    return
}

# get the latest version from the source
$repoParams = @{
    Name   = $Name
    Source = $Source
}
if ($Credential) {
    $repoParams.Credential = $Credential
}

Write-Verbose "Getting latest package [$Name] version from source [$Source]."
$repositoryVersion = (Get-ChocoLatestPackage @repoParams).Version
if ($repositoryVersion) {
    Write-Verbose "Found package [$Name] version [$repositoryVersion] on source [$Source]."
}
else {
    Write-Verbose "Package [$Name] not found on source [$Source]. Nothing more can be done."
    return  # cannot continue
}

# If the version in the remote repository is less than or equal to the version installed, then we have the latest already
[System.Version]$parsedRepositoryVersion = $null
[System.Version]$parsedExistingVersion = $null
[System.Management.Automation.SemanticVersion]$parsedRepositorySemanticVersion = $null
[System.Management.Automation.SemanticVersion]$parsedExistingSemanticVersion = $null
$haveLatest = if (
    [System.Management.Automation.SemanticVersion]::TryParse([string]$repositoryVersion, [ref]$parsedRepositorySemanticVersion) -and
    [System.Management.Automation.SemanticVersion]::TryParse([string]$existingVersion, [ref]$parsedExistingSemanticVersion)
) {
    $parsedRepositorySemanticVersion -le $parsedExistingSemanticVersion
}
elseif (
    [System.Version]::TryParse([string]$repositoryVersion, [ref]$parsedRepositoryVersion) -and
    [System.Version]::TryParse([string]$existingVersion, [ref]$parsedExistingVersion)
) {
    $parsedRepositoryVersion -le $parsedExistingVersion
}
else {
    $false
}
if ($Version -eq 'latest' -and $haveLatest) {
    Write-Verbose "You have the latest version of [$Name], with installed version [$existingVersion] and Source version [$repositoryVersion]"
    if ($PSDependAction -contains 'Test') {
        return $true
    }

    return
}

# if we get here then we do not have the latest version installed and that is
# what has been requested
Write-Verbose "You do not have the version requested of [$Name]: Requested version [$Version], existing version [$existingVersion], available version [$repositoryVersion]."
if ($PSDependAction -contains 'Install') {
    $params = @{
        Name    = $Name
        Version = $Version
        Source  = $Source
        Force   = $Force.IsPresent
    }

    if ($Credential) {
        $params.Credential = $Credential
    }

    Invoke-ChocoInstallPackage @params
}
elseif ($PSDependAction -contains 'Test' -and $PSDependAction.count -eq 1) {
    return $false
}