ChoVersion.psm1

#Region '.\Header\Constants.ps1' 0
if ($PSScriptRoot -match 'Header$') {
    Write-Warning "This module isn't really meant to be imported from source. YMMV"
    $PSScriptRoot = $PSScriptRoot | Split-Path
}
$Shim = Get-FileHash "$PSScriptRoot\lib\shim.exe" -ErrorAction SilentlyContinue -ErrorVariable MissingShim
if ($MissingShim) {
    Write-Error "The 'shim.exe' is missing from '$PSScriptRoot\lib' and ChoVersion will not work. Please reinstall ChoVersion."
}
if ($Shim.Hash -ne "AA685053F4A5C0E7145F2A27514C8A56CEAE25B0824062326F04037937CAA558") {
    Write-Error "The 'shim.exe' file has been changed in '$PSScriptRoot\lib' and ChoVersion will not work. Please reinstall ChoVersion."
}
#EndRegion '.\Header\Constants.ps1' 12
#Region '.\Public\Add-ToolPath.ps1' 0
filter Add-ToolPath {
    <#
        .SYNOPSIS
            Prepend a path to the PATH environment variable, with support for Github Actions and Azure Pipelines
    #>

    [CmdletBinding()]
    param(
        # The path to prepend to the PATH environment variable
        [Parameter(Mandatory, ValueFromPipelineByPropertyName, Position = 0)]
        [ValidateScript({ Test-Path $_ })]
        [string]$Path,

        # If set, and on Windows, update the user's PATH
        [switch]$SetForUserExperimental
    )

    # If they pass the path to a file, we'll look at the folder
    if (Test-Path $Path -PathType Leaf) {
        $Path = Split-Path $Path
    }

    $ChocoLib = "$Env:ChocolateyInstall\lib\"
    if ($Path.ToLowerInvariant().StartsWith($ChocoLib.ToLowerInvariant())) {
        $RemovablePaths = $Path -replace "[\d\.]+$"
    }

    ## ShimGen is a whole different option
    # if ($ExecutablePath.ToLowerInvariant().StartsWith()) {
    # try {
    # if ((Get-FileHash $ExecutablePath) -eq $Shim.Hash) {
    # Set-Content ([IO.Path]::ChangeExtension($ExecutablePath,".shim")) "path = $($ChoPackage.Path)"
    # }
    # Copy-Item $Shim $ExecutablePath -Force
    # } catch [UnauthorizedAccessException] {
    # Write-Warning "Elevating to overwrite the chocolatey shim at: $ExecutablePath"
    # $pwsh = Get-Command PowerShell, pwsh -ErrorAction SilentlyContinue | Select-Object -First 1
    # Start-Process $pwsh.Name -Verb RunAs -ArgumentList "-Command", "Copy-Item", $Shim, $ExecutablePath
    # }
    # } else {
    # Write-Warning "The default '$Name' command was not a chocolatey copy: $ExecutablePath"
    # }

    # On Windows, we can prepend the path in the user environment to make it sticky
    if ($SetForUser -and (-not (Test-Path Variable:IsWindows) -or $IsWindows)) {

        [string[]]$EnvPath = [System.Environment]::GetEnvironmentVariable("PATH", "User").Split([IO.Path]::PathSeparator).Where{
            $_ -ine $Path
        }

        # we might want to remove any previous copies of this from the path
        if ($Path.ToLowerInvariant().StartsWith($ChocoLib.ToLowerInvariant())) {
            $ChocoLib = $Path.Substring(0, $ChocoLib.Length)
            $Name, $Folder = $Path.Substring($ChocoLib.Length) -split "(?=[\\/])", 2
            $Pattern = $Name -replace "\.[\d\.]+$", "\.[\d\.]+"
            $RemovablePaths = [regex]::Escape($ChocoLib) + $Pattern + [regex]::Escape($Folder)
            $EnvPath = $EnvPath -notmatch $RemovablePaths
        }

        [string]$EnvPath = @(@($Path) + $EnvPath) -join [IO.Path]::PathSeparator
        Write-Verbose "Set User PATH: $ENVPATH"
        [System.Environment]::SetEnvironmentVariable("PATH", $EnvPath, "User")
    }

    ## Prepend the path in the current session
    Write-Verbose "Prepending '$Path' to PATH"
    $ENV:PATH = $Path + [IO.Path]::PathSeparator + $Env:PATH

    if (Test-Path ENV:SYSTEM_TEAMFOUNDATIONCOLLECTIONURI) {
        ## Prepend the path in Azure DevOps:
        Write-Host "##vso[task.prependpath]$Path"
    }
    if (Test-Path Env:GITHUB_PATH) {
        ## Prepend the path in Github Actions:
        Add-Content $Env:GITHUB_PATH $Path -Encoding UTF8
    }
}
#EndRegion '.\Public\Add-ToolPath.ps1' 77
#Region '.\Public\Get-ChoVersion.ps1' 0
filter Get-ChoVersion {
    [CmdletBinding()]
    param(
        # The name of the chocolatey package
        [Parameter(ValueFromPipelineByPropertyName, Mandatory, Position = 0)]
        [string]$Package,

        # The version of the chocolatey package
        [Parameter(ValueFromPipelineByPropertyName)]
        [string]$Version,

        # The base name of the executable (without the .exe), like "terraform"
        [Parameter(ValueFromPipelineByPropertyName)]
        [string]$Executable = "*",

        # If set, lists all versions available.
        # Otherwise, Get-ChoVersion only returns the specified or newest version
        [switch]$ListAvailable
    )
    if (!$Executable) {
        $Executable = "*"
    }
    $Pattern = $Package -replace "[-\.]", "|"
    $ChildParam = @{
        Filter = "$Executable.exe"
        Recurse = $true
        OutVariable = "App"
    }

    $ChocoApplications = if ($Version) {
        choco list --by-id-only $Package --version $Version --localonly -allversions --limitoutput --includeprograms |
            ConvertFrom-Csv -Delimiter "|" -Header Package, Version
    } else {
        choco list --by-id-only $Package --localonly -allversions --limitoutput --includeprograms |
            ConvertFrom-Csv -Delimiter "|" -Header Package, Version
    }

    if (!$ChocoApplications) {
        Write-Error "Choco package '$Package'$(if($Version){ " version $Version" }) not found!"
        return
    }

    if ($ChocoApplications.Count -gt 1) {
        # On Windows PowerShell, there's no built-in [semver] type accelerator
        if (-not ('semver' -as [type])) {
            Import-Module PackageManagement -Scope Global
            $xlr8r = [psobject].assembly.gettype("System.Management.Automation.TypeAccelerators")
            $xlr8r::Add('semver', [Microsoft.PackageManagement.Provider.Utility.SemanticVersion])
        }
        $ChocoApplications = $ChocoApplications | Sort-Object { [semver]$_.Version } -Descending

        if (!$ListAvailable) {
            $ChocoApplications = $ChocoApplications[0]
        }
    }

    @($ChocoApplications).ForEach{
        Add-Member -InputObject $_ -Passthru -NotePropertyName Path -NotePropertyValue $(
            if (Test-Path "$Env:ChocolateyInstall\lib\$($_.Package).$($_.Version)") {
                $ChildParam["Path"] = "$Env:ChocolateyInstall\lib\$($_.Package).$($_.Version)"
            } elseif (Test-Path $Env:ChocolateyInstall\lib\$($_.Package)) {
                $ChildParam["Path"] = "$Env:ChocolateyInstall\lib\$($_.Package)"
            }

            Get-ChildItem @ChildParam | Convert-Path | Where-Object {
                # we can try matching based on the package name
                $Executable -ne "*" -or $_ -match $Pattern
            } | Select-Object -Unique -First 1
        )
    }
}
#EndRegion '.\Public\Get-ChoVersion.ps1' 72
#Region '.\Public\Set-ChoVersion.ps1' 0
filter Set-ChoVersion {
    <#
        .SYNOPSIS
            Set which version of a chocolatey tool package should be used.
        .EXAMPLE
            Set-ChoVersion terraform 0.13.2
 
            Switches to terraform version 0.13.2
        .Example
            Set-ChoVersion @{ Package = "terraform"; Version = "1.0.1" },
                           @{ Package = "bicep"; Version = "0.4.6" }
 
            Switches to terraform 1.0.1 and bicep 0.4.6
        .Example
            Set-Content ChoVersion.psd1 @"
            @{ ChocolateyPackages = @(
                @{ Package = "terraform"; Version = "1.0.1" }
                @{ Package = "bicep"; Version = "0.4.6" }
                )
            }
            "@
            Set-ChoVersion
 
            Shows how a psd1 file can be used in a project repository to support installing tool dependencies and using specific versions of tools
    #>

    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = "Chocolatey", ConfirmImpact = "Medium")]
    param(
        # The name of the chocolatey package
        [Parameter(ParameterSetName = "One", Mandatory, ValueFromPipelineByPropertyName, Position = 0)]
        [string]$Package,

        # The version of the chocolatey package
        [Parameter(ParameterSetName = "One", ValueFromPipelineByPropertyName, Position = 1)]
        [string]$Version,

        # The base name of the executable (without the .exe), like "terraform"
        [Parameter(ParameterSetName = "One", ValueFromPipelineByPropertyName)]
        [string]$Executable = ($Package -replace "\.portable$|\.install$"),

        # An array of hashtables specifying multiple applications to set or install
        # This parameter is intended to support configuration via a ChoVersion.psd1 file like:
        # @{ChocolateyPackages = @(@{ Package = "terraform"; Version = "1.0.0" })}
        [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = "Chocolatey", Position = 0)]
        [hashtable[]]$ChocolateyPackages,

        # If set, makes the change permanent for the current user by modifying their PATH at user scope
        [switch]$SetForUserExperimental
    )
    Write-Verbose $PSCmdlet.ParameterSetName
    if ($PSCmdlet.ParameterSetName -eq 'Chocolatey') {
        if (-not $ChocolateyPackages) {
            # Only import default parameters if we actually need them
            Import-ParameterConfiguration
        }
        if ($ChocolateyPackages) {
            Write-Verbose "Installing multiple ChocolateyPackages: "
            $Packages = $ChocolateyPackages.ForEach{ [PSCustomObject]$_ }
            Write-Verbose $($Packages | Format-Table -Auto | Out-String)
            $Packages | Set-ChoVersion -SetForUserExperimental:$SetForUserExperimental
            return
        } else {
            Write-Warning "No ChocolateyPackages specified"
            return
        }
    }

    $null = $PSBoundParameters.Remove("SetForUserExperimental")
    $null = $PSBoundParameters.Remove("Confirm")
    Write-Verbose "Setting choco package '$Package'$(if($Version){ " version $Version" })$(if($Executable){ " for executable $Executable" })"

    if (!(($ChoPackage = Get-ChoVersion @PSBoundParameters -ErrorAction SilentlyContinue))) {
        if ($Version) {
            choco install -y $Package --version $Version --sxs
        } else {
            choco install -y $Package --sxs
        }
        $ChoPackage = Get-ChoVersion @PSBoundParameters -ErrorAction Stop
    }

    $ExecutablePath = Get-Command $Executable -ErrorAction SilentlyContinue | Convert-Path
    if ($ExecutablePath -eq $ChoPackage.Path) {
        # nothing to do, it's already the default
        return
    }

    if ($PSCmdlet.ShouldProcess("Use $($ChoPackage.Package) v$($ChoPackage.Version)", "Prepend PATH for '$($ChoPackage.Path)'")) {
        $ChoPackage | Add-ToolPath -SetForUserExperimental:$SetForUserExperimental
    }
}
#EndRegion '.\Public\Set-ChoVersion.ps1' 90