PowerShell script to install .NET Core SDK and the shared runtime.

#
# PowerShell CI script to install .NET Core SDK and the shared runtime.
# <https://jonlabelle.com/snippets/view/powershell/powershell-script-to-install-net-core-sdk>
#
# Will check to see if the specified SDK version is already installed.
#
# Hat-tip to Azure/DotNetty build.ps1 CI build script:
# https://github.com/Azure/DotNetty/blob/dev/build.ps1
#
# Dotnet Install Script Reference:
#   - Homepage: https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-install-script
#   - Bash (Linux/macOS): https://dot.net/v1/dotnet-install.sh
#   - PowerShell (Windows): https://dot.net/v1/dotnet-install.ps1
#

Set-StrictMode -Version Latest
$ErrorActionPreference="Stop"

$DotNetChannel = "Current";
$DotNetVersion = "3.1.101";
$DotNetInstallScript = "dotnet-install.ps1";
$DotNetInstallerUri = "https://dotnet.microsoft.com/download/dotnet-core/scripts/v1/$DotNetInstallScript";
$DotNetInstallDir = Join-Path -Path $PSScriptRoot -ChildPath ".dotnet";
$DotNetToolsDir = Join-Path -Path $DotNetInstallDir -ChildPath "tools"; # https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-tool-install
$DotNetInstallScriptPath = Join-Path -Path $DotNetInstallDir -ChildPath $DotNetInstallScript;
$OSVersion = [System.Environment]::OSVersion | Select-Object -ExpandProperty Platform

function Remove-PathVariable([string] $VariableToRemove)
{
    if (($OSVersion -eq "Unix") -or ($OSVersion -eq "Linux"))
    {
        # *NIX
        $path = [System.Environment]::GetEnvironmentVariable("PATH")
        if ($null -ne $path)
        {
            $newItems = $path.Split(':', [StringSplitOptions]::RemoveEmptyEntries) | Where-Object { "$($_)" -INotLike $VariableToRemove }
            [System.Environment]::SetEnvironmentVariable("PATH", [System.String]::Join(':', $newItems))
        }

        $path = [System.Environment]::GetEnvironmentVariable("PATH", [System.EnvironmentVariableTarget]::Process)
        if ($null -ne $path)
        {
            $newItems = $path.Split(':', [StringSplitOptions]::RemoveEmptyEntries) | Where-Object { "$($_)" -INotLike $VariableToRemove }
            [System.Environment]::SetEnvironmentVariable("PATH", [System.String]::Join(':', $newItems), [System.EnvironmentVariableTarget]::Process)
        }

        $path = [System.Environment]::GetEnvironmentVariable("PATH", [System.EnvironmentVariableTarget]::Machine)
        if ($null -ne $path)
        {
            $newItems = $path.Split(':', [StringSplitOptions]::RemoveEmptyEntries) | Where-Object { "$($_)" -INotLike $VariableToRemove }
            [System.Environment]::SetEnvironmentVariable("PATH", [System.String]::Join(':', $newItems), [System.EnvironmentVariableTarget]::Machine)
        }
    }
    else
    {
        # Windows
        $path = [System.Environment]::GetEnvironmentVariable("Path", [System.EnvironmentVariableTarget]::User)
        if ($null -ne $path)
        {
            $newItems = $path.Split(';', [StringSplitOptions]::RemoveEmptyEntries) | Where-Object { "$($_)" -INotLike $VariableToRemove }
            [System.Environment]::SetEnvironmentVariable("Path", [System.String]::Join(';', $newItems), [System.EnvironmentVariableTarget]::User)
        }

        $path = [System.Environment]::GetEnvironmentVariable("Path", [System.EnvironmentVariableTarget]::Process)
        if ($null -ne $path)
        {
            $newItems = $path.Split(';', [StringSplitOptions]::RemoveEmptyEntries) | Where-Object { "$($_)" -INotLike $VariableToRemove }
            [System.Environment]::SetEnvironmentVariable("Path", [System.String]::Join(';', $newItems), [System.EnvironmentVariableTarget]::Process)
        }
    }
}

function Get-HttpResource {
<#
.SYNOPSIS
    Downloads the contents from a URL

.DESCRIPTION
    Get-HttpResource downloads the contents of an HTTP url.
    When -PassThru is specified it returns the string content.

.PARAMETER Url
    The url containing the content to download.

.PARAMETER OutputPath
    If provided, the content will be saved to this path.

.PARAMETER PassThru
    If provided, the string will be output to the pipeline.

.EXAMPLE
    $content = Get-HttpResource -Url 'http://my/url' -OutputPath 'c:\myfile.txt' -PassThru

    This downloads the content located at http://my/url and
    saves it to a file at c:\myfile.txt and also returns
    the downloaded string.

.LINK
    https://boxstarter.org
#>
    param (
        [string]$Url,
        [string]$OutputPath = $null,
        [switch]$PassThru
    )

    Write-Verbose "Downloading $url"

    $str = Invoke-RetriableScript -RetryScript {
        $downloader=New-Object System.Net.WebClient
        $wp=[System.Net.WebProxy]::GetDefaultProxy()
        $wp.UseDefaultCredentials=$true
        $downloader.Proxy=$wp
        $downloader.UseDefaultCredentials=$true

        # Fixes: "Powershell Invoke-WebRequest Fails with SSL/TLS Secure Channel"
        [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

        try
        {
            if ($args[1])
            {
                Write-Verbose "Saving $($args[0]) to $($args[1])"
                $downloader.DownloadFile($args[0], $args[1])
            }
            else
            {
                $downloader.DownloadString($args[0])
            }
        }
        catch
        {
            if ($VerbosePreference -eq "Continue")
            {
                Write-Error $($_.Exception | Format-List * -Force | Out-String)
            }
            throw $_
        }
    } $Url $OutputPath

    if ($PassThru)
    {
        if ($str)
        {
            Write-Output $str
        }
        elseif ($OutputPath)
        {
            Get-Content -Path $OutputPath
        }
    }
}

function Invoke-RetriableScript
{
<#
.SYNOPSIS
    Retries a script 5 times or until it completes without terminating errors.
    All Unnamed arguments will be passed as arguments to the script
#>
    param([ScriptBlock] $RetryScript)

    $currentErrorAction=$ErrorActionPreference

    try {
        $ErrorActionPreference = "Stop"
        for ($count = 1; $count -le 5; $count++)
        {
            try
            {
                Write-Verbose "Attempt #$count..."
                $ret = Invoke-Command -ScriptBlock $RetryScript -ArgumentList $args
                return $ret
                break
            }
            catch
            {
                if ($global:Error.Count -gt 0)
                {
                    $global:Error.RemoveAt(0)
                }
                if ($count -eq 5)
                {
                    throw $_
                }
                else
                {
                    Sleep 10
                }
            }
        }
    }
    finally
    {
        $ErrorActionPreference = $currentErrorAction
    }
}

# Get .NET Core CLI path if installed.
$FoundDotNetCliVersion = $null;
if (Get-Command dotnet -ErrorAction SilentlyContinue)
{
    $FoundDotNetCliVersion = dotnet --version;
}

if ($FoundDotNetCliVersion -ne $DotNetVersion)
{
    if (!(Test-Path $DotNetInstallDir))
    {
        New-Item -Path $DotNetInstallDir -ItemType Directory -Force | Out-Null;
    }

    (Get-HttpResource -Url $DotNetInstallerUri -OutputPath $DotNetInstallScriptPath);
    & $DotNetInstallScriptPath -Channel $DotNetChannel -Version $DotNetVersion -InstallDir $DotNetInstallDir;

    # Run `Get-ChildItem -Path Env:` to list current env variables
    Remove-PathVariable "$DotNetToolsDir"
    Remove-PathVariable "$DotNetInstallDir"
    $env:PATH = "$DotNetInstallDir;$DotNetToolsDir;$env:PATH"
}

# Opt out of dotnet first time setup and telemetry
$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
$env:DOTNET_CLI_TELEMETRY_OPTOUT=1