diff --git a/__tests__/installer.test.ts b/__tests__/installer.test.ts index 3bcd0ae..2988f8f 100644 --- a/__tests__/installer.test.ts +++ b/__tests__/installer.test.ts @@ -2,6 +2,7 @@ import io = require('@actions/io'); import fs = require('fs'); import os = require('os'); import path = require('path'); +import httpClient = require('typed-rest-client/HttpClient'); const toolDir = path.join(__dirname, 'runner', 'tools'); const tempDir = path.join(__dirname, 'runner', 'temp'); @@ -10,12 +11,117 @@ process.env['RUNNER_TOOLSDIRECTORY'] = toolDir; process.env['RUNNER_TEMPDIRECTORY'] = tempDir; import * as installer from '../src/installer'; +const IS_WINDOWS = process.platform === 'win32'; + describe('installer tests', () => { - beforeAll(() => {}); beforeAll(async () => { await io.rmRF(toolDir); await io.rmRF(tempDir); }); - it('TODO - Add tests', async () => {}); + afterAll(async () => { + try { + await io.rmRF(toolDir); + await io.rmRF(tempDir); + } catch { + console.log('Failed to remove test directories'); + } + }, 100000); + + it('Acquires version of dotnet if no matching version is installed', async () => { + await getDotnet('2.2.104'); + const dotnetDir = path.join(toolDir, 'dncs', '2.2.104', os.arch()); + + expect(fs.existsSync(`${dotnetDir}.complete`)).toBe(true); + if (IS_WINDOWS) { + expect(fs.existsSync(path.join(dotnetDir, 'dotnet.exe'))).toBe(true); + } else { + expect(fs.existsSync(path.join(dotnetDir, 'dotnet'))).toBe(true); + } + }, 100000); + + it('Throws if no location contains correct dotnet version', async () => { + let thrown = false; + try { + await getDotnet('1000.0.0'); + } catch { + thrown = true; + } + expect(thrown).toBe(true); + }); + + it('Uses version of dotnet installed in cache', async () => { + const dotnetDir: string = path.join(toolDir, 'dncs', '250.0.0', os.arch()); + await io.mkdirP(dotnetDir); + fs.writeFileSync(`${dotnetDir}.complete`, 'hello'); + // This will throw if it doesn't find it in the cache (because no such version exists) + await getDotnet('250.0.0'); + return; + }); + + it('Doesnt use version of dotnet that was only partially installed in cache', async () => { + const dotnetDir: string = path.join(toolDir, 'dncs', '251.0.0', os.arch()); + await io.mkdirP(dotnetDir); + let thrown = false; + try { + // This will throw if it doesn't find it in the cache (because no such version exists) + await getDotnet('251.0.0'); + } catch { + thrown = true; + } + expect(thrown).toBe(true); + return; + }); + + it('Uses an up to date bash download script', async () => { + var httpCallbackClient = new httpClient.HttpClient( + 'setup-dotnet-test', + [], + {} + ); + const response: httpClient.HttpClientResponse = await httpCallbackClient.get( + 'https://dot.net/v1/dotnet-install.sh' + ); + const upToDateContents: string = await response.readBody(); + const currentContents: string = fs + .readFileSync( + path.join(__dirname, '..', 'externals', 'install-dotnet.sh') + ) + .toString(); + expect(normalizeFileContents(currentContents)).toBe( + normalizeFileContents(upToDateContents) + ); + }); + + it('Uses an up to date powershell download script', async () => { + var httpCallbackClient = new httpClient.HttpClient( + 'setup-dotnet-test', + [], + {} + ); + const response: httpClient.HttpClientResponse = await httpCallbackClient.get( + 'https://dot.net/v1/dotnet-install.ps1' + ); + const upToDateContents: string = await response.readBody(); + const currentContents: string = fs + .readFileSync( + path.join(__dirname, '..', 'externals', 'install-dotnet.ps1') + ) + .toString(); + expect(normalizeFileContents(currentContents)).toBe( + normalizeFileContents(upToDateContents) + ); + }); }); + +function normalizeFileContents(contents: string): string { + return contents + .trim() + .replace(new RegExp('\r\n', 'g'), '\n') + .replace(new RegExp('\r', 'g'), '\n'); +} + +async function getDotnet(version: string): Promise { + const dotnetInstaller = new installer.DotnetCoreInstaller(version); + await dotnetInstaller.installDotnet(); +} diff --git a/externals/get-os-distro.sh b/externals/get-os-distro.sh new file mode 100644 index 0000000..4bc5c45 --- /dev/null +++ b/externals/get-os-distro.sh @@ -0,0 +1,192 @@ +#!/usr/bin/env bash +# Copyright (c) .NET Foundation and contributors. All rights reserved. +# Licensed under the MIT license. See LICENSE file in the project root for full license information. +# + +# Stop script on NZEC +set -e +# Stop script if unbound variable found (use ${var:-} if intentional) +set -u +# By default cmd1 | cmd2 returns exit code of cmd2 regardless of cmd1 success +# This is causing it to fail +set -o pipefail + +# Use in the the functions: eval $invocation +invocation='say_verbose "Calling: ${yellow:-}${FUNCNAME[0]} ${green:-}$*${normal:-}"' + +# standard output may be used as a return value in the functions +# we need a way to write text on the screen in the functions so that +# it won't interfere with the return value. +# Exposing stream 3 as a pipe to standard output of the script itself +exec 3>&1 + +say_err() { + printf "%b\n" "get-os-distro: Error: $1" >&2 +} + +# This platform list is finite - if the SDK/Runtime has supported Linux distribution-specific assets, +# then and only then should the Linux distribution appear in this list. +# Adding a Linux distribution to this list does not imply distribution-specific support. +get_legacy_os_name_from_platform() { + + platform="$1" + case "$platform" in + "centos.7") + echo "centos" + return 0 + ;; + "debian.8") + echo "debian" + return 0 + ;; + "fedora.23") + echo "fedora.23" + return 0 + ;; + "fedora.27") + echo "fedora.27" + return 0 + ;; + "fedora.24") + echo "fedora.24" + return 0 + ;; + "opensuse.13.2") + echo "opensuse.13.2" + return 0 + ;; + "opensuse.42.1") + echo "opensuse.42.1" + return 0 + ;; + "opensuse.42.3") + echo "opensuse.42.3" + return 0 + ;; + "rhel.7"*) + echo "rhel" + return 0 + ;; + "ubuntu.14.04") + echo "ubuntu" + return 0 + ;; + "ubuntu.16.04") + echo "ubuntu.16.04" + return 0 + ;; + "ubuntu.16.10") + echo "ubuntu.16.10" + return 0 + ;; + "ubuntu.18.04") + echo "ubuntu.18.04" + return 0 + ;; + "alpine.3.4.3") + echo "alpine" + return 0 + ;; + esac + return 1 +} + +get_linux_platform_name() { + + if [ -e /etc/os-release ]; then + . /etc/os-release + echo "$ID.$VERSION_ID" + return 0 + elif [ -e /etc/redhat-release ]; then + local redhatRelease=$( /dev/null; then + CPUName=$(uname -m) + case $CPUName in + armv7l) + echo "arm" + return 0 + ;; + aarch64) + echo "arm64" + return 0 + ;; + esac + fi + + # Always default to 'x64' + echo "x64" + return 0 +} + +osName=$(get_current_os_name || echo "") +legacyOsName=$(get_legacy_os_name || echo "") +arch=$(get_machine_architecture || echo "") + +primaryName="$osName-$arch" +legacyName="$legacyOsName" + +echo "Primary:$primaryName" +echo "Legacy:$legacyName" + +if [ -z "$osName" ] && [ -z "$legacyOsName" ];then + exit 1 +fi \ No newline at end of file diff --git a/externals/get-os-platform.ps1 b/externals/get-os-platform.ps1 new file mode 100644 index 0000000..4ac8d07 --- /dev/null +++ b/externals/get-os-platform.ps1 @@ -0,0 +1,18 @@ +function Get-Machine-Architecture() +{ + # possible values: AMD64, IA64, x86 + return $ENV:PROCESSOR_ARCHITECTURE +} + +function Get-CLIArchitecture-From-Architecture([string]$Architecture) +{ + switch ($Architecture.ToLower()) + { + { ($_ -eq "amd64") -or ($_ -eq "x64") } { return "x64" } + { $_ -eq "x86" } { return "x86" } + default { throw "Architecture not supported. If you think this is a bug, please report it at https://github.com/dotnet/cli/issues" } + } +} + +$CLIArchitecture = Get-CLIArchitecture-From-Architecture $(Get-Machine-Architecture) +Write-Output "Primary:win-$CLIArchitecture" \ No newline at end of file diff --git a/externals/install-dotnet.ps1 b/externals/install-dotnet.ps1 new file mode 100644 index 0000000..f17bd85 --- /dev/null +++ b/externals/install-dotnet.ps1 @@ -0,0 +1,638 @@ +# +# Copyright (c) .NET Foundation and contributors. All rights reserved. +# Licensed under the MIT license. See LICENSE file in the project root for full license information. +# + +<# +.SYNOPSIS + Installs dotnet cli +.DESCRIPTION + Installs dotnet cli. If dotnet installation already exists in the given directory + it will update it only if the requested version differs from the one already installed. +.PARAMETER Channel + Default: LTS + Download from the Channel specified. Possible values: + - Current - most current release + - LTS - most current supported release + - 2-part version in a format A.B - represents a specific release + examples: 2.0, 1.0 + - Branch name + examples: release/2.0.0, Master + Note: The version parameter overrides the channel parameter. +.PARAMETER Version + Default: latest + Represents a build version on specific channel. Possible values: + - latest - most latest build on specific channel + - coherent - most latest coherent build on specific channel + coherent applies only to SDK downloads + - 3-part version in a format A.B.C - represents specific version of build + examples: 2.0.0-preview2-006120, 1.1.0 +.PARAMETER InstallDir + Default: %LocalAppData%\Microsoft\dotnet + Path to where to install dotnet. Note that binaries will be placed directly in a given directory. +.PARAMETER Architecture + Default: - this value represents currently running OS architecture + Architecture of dotnet binaries to be installed. + Possible values are: , amd64, x64, x86, arm64, arm +.PARAMETER SharedRuntime + This parameter is obsolete and may be removed in a future version of this script. + The recommended alternative is '-Runtime dotnet'. + + Default: false + Installs just the shared runtime bits, not the entire SDK. + This is equivalent to specifying `-Runtime dotnet`. +.PARAMETER Runtime + Installs just a shared runtime, not the entire SDK. + Possible values: + - dotnet - the Microsoft.NETCore.App shared runtime + - aspnetcore - the Microsoft.AspNetCore.App shared runtime +.PARAMETER DryRun + If set it will not perform installation but instead display what command line to use to consistently install + currently requested version of dotnet cli. In example if you specify version 'latest' it will display a link + with specific version so that this command can be used deterministicly in a build script. + It also displays binaries location if you prefer to install or download it yourself. +.PARAMETER NoPath + By default this script will set environment variable PATH for the current process to the binaries folder inside installation folder. + If set it will display binaries location but not set any environment variable. +.PARAMETER Verbose + Displays diagnostics information. +.PARAMETER AzureFeed + Default: https://dotnetcli.azureedge.net/dotnet + This parameter typically is not changed by the user. + It allows changing the URL for the Azure feed used by this installer. +.PARAMETER UncachedFeed + This parameter typically is not changed by the user. + It allows changing the URL for the Uncached feed used by this installer. +.PARAMETER FeedCredential + Used as a query string to append to the Azure feed. + It allows changing the URL to use non-public blob storage accounts. +.PARAMETER ProxyAddress + If set, the installer will use the proxy when making web requests +.PARAMETER ProxyUseDefaultCredentials + Default: false + Use default credentials, when using proxy address. +.PARAMETER SkipNonVersionedFiles + Default: false + Skips installing non-versioned files if they already exist, such as dotnet.exe. +.PARAMETER NoCdn + Disable downloading from the Azure CDN, and use the uncached feed directly. +#> +[cmdletbinding()] +param( + [string]$Channel="LTS", + [string]$Version="Latest", + [string]$InstallDir="", + [string]$Architecture="", + [ValidateSet("dotnet", "aspnetcore", IgnoreCase = $false)] + [string]$Runtime, + [Obsolete("This parameter may be removed in a future version of this script. The recommended alternative is '-Runtime dotnet'.")] + [switch]$SharedRuntime, + [switch]$DryRun, + [switch]$NoPath, + [string]$AzureFeed="https://dotnetcli.azureedge.net/dotnet", + [string]$UncachedFeed="https://dotnetcli.blob.core.windows.net/dotnet", + [string]$FeedCredential, + [string]$ProxyAddress, + [switch]$ProxyUseDefaultCredentials, + [switch]$SkipNonVersionedFiles, + [switch]$NoCdn +) + +Set-StrictMode -Version Latest +$ErrorActionPreference="Stop" +$ProgressPreference="SilentlyContinue" + +if ($NoCdn) { + $AzureFeed = $UncachedFeed +} + +$BinFolderRelativePath="" + +if ($SharedRuntime -and (-not $Runtime)) { + $Runtime = "dotnet" +} + +# example path with regex: shared/1.0.0-beta-12345/somepath +$VersionRegEx="/\d+\.\d+[^/]+/" +$OverrideNonVersionedFiles = !$SkipNonVersionedFiles + +function Say($str) { + Write-Host "dotnet-install: $str" +} + +function Say-Verbose($str) { + Write-Verbose "dotnet-install: $str" +} + +function Say-Invocation($Invocation) { + $command = $Invocation.MyCommand; + $args = (($Invocation.BoundParameters.Keys | foreach { "-$_ `"$($Invocation.BoundParameters[$_])`"" }) -join " ") + Say-Verbose "$command $args" +} + +function Invoke-With-Retry([ScriptBlock]$ScriptBlock, [int]$MaxAttempts = 3, [int]$SecondsBetweenAttempts = 1) { + $Attempts = 0 + + while ($true) { + try { + return $ScriptBlock.Invoke() + } + catch { + $Attempts++ + if ($Attempts -lt $MaxAttempts) { + Start-Sleep $SecondsBetweenAttempts + } + else { + throw + } + } + } +} + +function Get-Machine-Architecture() { + Say-Invocation $MyInvocation + + # possible values: amd64, x64, x86, arm64, arm + return $ENV:PROCESSOR_ARCHITECTURE +} + +function Get-CLIArchitecture-From-Architecture([string]$Architecture) { + Say-Invocation $MyInvocation + + switch ($Architecture.ToLower()) { + { $_ -eq "" } { return Get-CLIArchitecture-From-Architecture $(Get-Machine-Architecture) } + { ($_ -eq "amd64") -or ($_ -eq "x64") } { return "x64" } + { $_ -eq "x86" } { return "x86" } + { $_ -eq "arm" } { return "arm" } + { $_ -eq "arm64" } { return "arm64" } + default { throw "Architecture not supported. If you think this is a bug, report it at https://github.com/dotnet/cli/issues" } + } +} + +# The version text returned from the feeds is a 1-line or 2-line string: +# For the SDK and the dotnet runtime (2 lines): +# Line 1: # commit_hash +# Line 2: # 4-part version +# For the aspnetcore runtime (1 line): +# Line 1: # 4-part version +function Get-Version-Info-From-Version-Text([string]$VersionText) { + Say-Invocation $MyInvocation + + $Data = -split $VersionText + + $VersionInfo = @{ + CommitHash = $(if ($Data.Count -gt 1) { $Data[0] }) + Version = $Data[-1] # last line is always the version number. + } + return $VersionInfo +} + +function Load-Assembly([string] $Assembly) { + try { + Add-Type -Assembly $Assembly | Out-Null + } + catch { + # On Nano Server, Powershell Core Edition is used. Add-Type is unable to resolve base class assemblies because they are not GAC'd. + # Loading the base class assemblies is not unnecessary as the types will automatically get resolved. + } +} + +function GetHTTPResponse([Uri] $Uri) +{ + Invoke-With-Retry( + { + + $HttpClient = $null + + try { + # HttpClient is used vs Invoke-WebRequest in order to support Nano Server which doesn't support the Invoke-WebRequest cmdlet. + Load-Assembly -Assembly System.Net.Http + + if(-not $ProxyAddress) { + try { + # Despite no proxy being explicitly specified, we may still be behind a default proxy + $DefaultProxy = [System.Net.WebRequest]::DefaultWebProxy; + if($DefaultProxy -and (-not $DefaultProxy.IsBypassed($Uri))) { + $ProxyAddress = $DefaultProxy.GetProxy($Uri).OriginalString + $ProxyUseDefaultCredentials = $true + } + } catch { + # Eat the exception and move forward as the above code is an attempt + # at resolving the DefaultProxy that may not have been a problem. + $ProxyAddress = $null + Say-Verbose("Exception ignored: $_.Exception.Message - moving forward...") + } + } + + if($ProxyAddress) { + $HttpClientHandler = New-Object System.Net.Http.HttpClientHandler + $HttpClientHandler.Proxy = New-Object System.Net.WebProxy -Property @{Address=$ProxyAddress;UseDefaultCredentials=$ProxyUseDefaultCredentials} + $HttpClient = New-Object System.Net.Http.HttpClient -ArgumentList $HttpClientHandler + } + else { + + $HttpClient = New-Object System.Net.Http.HttpClient + } + # Default timeout for HttpClient is 100s. For a 50 MB download this assumes 500 KB/s average, any less will time out + # 20 minutes allows it to work over much slower connections. + $HttpClient.Timeout = New-TimeSpan -Minutes 20 + $Response = $HttpClient.GetAsync("${Uri}${FeedCredential}").Result + if (($Response -eq $null) -or (-not ($Response.IsSuccessStatusCode))) { + # The feed credential is potentially sensitive info. Do not log FeedCredential to console output. + $ErrorMsg = "Failed to download $Uri." + if ($Response -ne $null) { + $ErrorMsg += " $Response" + } + + throw $ErrorMsg + } + + return $Response + } + finally { + if ($HttpClient -ne $null) { + $HttpClient.Dispose() + } + } + }) +} + + +function Get-Latest-Version-Info([string]$AzureFeed, [string]$Channel, [bool]$Coherent) { + Say-Invocation $MyInvocation + + $VersionFileUrl = $null + if ($Runtime -eq "dotnet") { + $VersionFileUrl = "$UncachedFeed/Runtime/$Channel/latest.version" + } + elseif ($Runtime -eq "aspnetcore") { + $VersionFileUrl = "$UncachedFeed/aspnetcore/Runtime/$Channel/latest.version" + } + elseif (-not $Runtime) { + if ($Coherent) { + $VersionFileUrl = "$UncachedFeed/Sdk/$Channel/latest.coherent.version" + } + else { + $VersionFileUrl = "$UncachedFeed/Sdk/$Channel/latest.version" + } + } + else { + throw "Invalid value for `$Runtime" + } + try { + $Response = GetHTTPResponse -Uri $VersionFileUrl + } + catch { + throw "Could not resolve version information." + } + $StringContent = $Response.Content.ReadAsStringAsync().Result + + switch ($Response.Content.Headers.ContentType) { + { ($_ -eq "application/octet-stream") } { $VersionText = $StringContent } + { ($_ -eq "text/plain") } { $VersionText = $StringContent } + { ($_ -eq "text/plain; charset=UTF-8") } { $VersionText = $StringContent } + default { throw "``$Response.Content.Headers.ContentType`` is an unknown .version file content type." } + } + + $VersionInfo = Get-Version-Info-From-Version-Text $VersionText + + return $VersionInfo +} + + +function Get-Specific-Version-From-Version([string]$AzureFeed, [string]$Channel, [string]$Version) { + Say-Invocation $MyInvocation + + switch ($Version.ToLower()) { + { $_ -eq "latest" } { + $LatestVersionInfo = Get-Latest-Version-Info -AzureFeed $AzureFeed -Channel $Channel -Coherent $False + return $LatestVersionInfo.Version + } + { $_ -eq "coherent" } { + $LatestVersionInfo = Get-Latest-Version-Info -AzureFeed $AzureFeed -Channel $Channel -Coherent $True + return $LatestVersionInfo.Version + } + default { return $Version } + } +} + +function Get-Download-Link([string]$AzureFeed, [string]$SpecificVersion, [string]$CLIArchitecture) { + Say-Invocation $MyInvocation + + if ($Runtime -eq "dotnet") { + $PayloadURL = "$AzureFeed/Runtime/$SpecificVersion/dotnet-runtime-$SpecificVersion-win-$CLIArchitecture.zip" + } + elseif ($Runtime -eq "aspnetcore") { + $PayloadURL = "$AzureFeed/aspnetcore/Runtime/$SpecificVersion/aspnetcore-runtime-$SpecificVersion-win-$CLIArchitecture.zip" + } + elseif (-not $Runtime) { + $PayloadURL = "$AzureFeed/Sdk/$SpecificVersion/dotnet-sdk-$SpecificVersion-win-$CLIArchitecture.zip" + } + else { + throw "Invalid value for `$Runtime" + } + + Say-Verbose "Constructed primary named payload URL: $PayloadURL" + + return $PayloadURL +} + +function Get-LegacyDownload-Link([string]$AzureFeed, [string]$SpecificVersion, [string]$CLIArchitecture) { + Say-Invocation $MyInvocation + + if (-not $Runtime) { + $PayloadURL = "$AzureFeed/Sdk/$SpecificVersion/dotnet-dev-win-$CLIArchitecture.$SpecificVersion.zip" + } + elseif ($Runtime -eq "dotnet") { + $PayloadURL = "$AzureFeed/Runtime/$SpecificVersion/dotnet-win-$CLIArchitecture.$SpecificVersion.zip" + } + else { + return $null + } + + Say-Verbose "Constructed legacy named payload URL: $PayloadURL" + + return $PayloadURL +} + +function Get-User-Share-Path() { + Say-Invocation $MyInvocation + + $InstallRoot = $env:DOTNET_INSTALL_DIR + if (!$InstallRoot) { + $InstallRoot = "$env:LocalAppData\Microsoft\dotnet" + } + return $InstallRoot +} + +function Resolve-Installation-Path([string]$InstallDir) { + Say-Invocation $MyInvocation + + if ($InstallDir -eq "") { + return Get-User-Share-Path + } + return $InstallDir +} + +function Get-Version-Info-From-Version-File([string]$InstallRoot, [string]$RelativePathToVersionFile) { + Say-Invocation $MyInvocation + + $VersionFile = Join-Path -Path $InstallRoot -ChildPath $RelativePathToVersionFile + Say-Verbose "Local version file: $VersionFile" + + if (Test-Path $VersionFile) { + $VersionText = cat $VersionFile + Say-Verbose "Local version file text: $VersionText" + return Get-Version-Info-From-Version-Text $VersionText + } + + Say-Verbose "Local version file not found." + + return $null +} + +function Is-Dotnet-Package-Installed([string]$InstallRoot, [string]$RelativePathToPackage, [string]$SpecificVersion) { + Say-Invocation $MyInvocation + + $DotnetPackagePath = Join-Path -Path $InstallRoot -ChildPath $RelativePathToPackage | Join-Path -ChildPath $SpecificVersion + Say-Verbose "Is-Dotnet-Package-Installed: Path to a package: $DotnetPackagePath" + return Test-Path $DotnetPackagePath -PathType Container +} + +function Get-Absolute-Path([string]$RelativeOrAbsolutePath) { + # Too much spam + # Say-Invocation $MyInvocation + + return $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($RelativeOrAbsolutePath) +} + +function Get-Path-Prefix-With-Version($path) { + $match = [regex]::match($path, $VersionRegEx) + if ($match.Success) { + return $entry.FullName.Substring(0, $match.Index + $match.Length) + } + + return $null +} + +function Get-List-Of-Directories-And-Versions-To-Unpack-From-Dotnet-Package([System.IO.Compression.ZipArchive]$Zip, [string]$OutPath) { + Say-Invocation $MyInvocation + + $ret = @() + foreach ($entry in $Zip.Entries) { + $dir = Get-Path-Prefix-With-Version $entry.FullName + if ($dir -ne $null) { + $path = Get-Absolute-Path $(Join-Path -Path $OutPath -ChildPath $dir) + if (-Not (Test-Path $path -PathType Container)) { + $ret += $dir + } + } + } + + $ret = $ret | Sort-Object | Get-Unique + + $values = ($ret | foreach { "$_" }) -join ";" + Say-Verbose "Directories to unpack: $values" + + return $ret +} + +# Example zip content and extraction algorithm: +# Rule: files if extracted are always being extracted to the same relative path locally +# .\ +# a.exe # file does not exist locally, extract +# b.dll # file exists locally, override only if $OverrideFiles set +# aaa\ # same rules as for files +# ... +# abc\1.0.0\ # directory contains version and exists locally +# ... # do not extract content under versioned part +# abc\asd\ # same rules as for files +# ... +# def\ghi\1.0.1\ # directory contains version and does not exist locally +# ... # extract content +function Extract-Dotnet-Package([string]$ZipPath, [string]$OutPath) { + Say-Invocation $MyInvocation + + Load-Assembly -Assembly System.IO.Compression.FileSystem + Set-Variable -Name Zip + try { + $Zip = [System.IO.Compression.ZipFile]::OpenRead($ZipPath) + + $DirectoriesToUnpack = Get-List-Of-Directories-And-Versions-To-Unpack-From-Dotnet-Package -Zip $Zip -OutPath $OutPath + + foreach ($entry in $Zip.Entries) { + $PathWithVersion = Get-Path-Prefix-With-Version $entry.FullName + if (($PathWithVersion -eq $null) -Or ($DirectoriesToUnpack -contains $PathWithVersion)) { + $DestinationPath = Get-Absolute-Path $(Join-Path -Path $OutPath -ChildPath $entry.FullName) + $DestinationDir = Split-Path -Parent $DestinationPath + $OverrideFiles=$OverrideNonVersionedFiles -Or (-Not (Test-Path $DestinationPath)) + if ((-Not $DestinationPath.EndsWith("\")) -And $OverrideFiles) { + New-Item -ItemType Directory -Force -Path $DestinationDir | Out-Null + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $DestinationPath, $OverrideNonVersionedFiles) + } + } + } + } + finally { + if ($Zip -ne $null) { + $Zip.Dispose() + } + } +} + +function DownloadFile($Source, [string]$OutPath) { + if ($Source -notlike "http*") { + # Using System.IO.Path.GetFullPath to get the current directory + # does not work in this context - $pwd gives the current directory + if (![System.IO.Path]::IsPathRooted($Source)) { + $Source = $(Join-Path -Path $pwd -ChildPath $Source) + } + $Source = Get-Absolute-Path $Source + Say "Copying file from $Source to $OutPath" + Copy-Item $Source $OutPath + return + } + + $Stream = $null + + try { + $Response = GetHTTPResponse -Uri $Source + $Stream = $Response.Content.ReadAsStreamAsync().Result + $File = [System.IO.File]::Create($OutPath) + $Stream.CopyTo($File) + $File.Close() + } + finally { + if ($Stream -ne $null) { + $Stream.Dispose() + } + } +} + +function Prepend-Sdk-InstallRoot-To-Path([string]$InstallRoot, [string]$BinFolderRelativePath) { + $BinPath = Get-Absolute-Path $(Join-Path -Path $InstallRoot -ChildPath $BinFolderRelativePath) + if (-Not $NoPath) { + $SuffixedBinPath = "$BinPath;" + if (-Not $env:path.Contains($SuffixedBinPath)) { + Say "Adding to current process PATH: `"$BinPath`". Note: This change will not be visible if PowerShell was run as a child process." + $env:path = $SuffixedBinPath + $env:path + } else { + Say-Verbose "Current process PATH already contains `"$BinPath`"" + } + } + else { + Say "Binaries of dotnet can be found in $BinPath" + } +} + +$CLIArchitecture = Get-CLIArchitecture-From-Architecture $Architecture +$SpecificVersion = Get-Specific-Version-From-Version -AzureFeed $AzureFeed -Channel $Channel -Version $Version +$DownloadLink = Get-Download-Link -AzureFeed $AzureFeed -SpecificVersion $SpecificVersion -CLIArchitecture $CLIArchitecture +$LegacyDownloadLink = Get-LegacyDownload-Link -AzureFeed $AzureFeed -SpecificVersion $SpecificVersion -CLIArchitecture $CLIArchitecture + +$InstallRoot = Resolve-Installation-Path $InstallDir +Say-Verbose "InstallRoot: $InstallRoot" +$ScriptName = $MyInvocation.MyCommand.Name + +if ($DryRun) { + Say "Payload URLs:" + Say "Primary named payload URL: $DownloadLink" + if ($LegacyDownloadLink) { + Say "Legacy named payload URL: $LegacyDownloadLink" + } + $RepeatableCommand = ".\$ScriptName -Version `"$SpecificVersion`" -InstallDir `"$InstallRoot`" -Architecture `"$CLIArchitecture`"" + if ($Runtime -eq "dotnet") { + $RepeatableCommand+=" -Runtime `"dotnet`"" + } + elseif ($Runtime -eq "aspnetcore") { + $RepeatableCommand+=" -Runtime `"aspnetcore`"" + } + foreach ($key in $MyInvocation.BoundParameters.Keys) { + if (-not (@("Architecture","Channel","DryRun","InstallDir","Runtime","SharedRuntime","Version") -contains $key)) { + $RepeatableCommand+=" -$key `"$($MyInvocation.BoundParameters[$key])`"" + } + } + Say "Repeatable invocation: $RepeatableCommand" + exit 0 +} + +if ($Runtime -eq "dotnet") { + $assetName = ".NET Core Runtime" + $dotnetPackageRelativePath = "shared\Microsoft.NETCore.App" +} +elseif ($Runtime -eq "aspnetcore") { + $assetName = "ASP.NET Core Runtime" + $dotnetPackageRelativePath = "shared\Microsoft.AspNetCore.App" +} +elseif (-not $Runtime) { + $assetName = ".NET Core SDK" + $dotnetPackageRelativePath = "sdk" +} +else { + throw "Invalid value for `$Runtime" +} + +# Check if the SDK version is already installed. +$isAssetInstalled = Is-Dotnet-Package-Installed -InstallRoot $InstallRoot -RelativePathToPackage $dotnetPackageRelativePath -SpecificVersion $SpecificVersion +if ($isAssetInstalled) { + Say "$assetName version $SpecificVersion is already installed." + Prepend-Sdk-InstallRoot-To-Path -InstallRoot $InstallRoot -BinFolderRelativePath $BinFolderRelativePath + exit 0 +} + +New-Item -ItemType Directory -Force -Path $InstallRoot | Out-Null + +$installDrive = $((Get-Item $InstallRoot).PSDrive.Name); +$diskInfo = Get-PSDrive -Name $installDrive +if ($diskInfo.Free / 1MB -le 100) { + Say "There is not enough disk space on drive ${installDrive}:" + exit 0 +} + +$ZipPath = [System.IO.Path]::combine([System.IO.Path]::GetTempPath(), [System.IO.Path]::GetRandomFileName()) +Say-Verbose "Zip path: $ZipPath" + +$DownloadFailed = $false +Say "Downloading link: $DownloadLink" +try { + DownloadFile -Source $DownloadLink -OutPath $ZipPath +} +catch { + Say "Cannot download: $DownloadLink" + if ($LegacyDownloadLink) { + $DownloadLink = $LegacyDownloadLink + $ZipPath = [System.IO.Path]::combine([System.IO.Path]::GetTempPath(), [System.IO.Path]::GetRandomFileName()) + Say-Verbose "Legacy zip path: $ZipPath" + Say "Downloading legacy link: $DownloadLink" + try { + DownloadFile -Source $DownloadLink -OutPath $ZipPath + } + catch { + Say "Cannot download: $DownloadLink" + $DownloadFailed = $true + } + } + else { + $DownloadFailed = $true + } +} + +if ($DownloadFailed) { + throw "Could not find/download: `"$assetName`" with version = $SpecificVersion`nRefer to: https://aka.ms/dotnet-os-lifecycle for information on .NET Core support" +} + +Say "Extracting zip from $DownloadLink" +Extract-Dotnet-Package -ZipPath $ZipPath -OutPath $InstallRoot + +# Check if the SDK version is now installed; if not, fail the installation. +$isAssetInstalled = Is-Dotnet-Package-Installed -InstallRoot $InstallRoot -RelativePathToPackage $dotnetPackageRelativePath -SpecificVersion $SpecificVersion +if (!$isAssetInstalled) { + throw "`"$assetName`" with version = $SpecificVersion failed to install with an unknown error." +} + +Remove-Item $ZipPath + +Prepend-Sdk-InstallRoot-To-Path -InstallRoot $InstallRoot -BinFolderRelativePath $BinFolderRelativePath + +Say "Installation finished" +exit 0 diff --git a/externals/install-dotnet.sh b/externals/install-dotnet.sh new file mode 100644 index 0000000..2217d47 --- /dev/null +++ b/externals/install-dotnet.sh @@ -0,0 +1,1019 @@ +#!/usr/bin/env bash +# Copyright (c) .NET Foundation and contributors. All rights reserved. +# Licensed under the MIT license. See LICENSE file in the project root for full license information. +# + +# Stop script on NZEC +set -e +# Stop script if unbound variable found (use ${var:-} if intentional) +set -u +# By default cmd1 | cmd2 returns exit code of cmd2 regardless of cmd1 success +# This is causing it to fail +set -o pipefail + +# Use in the the functions: eval $invocation +invocation='say_verbose "Calling: ${yellow:-}${FUNCNAME[0]} ${green:-}$*${normal:-}"' + +# standard output may be used as a return value in the functions +# we need a way to write text on the screen in the functions so that +# it won't interfere with the return value. +# Exposing stream 3 as a pipe to standard output of the script itself +exec 3>&1 + +# Setup some colors to use. These need to work in fairly limited shells, like the Ubuntu Docker container where there are only 8 colors. +# See if stdout is a terminal +if [ -t 1 ] && command -v tput > /dev/null; then + # see if it supports colors + ncolors=$(tput colors) + if [ -n "$ncolors" ] && [ $ncolors -ge 8 ]; then + bold="$(tput bold || echo)" + normal="$(tput sgr0 || echo)" + black="$(tput setaf 0 || echo)" + red="$(tput setaf 1 || echo)" + green="$(tput setaf 2 || echo)" + yellow="$(tput setaf 3 || echo)" + blue="$(tput setaf 4 || echo)" + magenta="$(tput setaf 5 || echo)" + cyan="$(tput setaf 6 || echo)" + white="$(tput setaf 7 || echo)" + fi +fi + +say_warning() { + printf "%b\n" "${yellow:-}dotnet_install: Warning: $1${normal:-}" +} + +say_err() { + printf "%b\n" "${red:-}dotnet_install: Error: $1${normal:-}" >&2 +} + +say() { + # using stream 3 (defined in the beginning) to not interfere with stdout of functions + # which may be used as return value + printf "%b\n" "${cyan:-}dotnet-install:${normal:-} $1" >&3 +} + +say_verbose() { + if [ "$verbose" = true ]; then + say "$1" + fi +} + +# This platform list is finite - if the SDK/Runtime has supported Linux distribution-specific assets, +# then and only then should the Linux distribution appear in this list. +# Adding a Linux distribution to this list does not imply distribution-specific support. +get_legacy_os_name_from_platform() { + eval $invocation + + platform="$1" + case "$platform" in + "centos.7") + echo "centos" + return 0 + ;; + "debian.8") + echo "debian" + return 0 + ;; + "debian.9") + echo "debian.9" + return 0 + ;; + "fedora.23") + echo "fedora.23" + return 0 + ;; + "fedora.24") + echo "fedora.24" + return 0 + ;; + "fedora.27") + echo "fedora.27" + return 0 + ;; + "fedora.28") + echo "fedora.28" + return 0 + ;; + "opensuse.13.2") + echo "opensuse.13.2" + return 0 + ;; + "opensuse.42.1") + echo "opensuse.42.1" + return 0 + ;; + "opensuse.42.3") + echo "opensuse.42.3" + return 0 + ;; + "rhel.7"*) + echo "rhel" + return 0 + ;; + "ubuntu.14.04") + echo "ubuntu" + return 0 + ;; + "ubuntu.16.04") + echo "ubuntu.16.04" + return 0 + ;; + "ubuntu.16.10") + echo "ubuntu.16.10" + return 0 + ;; + "ubuntu.18.04") + echo "ubuntu.18.04" + return 0 + ;; + "alpine.3.4.3") + echo "alpine" + return 0 + ;; + esac + return 1 +} + +get_linux_platform_name() { + eval $invocation + + if [ -n "$runtime_id" ]; then + echo "${runtime_id%-*}" + return 0 + else + if [ -e /etc/os-release ]; then + . /etc/os-release + echo "$ID.$VERSION_ID" + return 0 + elif [ -e /etc/redhat-release ]; then + local redhatRelease=$( /dev/null 2>&1 + return $? +} + + +check_min_reqs() { + local hasMinimum=false + if machine_has "curl"; then + hasMinimum=true + elif machine_has "wget"; then + hasMinimum=true + fi + + if [ "$hasMinimum" = "false" ]; then + say_err "curl (recommended) or wget are required to download dotnet. Install missing prerequisite to proceed." + return 1 + fi + return 0 +} + +check_pre_reqs() { + eval $invocation + + if [ "${DOTNET_INSTALL_SKIP_PREREQS:-}" = "1" ]; then + return 0 + fi + + if [ "$(uname)" = "Linux" ]; then + if [ ! -x "$(command -v ldconfig)" ]; then + echo "ldconfig is not in PATH, trying /sbin/ldconfig." + LDCONFIG_COMMAND="/sbin/ldconfig" + else + LDCONFIG_COMMAND="ldconfig" + fi + + local librarypath=${LD_LIBRARY_PATH:-} + LDCONFIG_COMMAND="$LDCONFIG_COMMAND -NXv ${librarypath//:/ }" + + [ -z "$($LDCONFIG_COMMAND 2>/dev/null | grep libunwind)" ] && say_warning "Unable to locate libunwind. Probable prerequisite missing; install libunwind." + [ -z "$($LDCONFIG_COMMAND 2>/dev/null | grep libssl)" ] && say_warning "Unable to locate libssl. Probable prerequisite missing; install libssl." + [ -z "$($LDCONFIG_COMMAND 2>/dev/null | grep libicu)" ] && say_warning "Unable to locate libicu. Probable prerequisite missing; install libicu." + [ -z "$($LDCONFIG_COMMAND 2>/dev/null | grep -F libcurl.so)" ] && say_warning "Unable to locate libcurl. Probable prerequisite missing; install libcurl." + fi + + return 0 +} + +# args: +# input - $1 +to_lowercase() { + #eval $invocation + + echo "$1" | tr '[:upper:]' '[:lower:]' + return 0 +} + +# args: +# input - $1 +remove_trailing_slash() { + #eval $invocation + + local input="${1:-}" + echo "${input%/}" + return 0 +} + +# args: +# input - $1 +remove_beginning_slash() { + #eval $invocation + + local input="${1:-}" + echo "${input#/}" + return 0 +} + +# args: +# root_path - $1 +# child_path - $2 - this parameter can be empty +combine_paths() { + eval $invocation + + # TODO: Consider making it work with any number of paths. For now: + if [ ! -z "${3:-}" ]; then + say_err "combine_paths: Function takes two parameters." + return 1 + fi + + local root_path="$(remove_trailing_slash "$1")" + local child_path="$(remove_beginning_slash "${2:-}")" + say_verbose "combine_paths: root_path=$root_path" + say_verbose "combine_paths: child_path=$child_path" + echo "$root_path/$child_path" + return 0 +} + +get_machine_architecture() { + eval $invocation + + if command -v uname > /dev/null; then + CPUName=$(uname -m) + case $CPUName in + armv7l) + echo "arm" + return 0 + ;; + aarch64) + echo "arm64" + return 0 + ;; + esac + fi + + # Always default to 'x64' + echo "x64" + return 0 +} + +# args: +# architecture - $1 +get_normalized_architecture_from_architecture() { + eval $invocation + + local architecture="$(to_lowercase "$1")" + case "$architecture" in + \) + echo "$(get_normalized_architecture_from_architecture "$(get_machine_architecture)")" + return 0 + ;; + amd64|x64) + echo "x64" + return 0 + ;; + arm) + echo "arm" + return 0 + ;; + arm64) + echo "arm64" + return 0 + ;; + esac + + say_err "Architecture \`$architecture\` not supported. If you think this is a bug, report it at https://github.com/dotnet/cli/issues" + return 1 +} + +# The version text returned from the feeds is a 1-line or 2-line string: +# For the SDK and the dotnet runtime (2 lines): +# Line 1: # commit_hash +# Line 2: # 4-part version +# For the aspnetcore runtime (1 line): +# Line 1: # 4-part version + +# args: +# version_text - stdin +get_version_from_version_info() { + eval $invocation + + cat | tail -n 1 | sed 's/\r$//' + return 0 +} + +# args: +# install_root - $1 +# relative_path_to_package - $2 +# specific_version - $3 +is_dotnet_package_installed() { + eval $invocation + + local install_root="$1" + local relative_path_to_package="$2" + local specific_version="${3//[$'\t\r\n']}" + + local dotnet_package_path="$(combine_paths "$(combine_paths "$install_root" "$relative_path_to_package")" "$specific_version")" + say_verbose "is_dotnet_package_installed: dotnet_package_path=$dotnet_package_path" + + if [ -d "$dotnet_package_path" ]; then + return 0 + else + return 1 + fi +} + +# args: +# azure_feed - $1 +# channel - $2 +# normalized_architecture - $3 +# coherent - $4 +get_latest_version_info() { + eval $invocation + + local azure_feed="$1" + local channel="$2" + local normalized_architecture="$3" + local coherent="$4" + + local version_file_url=null + if [[ "$runtime" == "dotnet" ]]; then + version_file_url="$uncached_feed/Runtime/$channel/latest.version" + elif [[ "$runtime" == "aspnetcore" ]]; then + version_file_url="$uncached_feed/aspnetcore/Runtime/$channel/latest.version" + elif [ -z "$runtime" ]; then + if [ "$coherent" = true ]; then + version_file_url="$uncached_feed/Sdk/$channel/latest.coherent.version" + else + version_file_url="$uncached_feed/Sdk/$channel/latest.version" + fi + else + say_err "Invalid value for \$runtime" + return 1 + fi + say_verbose "get_latest_version_info: latest url: $version_file_url" + + download "$version_file_url" + return $? +} + +# args: +# azure_feed - $1 +# channel - $2 +# normalized_architecture - $3 +# version - $4 +get_specific_version_from_version() { + eval $invocation + + local azure_feed="$1" + local channel="$2" + local normalized_architecture="$3" + local version="$(to_lowercase "$4")" + + case "$version" in + latest) + local version_info + version_info="$(get_latest_version_info "$azure_feed" "$channel" "$normalized_architecture" false)" || return 1 + say_verbose "get_specific_version_from_version: version_info=$version_info" + echo "$version_info" | get_version_from_version_info + return 0 + ;; + coherent) + local version_info + version_info="$(get_latest_version_info "$azure_feed" "$channel" "$normalized_architecture" true)" || return 1 + say_verbose "get_specific_version_from_version: version_info=$version_info" + echo "$version_info" | get_version_from_version_info + return 0 + ;; + *) + echo "$version" + return 0 + ;; + esac +} + +# args: +# azure_feed - $1 +# channel - $2 +# normalized_architecture - $3 +# specific_version - $4 +construct_download_link() { + eval $invocation + + local azure_feed="$1" + local channel="$2" + local normalized_architecture="$3" + local specific_version="${4//[$'\t\r\n']}" + + local osname + osname="$(get_current_os_name)" || return 1 + + local download_link=null + if [[ "$runtime" == "dotnet" ]]; then + download_link="$azure_feed/Runtime/$specific_version/dotnet-runtime-$specific_version-$osname-$normalized_architecture.tar.gz" + elif [[ "$runtime" == "aspnetcore" ]]; then + download_link="$azure_feed/aspnetcore/Runtime/$specific_version/aspnetcore-runtime-$specific_version-$osname-$normalized_architecture.tar.gz" + elif [ -z "$runtime" ]; then + download_link="$azure_feed/Sdk/$specific_version/dotnet-sdk-$specific_version-$osname-$normalized_architecture.tar.gz" + else + return 1 + fi + + echo "$download_link" + return 0 +} + +# args: +# azure_feed - $1 +# channel - $2 +# normalized_architecture - $3 +# specific_version - $4 +construct_legacy_download_link() { + eval $invocation + + local azure_feed="$1" + local channel="$2" + local normalized_architecture="$3" + local specific_version="${4//[$'\t\r\n']}" + + local distro_specific_osname + distro_specific_osname="$(get_legacy_os_name)" || return 1 + + local legacy_download_link=null + if [[ "$runtime" == "dotnet" ]]; then + legacy_download_link="$azure_feed/Runtime/$specific_version/dotnet-$distro_specific_osname-$normalized_architecture.$specific_version.tar.gz" + elif [ -z "$runtime" ]; then + legacy_download_link="$azure_feed/Sdk/$specific_version/dotnet-dev-$distro_specific_osname-$normalized_architecture.$specific_version.tar.gz" + else + return 1 + fi + + echo "$legacy_download_link" + return 0 +} + +get_user_install_path() { + eval $invocation + + if [ ! -z "${DOTNET_INSTALL_DIR:-}" ]; then + echo "$DOTNET_INSTALL_DIR" + else + echo "$HOME/.dotnet" + fi + return 0 +} + +# args: +# install_dir - $1 +resolve_installation_path() { + eval $invocation + + local install_dir=$1 + if [ "$install_dir" = "" ]; then + local user_install_path="$(get_user_install_path)" + say_verbose "resolve_installation_path: user_install_path=$user_install_path" + echo "$user_install_path" + return 0 + fi + + echo "$install_dir" + return 0 +} + +# args: +# install_root - $1 +get_installed_version_info() { + eval $invocation + + local install_root="$1" + local version_file="$(combine_paths "$install_root" "$local_version_file_relative_path")" + say_verbose "Local version file: $version_file" + if [ ! -z "$version_file" ] | [ -r "$version_file" ]; then + local version_info="$(cat "$version_file")" + echo "$version_info" + return 0 + fi + + say_verbose "Local version file not found." + return 0 +} + +# args: +# relative_or_absolute_path - $1 +get_absolute_path() { + eval $invocation + + local relative_or_absolute_path=$1 + echo "$(cd "$(dirname "$1")" && pwd -P)/$(basename "$1")" + return 0 +} + +# args: +# input_files - stdin +# root_path - $1 +# out_path - $2 +# override - $3 +copy_files_or_dirs_from_list() { + eval $invocation + + local root_path="$(remove_trailing_slash "$1")" + local out_path="$(remove_trailing_slash "$2")" + local override="$3" + local osname="$(get_current_os_name)" + local override_switch=$( + if [ "$override" = false ]; then + if [[ "$osname" == "linux-musl" ]]; then + printf -- "-u"; + else + printf -- "-n"; + fi + fi) + + cat | uniq | while read -r file_path; do + local path="$(remove_beginning_slash "${file_path#$root_path}")" + local target="$out_path/$path" + if [ "$override" = true ] || (! ([ -d "$target" ] || [ -e "$target" ])); then + mkdir -p "$out_path/$(dirname "$path")" + cp -R $override_switch "$root_path/$path" "$target" + fi + done +} + +# args: +# zip_path - $1 +# out_path - $2 +extract_dotnet_package() { + eval $invocation + + local zip_path="$1" + local out_path="$2" + + local temp_out_path="$(mktemp -d "$temporary_file_template")" + + local failed=false + tar -xzf "$zip_path" -C "$temp_out_path" > /dev/null || failed=true + + local folders_with_version_regex='^.*/[0-9]+\.[0-9]+[^/]+/' + find "$temp_out_path" -type f | grep -Eo "$folders_with_version_regex" | sort | copy_files_or_dirs_from_list "$temp_out_path" "$out_path" false + find "$temp_out_path" -type f | grep -Ev "$folders_with_version_regex" | copy_files_or_dirs_from_list "$temp_out_path" "$out_path" "$override_non_versioned_files" + + rm -rf "$temp_out_path" + + if [ "$failed" = true ]; then + say_err "Extraction failed" + return 1 + fi +} + +# args: +# remote_path - $1 +# [out_path] - $2 - stdout if not provided +download() { + eval $invocation + + local remote_path="$1" + local out_path="${2:-}" + + if [[ "$remote_path" != "http"* ]]; then + cp "$remote_path" "$out_path" + return $? + fi + + local failed=false + if machine_has "curl"; then + downloadcurl "$remote_path" "$out_path" || failed=true + elif machine_has "wget"; then + downloadwget "$remote_path" "$out_path" || failed=true + else + failed=true + fi + if [ "$failed" = true ]; then + say_verbose "Download failed: $remote_path" + return 1 + fi + return 0 +} + +downloadcurl() { + eval $invocation + local remote_path="$1" + local out_path="${2:-}" + + # Append feed_credential as late as possible before calling curl to avoid logging feed_credential + remote_path="${remote_path}${feed_credential}" + + local failed=false + if [ -z "$out_path" ]; then + curl --retry 10 -sSL -f --create-dirs "$remote_path" || failed=true + else + curl --retry 10 -sSL -f --create-dirs -o "$out_path" "$remote_path" || failed=true + fi + if [ "$failed" = true ]; then + say_verbose "Curl download failed" + return 1 + fi + return 0 +} + +downloadwget() { + eval $invocation + local remote_path="$1" + local out_path="${2:-}" + + # Append feed_credential as late as possible before calling wget to avoid logging feed_credential + remote_path="${remote_path}${feed_credential}" + + local failed=false + if [ -z "$out_path" ]; then + wget -q --tries 10 -O - "$remote_path" || failed=true + else + wget --tries 10 -O "$out_path" "$remote_path" || failed=true + fi + if [ "$failed" = true ]; then + say_verbose "Wget download failed" + return 1 + fi + return 0 +} + +calculate_vars() { + eval $invocation + valid_legacy_download_link=true + + normalized_architecture="$(get_normalized_architecture_from_architecture "$architecture")" + say_verbose "normalized_architecture=$normalized_architecture" + + specific_version="$(get_specific_version_from_version "$azure_feed" "$channel" "$normalized_architecture" "$version")" + say_verbose "specific_version=$specific_version" + if [ -z "$specific_version" ]; then + say_err "Could not resolve version information." + return 1 + fi + + download_link="$(construct_download_link "$azure_feed" "$channel" "$normalized_architecture" "$specific_version")" + say_verbose "Constructed primary named payload URL: $download_link" + + legacy_download_link="$(construct_legacy_download_link "$azure_feed" "$channel" "$normalized_architecture" "$specific_version")" || valid_legacy_download_link=false + + if [ "$valid_legacy_download_link" = true ]; then + say_verbose "Constructed legacy named payload URL: $legacy_download_link" + else + say_verbose "Cound not construct a legacy_download_link; omitting..." + fi + + install_root="$(resolve_installation_path "$install_dir")" + say_verbose "InstallRoot: $install_root" +} + +install_dotnet() { + eval $invocation + local download_failed=false + local asset_name='' + local asset_relative_path='' + + if [[ "$runtime" == "dotnet" ]]; then + asset_relative_path="shared/Microsoft.NETCore.App" + asset_name=".NET Core Runtime" + elif [[ "$runtime" == "aspnetcore" ]]; then + asset_relative_path="shared/Microsoft.AspNetCore.App" + asset_name="ASP.NET Core Runtime" + elif [ -z "$runtime" ]; then + asset_relative_path="sdk" + asset_name=".NET Core SDK" + else + say_err "Invalid value for \$runtime" + return 1 + fi + + # Check if the SDK version is already installed. + if is_dotnet_package_installed "$install_root" "$asset_relative_path" "$specific_version"; then + say "$asset_name version $specific_version is already installed." + return 0 + fi + + mkdir -p "$install_root" + zip_path="$(mktemp "$temporary_file_template")" + say_verbose "Zip path: $zip_path" + + say "Downloading link: $download_link" + + # Failures are normal in the non-legacy case for ultimately legacy downloads. + # Do not output to stderr, since output to stderr is considered an error. + download "$download_link" "$zip_path" 2>&1 || download_failed=true + + # if the download fails, download the legacy_download_link + if [ "$download_failed" = true ]; then + say "Cannot download: $download_link" + + if [ "$valid_legacy_download_link" = true ]; then + download_failed=false + download_link="$legacy_download_link" + zip_path="$(mktemp "$temporary_file_template")" + say_verbose "Legacy zip path: $zip_path" + say "Downloading legacy link: $download_link" + download "$download_link" "$zip_path" 2>&1 || download_failed=true + + if [ "$download_failed" = true ]; then + say "Cannot download: $download_link" + fi + fi + fi + + if [ "$download_failed" = true ]; then + say_err "Could not find/download: \`$asset_name\` with version = $specific_version" + say_err "Refer to: https://aka.ms/dotnet-os-lifecycle for information on .NET Core support" + return 1 + fi + + say "Extracting zip from $download_link" + extract_dotnet_package "$zip_path" "$install_root" + + # Check if the SDK version is now installed; if not, fail the installation. + if ! is_dotnet_package_installed "$install_root" "$asset_relative_path" "$specific_version"; then + say_err "\`$asset_name\` with version = $specific_version failed to install with an unknown error." + return 1 + fi + + return 0 +} + +args=("$@") + +local_version_file_relative_path="/.version" +bin_folder_relative_path="" +temporary_file_template="${TMPDIR:-/tmp}/dotnet.XXXXXXXXX" + +channel="LTS" +version="Latest" +install_dir="" +architecture="" +dry_run=false +no_path=false +no_cdn=false +azure_feed="https://dotnetcli.azureedge.net/dotnet" +uncached_feed="https://dotnetcli.blob.core.windows.net/dotnet" +feed_credential="" +verbose=false +runtime="" +runtime_id="" +override_non_versioned_files=true +non_dynamic_parameters="" + +while [ $# -ne 0 ] +do + name="$1" + case "$name" in + -c|--channel|-[Cc]hannel) + shift + channel="$1" + ;; + -v|--version|-[Vv]ersion) + shift + version="$1" + ;; + -i|--install-dir|-[Ii]nstall[Dd]ir) + shift + install_dir="$1" + ;; + --arch|--architecture|-[Aa]rch|-[Aa]rchitecture) + shift + architecture="$1" + ;; + --shared-runtime|-[Ss]hared[Rr]untime) + say_warning "The --shared-runtime flag is obsolete and may be removed in a future version of this script. The recommended usage is to specify '--runtime dotnet'." + if [ -z "$runtime" ]; then + runtime="dotnet" + fi + ;; + --runtime|-[Rr]untime) + shift + runtime="$1" + if [[ "$runtime" != "dotnet" ]] && [[ "$runtime" != "aspnetcore" ]]; then + say_err "Unsupported value for --runtime: '$1'. Valid values are 'dotnet' and 'aspnetcore'." + exit 1 + fi + ;; + --dry-run|-[Dd]ry[Rr]un) + dry_run=true + ;; + --no-path|-[Nn]o[Pp]ath) + no_path=true + non_dynamic_parameters+=" $name" + ;; + --verbose|-[Vv]erbose) + verbose=true + non_dynamic_parameters+=" $name" + ;; + --no-cdn|-[Nn]o[Cc]dn) + no_cdn=true + non_dynamic_parameters+=" $name" + ;; + --azure-feed|-[Aa]zure[Ff]eed) + shift + azure_feed="$1" + non_dynamic_parameters+=" $name "\""$1"\""" + ;; + --uncached-feed|-[Uu]ncached[Ff]eed) + shift + uncached_feed="$1" + non_dynamic_parameters+=" $name "\""$1"\""" + ;; + --feed-credential|-[Ff]eed[Cc]redential) + shift + feed_credential="$1" + non_dynamic_parameters+=" $name "\""$1"\""" + ;; + --runtime-id|-[Rr]untime[Ii]d) + shift + runtime_id="$1" + non_dynamic_parameters+=" $name "\""$1"\""" + ;; + --skip-non-versioned-files|-[Ss]kip[Nn]on[Vv]ersioned[Ff]iles) + override_non_versioned_files=false + non_dynamic_parameters+=" $name" + ;; + -?|--?|-h|--help|-[Hh]elp) + script_name="$(basename "$0")" + echo ".NET Tools Installer" + echo "Usage: $script_name [-c|--channel ] [-v|--version ] [-p|--prefix ]" + echo " $script_name -h|-?|--help" + echo "" + echo "$script_name is a simple command line interface for obtaining dotnet cli." + echo "" + echo "Options:" + echo " -c,--channel Download from the channel specified, Defaults to \`$channel\`." + echo " -Channel" + echo " Possible values:" + echo " - Current - most current release" + echo " - LTS - most current supported release" + echo " - 2-part version in a format A.B - represents a specific release" + echo " examples: 2.0; 1.0" + echo " - Branch name" + echo " examples: release/2.0.0; Master" + echo " Note: The version parameter overrides the channel parameter." + echo " -v,--version Use specific VERSION, Defaults to \`$version\`." + echo " -Version" + echo " Possible values:" + echo " - latest - most latest build on specific channel" + echo " - coherent - most latest coherent build on specific channel" + echo " coherent applies only to SDK downloads" + echo " - 3-part version in a format A.B.C - represents specific version of build" + echo " examples: 2.0.0-preview2-006120; 1.1.0" + echo " -i,--install-dir Install under specified location (see Install Location below)" + echo " -InstallDir" + echo " --architecture Architecture of dotnet binaries to be installed, Defaults to \`$architecture\`." + echo " --arch,-Architecture,-Arch" + echo " Possible values: x64, arm, and arm64" + echo " --runtime Installs a shared runtime only, without the SDK." + echo " -Runtime" + echo " Possible values:" + echo " - dotnet - the Microsoft.NETCore.App shared runtime" + echo " - aspnetcore - the Microsoft.AspNetCore.App shared runtime" + echo " --skip-non-versioned-files Skips non-versioned files if they already exist, such as the dotnet executable." + echo " -SkipNonVersionedFiles" + echo " --dry-run,-DryRun Do not perform installation. Display download link." + echo " --no-path, -NoPath Do not set PATH for the current process." + echo " --verbose,-Verbose Display diagnostics information." + echo " --azure-feed,-AzureFeed Azure feed location. Defaults to $azure_feed, This parameter typically is not changed by the user." + echo " --uncached-feed,-UncachedFeed Uncached feed location. This parameter typically is not changed by the user." + echo " --no-cdn,-NoCdn Disable downloading from the Azure CDN, and use the uncached feed directly." + echo " --feed-credential,-FeedCredential Azure feed shared access token. This parameter typically is not specified." + echo " --runtime-id Installs the .NET Tools for the given platform (use linux-x64 for portable linux)." + echo " -RuntimeId" + echo " -?,--?,-h,--help,-Help Shows this help message" + echo "" + echo "Obsolete parameters:" + echo " --shared-runtime The recommended alternative is '--runtime dotnet'." + echo " -SharedRuntime Installs just the shared runtime bits, not the entire SDK." + echo "" + echo "Install Location:" + echo " Location is chosen in following order:" + echo " - --install-dir option" + echo " - Environmental variable DOTNET_INSTALL_DIR" + echo " - $HOME/.dotnet" + exit 0 + ;; + *) + say_err "Unknown argument \`$name\`" + exit 1 + ;; + esac + + shift +done + +if [ "$no_cdn" = true ]; then + azure_feed="$uncached_feed" +fi + +check_min_reqs +calculate_vars +script_name=$(basename "$0") + +if [ "$dry_run" = true ]; then + say "Payload URLs:" + say "Primary named payload URL: $download_link" + if [ "$valid_legacy_download_link" = true ]; then + say "Legacy named payload URL: $legacy_download_link" + fi + repeatable_command="./$script_name --version "\""$specific_version"\"" --install-dir "\""$install_root"\"" --architecture "\""$normalized_architecture"\""" + if [[ "$runtime" == "dotnet" ]]; then + repeatable_command+=" --runtime "\""dotnet"\""" + elif [[ "$runtime" == "aspnetcore" ]]; then + repeatable_command+=" --runtime "\""aspnetcore"\""" + fi + repeatable_command+="$non_dynamic_parameters" + say "Repeatable invocation: $repeatable_command" + exit 0 +fi + +check_pre_reqs +install_dotnet + +bin_path="$(get_absolute_path "$(combine_paths "$install_root" "$bin_folder_relative_path")")" +if [ "$no_path" = false ]; then + say "Adding to current process PATH: \`$bin_path\`. Note: This change will be visible only when sourcing script." + export PATH="$bin_path":"$PATH" +else + say "Binaries of dotnet can be found in $bin_path" +fi + +say "Installation finished successfully." diff --git a/lib/installer.js b/lib/installer.js new file mode 100644 index 0000000..21320e5 --- /dev/null +++ b/lib/installer.js @@ -0,0 +1,317 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +// Load tempDirectory before it gets wiped by tool-cache +let tempDirectory = process.env['RUNNER_TEMPDIRECTORY'] || ''; +const core = __importStar(require("@actions/core")); +const exec = __importStar(require("@actions/exec")); +const io = __importStar(require("@actions/io")); +const tc = __importStar(require("@actions/tool-cache")); +const httpClient = require("typed-rest-client/HttpClient"); +const fs_1 = require("fs"); +const os = __importStar(require("os")); +const path = __importStar(require("path")); +const semver = __importStar(require("semver")); +const util = __importStar(require("util")); +const IS_WINDOWS = process.platform === 'win32'; +if (!tempDirectory) { + let baseLocation; + if (IS_WINDOWS) { + // On windows use the USERPROFILE env variable + baseLocation = process.env['USERPROFILE'] || 'C:\\'; + } + else { + if (process.platform === 'darwin') { + baseLocation = '/Users'; + } + else { + baseLocation = '/home'; + } + } + tempDirectory = path.join(baseLocation, 'actions', 'temp'); +} +class DotnetCoreInstaller { + constructor(version) { + if (semver.valid(semver.clean(version) || '') == null) { + throw 'Implicit version not permitted'; + } + this.version = version; + this.cachedToolName = 'dncs'; + this.arch = 'x64'; + } + installDotnet() { + return __awaiter(this, void 0, void 0, function* () { + // Check cache + let toolPath; + let osSuffixes = yield this.detectMachineOS(); + let parts = osSuffixes[0].split('-'); + if (parts.length > 1) { + this.arch = parts[1]; + } + toolPath = this.getLocalTool(); + if (!toolPath) { + // download, extract, cache + console.log('Getting a download url', this.version); + let downloadUrls = yield this.getDownloadUrls(osSuffixes, this.version); + toolPath = yield this.downloadAndInstall(downloadUrls); + } + else { + console.log('Using cached tool'); + } + // Prepend the tools path. instructs the agent to prepend for future tasks + core.addPath(toolPath); + try { + let globalToolPath = ''; + if (IS_WINDOWS) { + globalToolPath = path.join(process.env.USERPROFILE || '', '.dotnet\\tools'); + } + else { + globalToolPath = path.join(process.env.HOME || '', '.dotnet/tools'); + } + yield io.mkdirP(globalToolPath); + core.addPath(globalToolPath); + } + catch (error) { + //nop + } + // Set DOTNET_ROOT for dotnet core Apphost to find runtime since it is installed to a non well-known location. + core.exportVariable('DOTNET_ROOT', toolPath); + }); + } + getLocalTool() { + console.log('Checking tool cache'); + return tc.find(this.cachedToolName, this.version, this.arch); + } + detectMachineOS() { + return __awaiter(this, void 0, void 0, function* () { + let osSuffix = []; + let output = ''; + let resultCode = 0; + if (IS_WINDOWS) { + let escapedScript = path + .join(__dirname, '..', 'externals', 'get-os-platform.ps1') + .replace(/'/g, "''"); + let command = `& '${escapedScript}'`; + const powershellPath = yield io.which('powershell', true); + resultCode = yield exec.exec(`"${powershellPath}"`, [ + '-NoLogo', + '-Sta', + '-NoProfile', + '-NonInteractive', + '-ExecutionPolicy', + 'Unrestricted', + '-Command', + command + ], { + listeners: { + stdout: (data) => { + output += data.toString(); + } + } + }); + } + else { + let scriptPath = path.join(__dirname, '..', 'externals', 'get-os-distro.sh'); + fs_1.chmodSync(scriptPath, '777'); + const toolPath = yield io.which(scriptPath, true); + resultCode = yield exec.exec(`"${toolPath}"`, [], { + listeners: { + stdout: (data) => { + output += data.toString(); + } + } + }); + } + if (resultCode != 0) { + throw `Failed to detect os with result code ${resultCode}`; + } + let index; + if ((index = output.indexOf('Primary:')) >= 0) { + let primary = output.substr(index + 'Primary:'.length).split(os.EOL)[0]; + osSuffix.push(primary); + } + if ((index = output.indexOf('Legacy:')) >= 0) { + let legacy = output.substr(index + 'Legacy:'.length).split(os.EOL)[0]; + osSuffix.push(legacy); + } + if (osSuffix.length == 0) { + throw 'Could not detect platform'; + } + return osSuffix; + }); + } + downloadAndInstall(downloadUrls) { + return __awaiter(this, void 0, void 0, function* () { + let downloaded = false; + let downloadPath = ''; + for (const url of downloadUrls) { + try { + downloadPath = yield tc.downloadTool(url); + downloaded = true; + break; + } + catch (error) { + console.log('Could Not Download', url, JSON.stringify(error)); + } + } + if (!downloaded) { + throw 'Failed to download package'; + } + // extract + console.log('Extracting Package', downloadPath); + let extPath = IS_WINDOWS + ? yield tc.extractZip(downloadPath) + : yield tc.extractTar(downloadPath); + // cache tool + console.log('Caching tool'); + let cachedDir = yield tc.cacheDir(extPath, this.cachedToolName, this.version, this.arch); + console.log('Successfully installed', this.version); + return cachedDir; + }); + } + // OsSuffixes - The suffix which is a part of the file name ex- linux-x64, windows-x86 + // Type - SDK / Runtime + // Version - Version of the SDK/Runtime + getDownloadUrls(osSuffixes, version) { + return __awaiter(this, void 0, void 0, function* () { + let downloadUrls = []; + let releasesJSON = yield this.getReleasesJson(); + let releasesInfo = JSON.parse(yield releasesJSON.readBody()); + releasesInfo = releasesInfo.filter((releaseInfo) => { + return (releaseInfo['version-sdk'] === version || + releaseInfo['version-sdk-display'] === version); + }); + if (releasesInfo.length != 0) { + let release = releasesInfo[0]; + let blobUrl = release['blob-sdk']; + let dlcUrl = release['dlc--sdk']; + let fileName = release['sdk-' + osSuffixes[0]] + ? release['sdk-' + osSuffixes[0]] + : release['sdk-' + osSuffixes[1]]; + if (!!fileName) { + fileName = fileName.trim(); + // For some latest version, the filename itself can be full download url. + // Do a very basic check for url(instead of regex) as the url is only for downloading and + // is coming from .net core releases json and not some ransom user input + if (fileName.toLowerCase().startsWith('https://')) { + downloadUrls.push(fileName); + } + else { + if (!!blobUrl) { + downloadUrls.push(util.format('%s%s', blobUrl.trim(), fileName)); + } + if (!!dlcUrl) { + downloadUrls.push(util.format('%s%s', dlcUrl.trim(), fileName)); + } + } + } + else { + throw `The specified version's download links are not correctly formed in the supported versions document => ${DotNetCoreReleasesUrl}`; + } + } + else { + console.log(`Could not fetch download information for version ${version}`); + downloadUrls = yield this.getFallbackDownloadUrls(version); + } + if (downloadUrls.length == 0) { + throw `Could not construct download URL. Please ensure that specified version ${version} is valid.`; + } + return downloadUrls; + }); + } + getReleasesJson() { + var httpCallbackClient = new httpClient.HttpClient('setup-dotnet', [], {}); + return httpCallbackClient.get(DotNetCoreReleasesUrl); + } + getFallbackDownloadUrls(version) { + return __awaiter(this, void 0, void 0, function* () { + let primaryUrlSearchString; + let legacyUrlSearchString; + let output = ''; + let resultCode = 0; + if (IS_WINDOWS) { + let escapedScript = path + .join(__dirname, '..', 'externals', 'install-dotnet.ps1') + .replace(/'/g, "''"); + let command = `& '${escapedScript}' -Version ${version} -DryRun`; + const powershellPath = yield io.which('powershell', true); + resultCode = yield exec.exec(`"${powershellPath}"`, [ + '-NoLogo', + '-Sta', + '-NoProfile', + '-NonInteractive', + '-ExecutionPolicy', + 'Unrestricted', + '-Command', + command + ], { + listeners: { + stdout: (data) => { + output += data.toString(); + } + } + }); + primaryUrlSearchString = 'dotnet-install: Primary - '; + legacyUrlSearchString = 'dotnet-install: Legacy - '; + } + else { + let escapedScript = path + .join(__dirname, '..', 'externals', 'install-dotnet.sh') + .replace(/'/g, "''"); + fs_1.chmodSync(escapedScript, '777'); + const scriptPath = yield io.which(escapedScript, true); + resultCode = yield exec.exec(`"${scriptPath}"`, ['--version', version, '--dry-run'], { + listeners: { + stdout: (data) => { + output += data.toString(); + } + } + }); + primaryUrlSearchString = 'dotnet-install: Payload URL: '; + legacyUrlSearchString = 'dotnet-install: Legacy payload URL: '; + } + if (resultCode != 0) { + throw `Failed to get download urls with result code ${resultCode}`; + } + let primaryUrl = ''; + let legacyUrl = ''; + if (!!output && output.length > 0) { + let lines = output.split(os.EOL); + if (!!lines && lines.length > 0) { + lines.forEach((line) => { + if (!line) { + return; + } + var primarySearchStringIndex = line.indexOf(primaryUrlSearchString); + if (primarySearchStringIndex > -1) { + primaryUrl = line.substring(primarySearchStringIndex + primaryUrlSearchString.length); + return; + } + var legacySearchStringIndex = line.indexOf(legacyUrlSearchString); + if (legacySearchStringIndex > -1) { + legacyUrl = line.substring(legacySearchStringIndex + legacyUrlSearchString.length); + return; + } + }); + } + } + return [primaryUrl, legacyUrl]; + }); + } +} +exports.DotnetCoreInstaller = DotnetCoreInstaller; +const DotNetCoreReleasesUrl = 'https://raw.githubusercontent.com/dotnet/core/master/release-notes/releases.json'; diff --git a/lib/setup-dotnet.js b/lib/setup-dotnet.js new file mode 100644 index 0000000..18bab69 --- /dev/null +++ b/lib/setup-dotnet.js @@ -0,0 +1,40 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const core = __importStar(require("@actions/core")); +const installer = __importStar(require("./installer")); +function run() { + return __awaiter(this, void 0, void 0, function* () { + try { + // + // Version is optional. If supplied, install / use from the tool cache + // If not supplied then task is still used to setup proxy, auth, etc... + // + const version = core.getInput('version'); + if (version) { + const dotnetInstaller = new installer.DotnetCoreInstaller(version); + yield dotnetInstaller.installDotnet(); + } + // TODO: setup proxy from runner proxy config + // TODO: problem matchers registered + } + catch (error) { + core.setFailed(error.message); + } + }); +} +run(); diff --git a/src/installer.ts b/src/installer.ts new file mode 100644 index 0000000..0ae3cf9 --- /dev/null +++ b/src/installer.ts @@ -0,0 +1,343 @@ +// Load tempDirectory before it gets wiped by tool-cache +let tempDirectory = process.env['RUNNER_TEMPDIRECTORY'] || ''; +import * as core from '@actions/core'; +import * as exec from '@actions/exec'; +import * as io from '@actions/io'; +import * as tc from '@actions/tool-cache'; +import httpClient = require('typed-rest-client/HttpClient'); +import {HttpClientResponse} from 'typed-rest-client/HttpClient'; +import {chmodSync} from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import * as semver from 'semver'; +import * as util from 'util'; + +const IS_WINDOWS = process.platform === 'win32'; + +if (!tempDirectory) { + let baseLocation; + if (IS_WINDOWS) { + // On windows use the USERPROFILE env variable + baseLocation = process.env['USERPROFILE'] || 'C:\\'; + } else { + if (process.platform === 'darwin') { + baseLocation = '/Users'; + } else { + baseLocation = '/home'; + } + } + tempDirectory = path.join(baseLocation, 'actions', 'temp'); +} + +export class DotnetCoreInstaller { + constructor(version: string) { + if (semver.valid(semver.clean(version) || '') == null) { + throw 'Implicit version not permitted'; + } + this.version = version; + this.cachedToolName = 'dncs'; + this.arch = 'x64'; + } + + public async installDotnet() { + // Check cache + let toolPath: string; + let osSuffixes = await this.detectMachineOS(); + let parts = osSuffixes[0].split('-'); + if (parts.length > 1) { + this.arch = parts[1]; + } + toolPath = this.getLocalTool(); + + if (!toolPath) { + // download, extract, cache + console.log('Getting a download url', this.version); + let downloadUrls = await this.getDownloadUrls(osSuffixes, this.version); + toolPath = await this.downloadAndInstall(downloadUrls); + } else { + console.log('Using cached tool'); + } + + // Prepend the tools path. instructs the agent to prepend for future tasks + core.addPath(toolPath); + } + + private getLocalTool(): string { + console.log('Checking tool cache'); + return tc.find(this.cachedToolName, this.version, this.arch); + } + + private async detectMachineOS(): Promise { + let osSuffix = []; + let output = ''; + + let resultCode = 0; + if (IS_WINDOWS) { + let escapedScript = path + .join(__dirname, '..', 'externals', 'get-os-platform.ps1') + .replace(/'/g, "''"); + let command = `& '${escapedScript}'`; + + const powershellPath = await io.which('powershell', true); + resultCode = await exec.exec( + `"${powershellPath}"`, + [ + '-NoLogo', + '-Sta', + '-NoProfile', + '-NonInteractive', + '-ExecutionPolicy', + 'Unrestricted', + '-Command', + command + ], + { + listeners: { + stdout: (data: Buffer) => { + output += data.toString(); + } + } + } + ); + } else { + let scriptPath = path.join( + __dirname, + '..', + 'externals', + 'get-os-distro.sh' + ); + chmodSync(scriptPath, '777'); + + const toolPath = await io.which(scriptPath, true); + resultCode = await exec.exec(`"${toolPath}"`, [], { + listeners: { + stdout: (data: Buffer) => { + output += data.toString(); + } + } + }); + } + + if (resultCode != 0) { + throw `Failed to detect os with result code ${resultCode}. Output: ${output}`; + } + + let index; + if ((index = output.indexOf('Primary:')) >= 0) { + let primary = output.substr(index + 'Primary:'.length).split(os.EOL)[0]; + osSuffix.push(primary); + } + + if ((index = output.indexOf('Legacy:')) >= 0) { + let legacy = output.substr(index + 'Legacy:'.length).split(os.EOL)[0]; + osSuffix.push(legacy); + } + + if (osSuffix.length == 0) { + throw 'Could not detect platform'; + } + + return osSuffix; + } + + private async downloadAndInstall(downloadUrls: string[]) { + let downloaded = false; + let downloadPath = ''; + for (const url of downloadUrls) { + try { + downloadPath = await tc.downloadTool(url); + downloaded = true; + break; + } catch (error) { + console.log('Could Not Download', url, JSON.stringify(error)); + } + } + + if (!downloaded) { + throw 'Failed to download package'; + } + + // extract + console.log('Extracting Package', downloadPath); + let extPath: string = IS_WINDOWS + ? await tc.extractZip(downloadPath) + : await tc.extractTar(downloadPath); + + // cache tool + console.log('Caching tool'); + let cachedDir = await tc.cacheDir( + extPath, + this.cachedToolName, + this.version, + this.arch + ); + console.log('Successfully installed', this.version); + return cachedDir; + } + + // OsSuffixes - The suffix which is a part of the file name ex- linux-x64, windows-x86 + // Type - SDK / Runtime + // Version - Version of the SDK/Runtime + private async getDownloadUrls( + osSuffixes: string[], + version: string + ): Promise { + let downloadUrls = []; + let releasesJSON = await this.getReleasesJson(); + core.debug('Releases: ' + releasesJSON); + + let releasesInfo = JSON.parse(await releasesJSON.readBody()); + releasesInfo = releasesInfo.filter((releaseInfo: any) => { + return ( + releaseInfo['version-sdk'] === version || + releaseInfo['version-sdk-display'] === version + ); + }); + + if (releasesInfo.length != 0) { + let release = releasesInfo[0]; + let blobUrl: string = release['blob-sdk']; + let dlcUrl: string = release['dlc--sdk']; + let fileName: string = release['sdk-' + osSuffixes[0]] + ? release['sdk-' + osSuffixes[0]] + : release['sdk-' + osSuffixes[1]]; + + if (!!fileName) { + fileName = fileName.trim(); + // For some latest version, the filename itself can be full download url. + // Do a very basic check for url(instead of regex) as the url is only for downloading and + // is coming from .net core releases json and not some ransom user input + if (fileName.toLowerCase().startsWith('https://')) { + downloadUrls.push(fileName); + } else { + if (!!blobUrl) { + downloadUrls.push(util.format('%s%s', blobUrl.trim(), fileName)); + } + + if (!!dlcUrl) { + downloadUrls.push(util.format('%s%s', dlcUrl.trim(), fileName)); + } + } + } else { + throw `The specified version's download links are not correctly formed in the supported versions document => ${DotNetCoreReleasesUrl}`; + } + } else { + console.log( + `Could not fetch download information for version ${version}` + ); + downloadUrls = await this.getFallbackDownloadUrls(version); + } + + if (downloadUrls.length == 0) { + throw `Could not construct download URL. Please ensure that specified version ${version} is valid.`; + } + + return downloadUrls; + } + + private getReleasesJson(): Promise { + var httpCallbackClient = new httpClient.HttpClient('setup-dotnet', [], {}); + return httpCallbackClient.get(DotNetCoreReleasesUrl); + } + + private async getFallbackDownloadUrls(version: string): Promise { + let primaryUrlSearchString: string; + let legacyUrlSearchString: string; + let output = ''; + let resultCode = 0; + + if (IS_WINDOWS) { + let escapedScript = path + .join(__dirname, '..', 'externals', 'install-dotnet.ps1') + .replace(/'/g, "''"); + let command = `& '${escapedScript}' -Version ${version} -DryRun`; + + const powershellPath = await io.which('powershell', true); + resultCode = await exec.exec( + `"${powershellPath}"`, + [ + '-NoLogo', + '-Sta', + '-NoProfile', + '-NonInteractive', + '-ExecutionPolicy', + 'Unrestricted', + '-Command', + command + ], + { + listeners: { + stdout: (data: Buffer) => { + output += data.toString(); + } + } + } + ); + + primaryUrlSearchString = 'dotnet-install: Primary - '; + legacyUrlSearchString = 'dotnet-install: Legacy - '; + } else { + let escapedScript = path + .join(__dirname, '..', 'externals', 'install-dotnet.sh') + .replace(/'/g, "''"); + chmodSync(escapedScript, '777'); + + const scriptPath = await io.which(escapedScript, true); + resultCode = await exec.exec( + `"${scriptPath}"`, + ['--version', version, '--dry-run'], + { + listeners: { + stdout: (data: Buffer) => { + output += data.toString(); + } + } + } + ); + + primaryUrlSearchString = 'dotnet-install: Payload URL: '; + legacyUrlSearchString = 'dotnet-install: Legacy payload URL: '; + } + + if (resultCode != 0) { + throw `Failed to get download urls with result code ${resultCode}. ${output}`; + } + + let primaryUrl: string = ''; + let legacyUrl: string = ''; + if (!!output && output.length > 0) { + let lines: string[] = output.split(os.EOL); + if (!!lines && lines.length > 0) { + lines.forEach((line: string) => { + if (!line) { + return; + } + var primarySearchStringIndex = line.indexOf(primaryUrlSearchString); + if (primarySearchStringIndex > -1) { + primaryUrl = line.substring( + primarySearchStringIndex + primaryUrlSearchString.length + ); + return; + } + + var legacySearchStringIndex = line.indexOf(legacyUrlSearchString); + if (legacySearchStringIndex > -1) { + legacyUrl = line.substring( + legacySearchStringIndex + legacyUrlSearchString.length + ); + return; + } + }); + } + } + + return [primaryUrl, legacyUrl]; + } + + private version: string; + private cachedToolName: string; + private arch: string; +} + +const DotNetCoreReleasesUrl: string = + 'https://raw.githubusercontent.com/dotnet/core/master/release-notes/releases.json'; diff --git a/src/setup-dotnet.ts b/src/setup-dotnet.ts new file mode 100644 index 0000000..dc5e2fd --- /dev/null +++ b/src/setup-dotnet.ts @@ -0,0 +1,23 @@ +import * as core from '@actions/core'; +import * as installer from './installer'; + +async function run() { + try { + // + // Version is optional. If supplied, install / use from the tool cache + // If not supplied then task is still used to setup proxy, auth, etc... + // + const version = core.getInput('version'); + if (version) { + const dotnetInstaller = new installer.DotnetCoreInstaller(version); + await dotnetInstaller.installDotnet(); + } + + // TODO: setup proxy from runner proxy config + // TODO: problem matchers registered + } catch (error) { + core.setFailed(error.message); + } +} + +run();