Private/Factory.ps1

function ConvertTo-ParcelScripts
{
    param(
        [Parameter()]
        [hashtable[]]
        $Scripts
    )

    return [ParcelScripts]::new($Scripts.pre, $Scripts.post)
}

function Initialize-ParcelProviders
{
    param(
        [Parameter()]
        [hashtable]
        $Providers,

        [switch]
        $WhatIf
    )

    # do nothing if there are no providers
    if (($null -eq $Providers) -or ($Providers.Count -eq 0)) {
        return
    }

    # each key here will be a provider
    foreach ($name in $Providers.Keys) {
        # get the provider
        $provider = [ParcelFactory]::Instance().GetProvider($name)

        # setup any custom sources
        $provider.SetCustomSources($Providers[$name].sources, $WhatIf)

        # setup any global custom arguments
        $provider.SetArguments($Providers[$name].args)
    }
}

function ConvertTo-ParcelPackages
{
    param(
        [Parameter()]
        [hashtable[]]
        $Packages,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $Context
    )

    # do nothing if there are no packages
    if (($Packages | Measure-Object).Count -eq 0) {
        Write-Host "No packages supplied" -ForegroundColor Yellow
        return
    }

    # convert each package to a parcel provider
    $Packages | ForEach-Object {
        ConvertTo-ParcelPackage -Package $_ -Context $Context
    }
}

function ConvertTo-ParcelPackage
{
    param(
        [Parameter(Mandatory=$true)]
        [hashtable]
        $Package,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $Context
    )

    if ([string]::IsNullOrEmpty($Package.provider)) {
        throw "Package provider is mandatory for $($Package.name)"
    }

    $_package = [ParcelPackage]::new($Package)

    if ($null -eq $_package.TestPackage($Context)) {
        [ParcelFactory]::Instance().AddProvider($_package.ProviderName)
    }

    return $_package
}

function Write-ParcelPackageHeader
{
    param(
        [Parameter(ParameterSetName='Message')]
        [string]
        $Message = [string]::Empty,

        [Parameter(ParameterSetName='Package')]
        [ParcelPackage]
        $Package,

        [Parameter(ParameterSetName='Package')]
        [ParcelProvider]
        $Provider
    )

    if ($PSCmdlet.ParameterSetName -ieq 'package') {
        $Message = $Provider.GetPackageHeaderMessage($Package)
    }

    Write-ParcelHeader -Message $Message
}

function Write-ParcelHeader
{
    param(
        [Parameter()]
        [string]
        $Message
    )

    $dashes = ('-' * (80 - $Message.Length))
    Write-Host "$($Message)$($dashes)>"
}

function Get-ParcelContext
{
    param(
        [Parameter()]
        [string]
        $Environment
    )

    # initial empty context
    $ctx = @{
        os = @{
            type = $null
            name = $null
            version = $null
        }
        environment = $null
        package = @{
            provider = $null
        }
    }

    # set os
    if ($PSVersionTable.PSEdition -ieq 'desktop') {
        $ctx.os.type = 'windows'
        $ctx.os.name = 'windows'
        $ctx.os.version = "$($PSVersionTable.BuildVersion)"
    }
    elseif ($IsWindows) {
        $ctx.os.type = 'windows'
        $ctx.os.name = 'windows'
        $ctx.os.version = ($PSVersionTable.OS -split '\s+')[-1]
    }
    elseif ($IsLinux) {
        $ctx.os.type = 'linux'
        $ctx.os.name = 'linux'

        if ($PSVersionTable.OS -imatch '(?<name>(ubuntu|centos|debian|fedora))') {
            $ctx.os.name = $Matches['name'].ToLowerInvariant()
        }

        if ($PSVersionTable.OS -imatch '^linux\s+[0-9\.\-]+microsoft') {
            $ctx.os.name = 'ubuntu'
        }
    }
    elseif ($IsMacOS) {
        $ctx.os.type = 'macos'
        $ctx.os.name = 'darwin'

        if ($PSVersionTable.OS -imatch '^(?<name>[a-z]+)\s+(?<version>[0-9\.]+)') {
            $ctx.os.name = $Matches['name'].ToLowerInvariant()
            $ctx.os.version = $Matches['version'].ToLowerInvariant()
        }
    }

    # set environment
    $ctx.environment = $Environment
    if ([string]::IsNullOrWhiteSpace($ctx.environment)) {
        $ctx.environment = 'none'
    }

    # return the context
    $ctx.environment = $ctx.environment.ToLowerInvariant()
    return $ctx
}

function Invoke-ParcelPackages
{
    param(
        [Parameter(Mandatory=$true)]
        [ValidateSet('Install', 'Uninstall')]
        [string]
        $Action,

        [Parameter(Mandatory=$true)]
        [ParcelPackage[]]
        $Packages,

        [Parameter(Mandatory=$true)]
        [ParcelScripts]
        $Scripts,

        [Parameter()]
        [hashtable]
        $Providers,

        [Parameter(Mandatory=$true)]
        [hashtable]
        $Context,

        [switch]
        $IgnoreEnsures,

        [switch]
        $WhatIf
    )

    $start = [datetime]::Now

    # stats of what's installed etc
    $stats = @{
        Install = 0
        Uninstall = 0
        Skipped = 0
    }

    # check if we need to install any providers
    $stats.Install += [ParcelFactory]::Instance().InstallProviders($WhatIf)

    # setup any provider details, like sources
    Initialize-ParcelProviders -Providers $Providers -WhatIf:$WhatIf

    # invoke any global pre install/uninstall
    Invoke-ParcelGlobalScript -Action $Action -Stage Pre -WhatIf:$WhatIf

    # attempt to install/uninstall each package
    foreach ($package in $Packages) {
        # get the provider
        $provider = [ParcelFactory]::Instance().GetProvider($package.ProviderName)

        # update the package's version with latest if required
        if ($package.IsLatest) {
            $provider.SetPackageLatestVersion($package)
        }

        # write out the strap line
        Write-ParcelPackageHeader -Package $package -Provider $provider

        # set parcel context for package
        $Context.package.provider = $package.ProviderName
        $_action = $Action

        # skip any package with a specific ensure type
        if ($IgnoreEnsures -and (@('present', 'absent') -icontains $package.Ensure)) {
            $result = [ParcelStatus]::new('Skipped', 'Ingoring ensures on packages')
        }

        # install or uninstall?
        else {
            # loop and retry to action the package
            foreach ($i in 1..3) {
                try {
                    switch ($Action.ToLowerInvariant()) {
                        'install' {
                            if (@('neutral', 'present') -icontains $package.Ensure) {
                                $result = $provider.Install($package, $Context, $WhatIf)
                            }
                            else {
                                $_action = 'uninstall'
                                $result = $provider.Uninstall($package, $Context, $WhatIf)
                            }
                        }

                        'uninstall' {
                            if (@('neutral', 'absent') -icontains $package.Ensure) {
                                $result = $provider.Uninstall($package, $Context, $WhatIf)
                            }
                            else {
                                $_action = 'install'
                                $result = $provider.Install($package, $Context, $WhatIf)
                            }
                        }
                    }

                    # this was successful, so dont retry
                    break
                }
                catch {
                    if ($i -eq 3) {
                        throw $_.Exception
                    }

                    continue
                }
            }
        }

        # add to stats
        if ($result.Status -ieq 'Skipped') {
            $_action = 'Skipped'
        }

        $stats[$_action]++

        # write out the status
        $result.WriteStatusMessage($WhatIf)
        Write-Host ([string]::Empty)

        # refresh the environment and path
        Update-ParcelEnvironmentVariables -WhatIf:$WhatIf
        Update-ParcelEnvironmentPath -WhatIf:$WhatIf
    }

    # invoke any global post install/uninstall
    Invoke-ParcelGlobalScript -Action $Action -Stage Post -WhatIf:$WhatIf

    # write out the stats
    if ($WhatIf) {
        Write-Host '[WhatIf]: ' -ForegroundColor Cyan -NoNewline
    }

    Write-Host "(installed: $($stats.Install), uninstalled: $($stats.Uninstall), skipped: $($stats.Skipped))"

    # write out the total time
    $end = ([datetime]::Now - $start)
    Write-Host "Duration: $($end.Hours) hour(s), $($end.Minutes) minute(s) and $($end.Seconds) second(s)"
}

function Invoke-ParcelGlobalScript
{
    param(
        [Parameter(Mandatory=$true)]
        [ValidateSet('Install', 'Uninstall')]
        [string]
        $Action,

        [Parameter(Mandatory=$true)]
        [ValidateSet('Pre', 'Post')]
        [string]
        $Stage,

        [switch]
        $WhatIf
    )

    if ($WhatIf) {
        return
    }

    switch ($Action.ToLowerInvariant()) {
        'install' {
            if ($Stage -ieq 'pre') {
                $Scripts.PreInstall($WhatIf)
            }
            else {
                $Scripts.PostInstall($WhatIf)
            }
        }

        'uninstall' {
            if ($Stage -ieq 'pre') {
                $Scripts.PreUninstall($WhatIf)
            }
            else {
                $Scripts.PostUninstall($WhatIf)
            }
        }
    }
}

function Invoke-ParcelPowershell
{
    param(
        [Parameter(Mandatory=$true)]
        [string]
        $Command
    )

    $Command += '; if (!$? -or ($LASTEXITCODE -ne 0)) { throw }'

    if ($PSVersionTable.PSEdition -ieq 'Desktop') {
        return (powershell -c $Command)
    }
    else {
        return (pwsh -c $Command)
    }
}

function Get-ParcelEnvironmentVariable
{
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Name,

        [Parameter(Mandatory=$true)]
        [string]
        $Scope
    )

    return [System.Environment]::GetEnvironmentVariable($Name, $Scope)
}

function Get-ParcelEnvironmentVariables
{
    param (
        [Parameter(Mandatory=$true)]
        [string]
        $Scope
    )

    return ([System.Environment]::GetEnvironmentVariables($Scope)).Keys
}

function Update-ParcelEnvironmentPath
{
    param(
        [switch]
        $WhatIf
    )

    if ($WhatIf) {
        return
    }

    # get items in current path
    $items = @(@($env:PATH -split ';') | Select-Object -Unique)

    # add new items from paths
    @('Machine', 'User') | ForEach-Object {
        @((Get-ParcelEnvironmentVariable -Name 'PATH' -Scope $_) -split ';') | Select-Object -Unique | ForEach-Object {
            if ($items -inotcontains $_) {
                $items += $_
            }
        }
    }

    $env:PATH = ($items -join ';')
}

function Update-ParcelEnvironmentVariables
{
    param(
        [switch]
        $WhatIf
    )

    if ($WhatIf) {
        return
    }

    foreach ($scope in @('Process', 'Machine', 'User')) {
        foreach ($var in (Get-ParcelEnvironmentVariables -Scope $scope)) {
            Set-Item "Env:$($var)" -Value (Get-ParcelEnvironmentVariable -Name $var -Scope $scope) -Force
        }
    }
}

function Test-ParcelAdminUser
{
    # check the current platform, if it's unix then return true
    if ($PSVersionTable.Platform -ieq 'unix') {
        return $true
    }

    try {
        $principal = New-Object System.Security.Principal.WindowsPrincipal([System.Security.Principal.WindowsIdentity]::GetCurrent())
        if ($null -eq $principal) {
            return $false
        }

        return $principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)
    }
    catch [exception] {
        Write-Host 'Error checking user administrator privileges' -ForegroundColor Red
        Write-Host $_.Exception.Message -ForegroundColor Red
        return $false
    }
}