Install-Dotnet.ps1
|
<#PSScriptInfo .VERSION 1.0.0 .GUID dedf41f1-31cc-4f4b-9751-1c221f4237d7 .AUTHOR Alain Herve .COMPANYNAME chadnpc .COPYRIGHT MIT .TAGS sdk, dotnet, installer-script .LICENSEURI https://github.com/chadnpc/dotnet-install-script/LICENSE .PROJECTURI https://github.com/chadnpc/dotnet-install-script .ICONURI .EXTERNALMODULEDEPENDENCIES clihelper.logger, clihelper.env .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES .PRIVATEDATA #> <# .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. Note that the intended use of this script is for Continuous Integration (CI) scenarios, where: - The SDK needs to be installed without user interaction and without admin rights. - The SDK installation doesn't need to persist across multiple CI runs. To set up a development environment or to run apps, use installers rather than this script. Visit https://dotnet.microsoft.com/download to get the installer. .PARAMETER Channel Default: LTS Download from the Channel specified. Possible values: - STS - the most recent Standard Term Support release - LTS - the most recent Long Term Support release - 2-part version in a format A.B - represents a specific release examples: 2.0, 1.0 - 3-part version in a format A.B.Cxx - represents a specific SDK release examples: 5.0.1xx, 5.0.2xx Supported since 5.0 release Warning: Value "Current" is deprecated for the Channel parameter. Use "STS" instead. Note: The version parameter overrides the channel parameter when any version other than 'latest' is used. .PARAMETER Quality Download the latest build of specified quality in the channel. The possible values are: daily, preview, GA. Works only in combination with channel. Not applicable for STS and LTS channels and will be ignored if those channels are used. Supported since 5.0 release. Note: The version parameter overrides the channel parameter when any version other than 'latest' is used, and therefore overrides the quality. .PARAMETER Version Default: latest Represents a build version on specific channel. Possible values: - latest - the latest build on specific channel - 3-part version in a format A.B.C - represents specific version of build examples: 2.0.0-preview2-006120, 1.1.0 .PARAMETER Internal Download internal builds. Requires providing credentials via -FeedCredential parameter. .PARAMETER FeedCredential Token to access Azure feed. Used as a query string to append to the Azure feed. This parameter typically is not specified. .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: <auto> - this value represents currently running OS architecture Architecture of dotnet binaries to be installed. Possible values are: <auto>, 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'. Installs just the shared runtime bits, not the entire SDK. .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 - windowsdesktop - the Microsoft.WindowsDesktop.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 deterministically 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://builds.dotnet.microsoft.com/dotnet For internal use only. Allows using a different storage to download SDK archives from. .PARAMETER UncachedFeed For internal use only. Allows using a different storage to download SDK archives from. .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 ProxyBypassList If set with ProxyAddress, will provide the list of comma separated urls that will bypass the proxy .PARAMETER SkipNonVersionedFiles Default: false Skips installing non-versioned files if they already exist, such as dotnet.exe. .PARAMETER JSonFile Determines the SDK version from a user specified global.json file Note: global.json must have a value for 'SDK:Version' .PARAMETER DownloadTimeout Determines timeout duration in seconds for downloading of the SDK file Default: 1200 seconds (20 minutes) .PARAMETER KeepZip If set, downloaded file is kept .PARAMETER ZipPath Use that path to store installer, generated by default .EXAMPLE Install-Dotnet.ps1 -Version 7.0.401 Installs the .NET SDK version 7.0.401 .EXAMPLE Install-Dotnet.ps1 -Channel 8.0 -Quality GA Installs the latest GA (general availability) version of the .NET 8.0 SDK #> [CmdletBinding()] param ( [string]$Channel = "LTS", [string]$Quality, [string]$Version = "Latest", [switch]$Internal, [string]$JSonFile, [Alias('i')][string]$InstallDir = "<auto>", [string]$Architecture = "<auto>", [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, [string]$UncachedFeed, [string]$FeedCredential, [string]$ProxyAddress, [switch]$ProxyUseDefaultCredentials, [string[]]$ProxyBypassList = @(), [switch]$SkipNonVersionedFiles, [int]$DownloadTimeout = 1200, [switch]$KeepZip, [string]$ZipPath = [System.IO.Path]::combine([System.IO.Path]::GetTempPath(), [System.IO.Path]::GetRandomFileName()), [switch]$Help ) begin { Set-StrictMode -Version Latest $ogeap = $ErrorActionPreference $ogpap = $ProgressPreference $ErrorActionPreference = "Stop" $ProgressPreference = "SilentlyContinue" #Requires -Modules cliHelper.logger # Requires -RunAsAdministrator was removed because it blocked non-admin execution. if ($Help) { Get-Help $PSCommandPath -Examples exit } $logger = [Logger]::new() if ($VerbosePreference -ne 'SilentlyContinue') { $logger.MinLevel = [LogLevel]::Debug } else { $logger.MinLevel = [LogLevel]::Info } $logger.AddLogAppender([ConsoleAppender]@{ }) $logger.set_default() enum ArchitectureType { Auto Amd64 X64 X86 Arm64 Arm } enum RuntimeProduct { DotNet AspNetCore WindowsDesktop Sdk } class DotnetInstallException : System.Exception { DotnetInstallException([string]$message) : base($message) {} DotnetInstallException([string]$message, [System.Exception]$innerException) : base($message, $innerException) {} } class DownloadException : DotnetInstallException { [int]$StatusCode [string]$ErrorMessage DownloadException([string]$message, [int]$statusCode, [string]$errorMsg) : base($message) { $this.StatusCode = $statusCode $this.ErrorMessage = $errorMsg } } class PerfHelper { static [void] MeasureAction([string]$name, [scriptblock]$block) { $time = Measure-Command $block Write-LogEntry -Level Debug -Message ("Action '$name' took $($time.TotalSeconds) seconds") } } class InstallContext { [string]$Channel [string]$Quality [string]$Version [bool]$Internal [string]$JSonFile [string]$InstallDir [string]$Architecture [string]$Runtime [bool]$SharedRuntime [bool]$DryRun [bool]$NoPath [string]$AzureFeed [string]$UncachedFeed [string]$FeedCredential [string]$ProxyAddress [bool]$ProxyUseDefaultCredentials [string[]]$ProxyBypassList [bool]$SkipNonVersionedFiles [int]$DownloadTimeout [bool]$KeepZip [string]$ZipPath [System.Management.Automation.InvocationInfo]$Invocation # Computed Values [string]$CLIArchitecture [string]$NormalizedQuality [string]$NormalizedChannel [string]$NormalizedProduct [string]$InstallRoot [string]$AssetName [string]$DotnetPackageRelativePath [string]$ScriptName = "Install-Dotnet.ps1" } class DownloadLinkInfo { [string]$DownloadLink [string]$SpecificVersion [string]$EffectiveVersion [string]$Type } class SystemUtils { static [void] LoadAssembly([string]$Assembly) { try { Add-Type -AssemblyName $Assembly | Out-Null } catch { $m = $_ | Format-List * -Force | Out-String Write-Host $m -f Red } } static [string] GetMachineArchitecture() { if ($null -ne $ENV:PROCESSOR_ARCHITEW6432) { return $ENV:PROCESSOR_ARCHITEW6432 } try { if (((Get-CimInstance -ClassName CIM_OperatingSystem).OSArchitecture) -like "ARM*") { if ([Environment]::Is64BitOperatingSystem) { return "arm64" } return "arm" } } catch { $m = $_ | Format-List * -Force | Out-String Write-Host $m -f Red } return $ENV:PROCESSOR_ARCHITECTURE } static [string] GetCLIArchitecture([string]$Architecture) { if ($Architecture -eq "<auto>") { $Architecture = [SystemUtils]::GetMachineArchitecture() } $a = switch ($Architecture.ToLowerInvariant()) { { $_ -in "amd64", "x64" } { "x64"; break } { $_ -eq "x86" } { "x86"; break } { $_ -eq "arm" } { "arm"; break } { $_ -eq "arm64" } { "arm64"; break } default { throw [DotnetInstallException]::new("Architecture '$Architecture' not supported.") } } return $a } static [string] GetAbsolutePath([string]$Path) { if ([System.IO.Path]::IsPathRooted($Path)) { return $Path } return [System.IO.Path]::GetFullPath([System.IO.Path]::Combine((Get-Location).ProviderPath, $Path)) } static [void] PrependToPath([string]$InstallRoot, [bool]$NoPath) { $BinPath = [SystemUtils]::GetAbsolutePath([System.IO.Path]::Combine($InstallRoot, "")) if (!$NoPath) { $SuffixedBinPath = "$BinPath;" if (!$env:path.Contains($SuffixedBinPath)) { Write-LogEntry -Level Info -Message ("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 { Write-LogEntry -Level Info -Message ("Current process PATH already contains `"$BinPath`"") } } else { Write-LogEntry -Level Info -Message ("Binaries of dotnet can be found in $BinPath") } } } class HttpUtils { static [object] InvokeWithRetry([scriptblock]$ScriptBlock, [System.Threading.CancellationToken]$cancellationToken, [int]$MaxAttempts, [int]$SecondsBetweenAttempts, [int]$DownloadTimeout) { $Attempts = 0; $result = $null $startTime = Get-Date do { try { $result = & $ScriptBlock } catch { $Attempts++ if (($Attempts -lt $MaxAttempts) -and !$cancellationToken.IsCancellationRequested) { Start-Sleep -Seconds $SecondsBetweenAttempts } else { $elapsedTime = (Get-Date) - $startTime if (($elapsedTime.TotalSeconds - $DownloadTimeout) -gt 0 -and !$cancellationToken.IsCancellationRequested) { throw [System.TimeoutException]::new("Failed to reach the server: connection timeout: default timeout is $DownloadTimeout second(s)", $_.Exception) } throw $_.Exception } } } while (($Attempts -lt $MaxAttempts) -and !$cancellationToken.IsCancellationRequested) return $result } static [object] GetHttpResponse([Uri]$Uri, [bool]$HeaderOnly, [bool]$DisableRedirect, [bool]$DisableFeedCredential, [InstallContext]$Ctx) { $cts = [System.Threading.CancellationTokenSource]::new() $downloadScript = { $HttpClient = $null try { [SystemUtils]::LoadAssembly("System.Net.Http") $ProxyAddress = $Ctx.ProxyAddress $ProxyUseDefaultCredentials = $Ctx.ProxyUseDefaultCredentials if (!$ProxyAddress) { try { $DefaultProxy = [System.Net.WebRequest]::DefaultWebProxy; if ($DefaultProxy -and (!$DefaultProxy.IsBypassed($Uri))) { if ($null -ne $DefaultProxy.GetProxy($Uri)) { $ProxyAddress = $DefaultProxy.GetProxy($Uri).OriginalString } else { $ProxyAddress = $null } $ProxyUseDefaultCredentials = $true } } catch { $ProxyAddress = $null Write-LogEntry -Level Debug -Message ("Exception ignored: $($_.Exception.Message) - moving forward...") } } $HttpClientHandler = [System.Net.Http.HttpClientHandler]::new() if ($ProxyAddress) { $HttpClientHandler.Proxy = [System.Net.WebProxy]::new() $HttpClientHandler.Proxy.Address = $ProxyAddress $HttpClientHandler.Proxy.UseDefaultCredentials = $ProxyUseDefaultCredentials $HttpClientHandler.Proxy.BypassList = $Ctx.ProxyBypassList } if ($DisableRedirect) { $HttpClientHandler.AllowAutoRedirect = $false } $HttpClient = [System.Net.Http.HttpClient]::new($HttpClientHandler) $HttpClient.Timeout = [TimeSpan]::FromSeconds($Ctx.DownloadTimeout) $completionOption = $HeaderOnly ? [System.Net.Http.HttpCompletionOption]::ResponseHeadersRead : [System.Net.Http.HttpCompletionOption]::ResponseContentRead $UriWithCredential = $DisableFeedCredential ? $Uri.ToString() : "$Uri$($Ctx.FeedCredential)" $Task = $HttpClient.GetAsync($UriWithCredential, $completionOption).ConfigureAwait($false) $Response = $Task.GetAwaiter().GetResult() if (($null -eq $Response) -or ((!$HeaderOnly) -and (!($Response.IsSuccessStatusCode)))) { $StatusCode = if ($null -ne $Response) { [int]$Response.StatusCode } else { 0 } $ErrMsg = "Unable to download $Uri. Returned HTTP status code: $StatusCode" if ($StatusCode -eq 404) { $cts.Cancel() } throw [DownloadException]::new("Unable to download $Uri.", $StatusCode, $ErrMsg) } return $Response } catch [System.Net.Http.HttpRequestException] { $CurrentException = $_.Exception $ErrorMsg = "$($CurrentException.Message)`r`n" while ($CurrentException.InnerException) { $CurrentException = $CurrentException.InnerException $ErrorMsg += "$($CurrentException.Message)`r`n" } if ($ErrorMsg -match "SSL/TLS") { $ErrorMsg += "Ensure that TLS 1.2 or higher is enabled.`r`n" } throw [DownloadException]::new("Unable to download $Uri.", 0, $ErrorMsg) } finally { if ($null -ne $HttpClient) { $HttpClient.Dispose() } } } try { return [HttpUtils]::InvokeWithRetry($downloadScript, $cts.Token, 3, 1, $Ctx.DownloadTimeout) } finally { if ($null -ne $cts) { $cts.Dispose() } } } static [string] GetRemoteFileSize([string]$zipUri, [InstallContext]$Ctx) { try { $response = Invoke-WebRequest -UseBasicParsing -Uri $zipUri -Method Head $fileSize = $response.Headers["Content-Length"] if (![string]::IsNullOrEmpty($fileSize)) { Write-LogEntry -Level Info -Message ("Remote file $zipUri size is $fileSize bytes.") return $fileSize } } catch { Write-LogEntry -Level Warn -Message ("Content-Length header was not extracted for $zipUri.") } return $null } static [void] ValidateRemoteLocalFileSizes([string]$LocalFileOutPath, [string]$SourceUri, [InstallContext]$Ctx) { try { $remoteFileSize = [HttpUtils]::GetRemoteFileSize($SourceUri, $Ctx) $fileSize = [long](Get-Item $LocalFileOutPath).Length Write-LogEntry -Level Info -Message ("Downloaded file $SourceUri size is $fileSize bytes.") if (![string]::IsNullOrEmpty($remoteFileSize) -and $fileSize -gt 0) { if ($remoteFileSize -ne $fileSize) { Write-LogEntry -Level Info -Message ("The remote and local file sizes are not equal. Remote file size is $remoteFileSize bytes and local size is $fileSize bytes. The local package may be corrupted.") } else { Write-LogEntry -Level Info -Message ("The remote and local file sizes are equal.") } } else { Write-LogEntry -Level Info -Message ("Either downloaded or local package size can not be measured. One of them may be corrupted.") } } catch { Write-LogEntry -Level Info -Message ("Either downloaded or local package size can not be measured. One of them may be corrupted.") } } static [void] DownloadFile([string]$Source, [string]$OutPath, [InstallContext]$Ctx) { if ($Source -notlike "http*") { $absSource = [SystemUtils]::GetAbsolutePath($Source) Write-LogEntry -Level Info -Message ("Copying file from $absSource to $OutPath") Copy-Item $absSource $OutPath return } $Stream = $null try { $Response = [HttpUtils]::GetHttpResponse($Source, $false, $false, $false, $Ctx) $Stream = $Response.Content.ReadAsStreamAsync().Result $File = [System.IO.File]::Create($OutPath) $Stream.CopyTo($File) $File.Close() [HttpUtils]::ValidateRemoteLocalFileSizes($OutPath, $Source, $Ctx) } finally { if ($null -ne $Stream) { $Stream.Dispose() } } } } class FileSystemUtils { static [string] GetUserSharePath() { $InstallRoot = $env:DOTNET_INSTALL_DIR if (!$InstallRoot) { $InstallRoot = "$env:LocalAppData\Microsoft\dotnet" } elseif ($InstallRoot -like "$env:ProgramFiles\dotnet\?*") { Write-LogEntry -Level Warn -Message ("The install root specified by DOTNET_INSTALL_DIR points to a sub folder of $env:ProgramFiles\dotnet. It is better to keep aligned with .NET SDK installer.") } return $InstallRoot } static [bool] TestUserWriteAccess([string]$InstallDir) { try { $tempFileName = [guid]::NewGuid().ToString() $tempFilePath = Join-Path -Path $InstallDir -ChildPath $tempFileName New-Item -Path $tempFilePath -ItemType File -Force | Out-Null Remove-Item $tempFilePath -Force return $true } catch { return $false } } static [void] PrepareInstallDirectory([string]$InstallRoot) { $diskSpaceWarning = "Failed to check the disk space. Installation will continue, but it may fail if you do not have enough disk space." if ($(Get-Variable PSVersionTable).Value.PSVersion.Major -lt 7) { Write-LogEntry -Level Warn -Message ($diskSpaceWarning) return } New-Item -ItemType Directory -Force -Path $InstallRoot | Out-Null $installDrive = $((Get-Item $InstallRoot -Force).PSDrive.Name) $diskInfo = $null try { $diskInfo = Get-PSDrive -Name $installDrive } catch { Write-LogEntry -Level Warn -Message ($diskSpaceWarning) } if (($null -ne $diskInfo) -and ($diskInfo.Free / 1MB -le 100)) { throw [DotnetInstallException]::new("There is not enough disk space on drive ${installDrive}:") } } static [bool] IsDotnetPackageInstalled([string]$InstallRoot, [string]$RelativePath, [string]$SpecificVersion) { $DotnetPackagePath = Join-Path -Path (Join-Path -Path $InstallRoot -ChildPath $RelativePath) -ChildPath $SpecificVersion Write-LogEntry -Level Debug -Message ("Is-Dotnet-Package-Installed: DotnetPackagePath=$DotnetPackagePath") return (Test-Path $DotnetPackagePath -PathType Container) } static [string] GetPathPrefixWithVersion([string]$Path) { $match = [regex]::Match($Path, "/\d+\.\d+[^/]+/") if ($match.Success) { return $Path.Substring(0, $match.Index + $match.Length) } return $null } static [string[]] GetDirectoriesToUnpack([object]$Zip, [string]$OutPath) { $ret = @() foreach ($entry in $Zip.Entries) { $dir = [FileSystemUtils]::GetPathPrefixWithVersion($entry.FullName) if ($null -ne $dir) { $path = [SystemUtils]::GetAbsolutePath((Join-Path -Path $OutPath -ChildPath $dir)) if (!(Test-Path $path -PathType Container)) { $ret += $dir } } } $ret = $ret | Sort-Object | Get-Unique Write-LogEntry -Level Debug -Message ("Directories to unpack: $(($ret | ForEach-Object { "$_" }) -join ';')") return $ret } static [void] ExtractDotnetPackage([string]$ZipPath, [string]$OutPath, [bool]$OverrideNonVersionedFiles) { [SystemUtils]::LoadAssembly("System.IO.Compression.FileSystem") $Zip = $null try { $Zip = [System.IO.Compression.ZipFile]::OpenRead($ZipPath) $DirectoriesToUnpack = [FileSystemUtils]::GetDirectoriesToUnpack($Zip, $OutPath) foreach ($entry in $Zip.Entries) { $PathWithVersion = [FileSystemUtils]::GetPathPrefixWithVersion($entry.FullName) if (($null -eq $PathWithVersion) -or ($DirectoriesToUnpack -contains $PathWithVersion)) { $DestinationPath = [SystemUtils]::GetAbsolutePath((Join-Path -Path $OutPath -ChildPath $entry.FullName)) $DestinationDir = Split-Path -Parent $DestinationPath $OverrideFiles = $OverrideNonVersionedFiles -or (!(Test-Path $DestinationPath)) if ((!$DestinationPath.EndsWith("\")) -and $OverrideFiles) { New-Item -ItemType Directory -Force -Path $DestinationDir | Out-Null [System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $DestinationPath, $OverrideNonVersionedFiles) } } } } catch { throw [DotnetInstallException]::new("Failed to extract package.", $_.Exception) } finally { if ($null -ne $Zip) { $Zip.Dispose() } } } static [void] SafeRemoveFile([string]$Path) { try { if (Test-Path $Path) { Remove-Item $Path -Force Write-LogEntry -Level Debug -Message ("The temporary file `"$Path`" was removed.") } } catch { Write-LogEntry -Level Warn -Message ("Failed to remove the temporary file: `"$Path`", remove it manually.") } } } class VersionResolver { static [string] ParseJsonFile([string]$JSonFile) { if (!(Test-Path $JSonFile)) { throw [DotnetInstallException]::new("Unable to find '$JSonFile'") } try { $JSonContent = Get-Content($JSonFile) -Raw | ConvertFrom-Json | Select-Object -expand "sdk" -ErrorAction SilentlyContinue } catch { Write-LogEntry -Level Error -Message ("Json file unreadable: '$JSonFile'") throw } $Version = $null if ($JSonContent) { try { foreach ($prop in $JSonContent.PSObject.Properties) { if ($prop.Name -eq "version") { $Version = $prop.Value Write-LogEntry -Level Debug -Message ("Version = $Version") } } } catch { throw [DotnetInstallException]::new("Unable to parse the SDK node in '$JSonFile'") } } else { throw [DotnetInstallException]::new("Unable to find the SDK node in '$JSonFile'") } if ($null -eq $Version) { throw [DotnetInstallException]::new("Unable to find the SDK:version node in '$JSonFile'") } return $Version } static [hashtable] GetFromLatestVersionFile([string]$AzureFeed, [string]$Channel, [string]$Runtime, [InstallContext]$Ctx) { $VersionFileUrl = $null if ($Runtime -eq "dotnet") { $VersionFileUrl = "$AzureFeed/Runtime/$Channel/latest.version" } elseif ($Runtime -eq "aspnetcore") { $VersionFileUrl = "$AzureFeed/aspnetcore/Runtime/$Channel/latest.version" } elseif ($Runtime -eq "windowsdesktop") { $VersionFileUrl = "$AzureFeed/WindowsDesktop/$Channel/latest.version" } elseif (!$Runtime) { $VersionFileUrl = "$AzureFeed/Sdk/$Channel/latest.version" } Write-LogEntry -Level Debug -Message ("Constructed latest.version URL: $VersionFileUrl") $Response = [HttpUtils]::GetHttpResponse($VersionFileUrl, $false, $false, $false, $Ctx) $VersionText = $Response.Content.ReadAsStringAsync().Result $Data = -split $VersionText return @{ CommitHash = $(if ($Data.Count -gt 1) { $Data[0] }) Version = $Data[-1] } } static [string] GetProductVersion([string]$AzureFeed, [string]$SpecificVersion, [string]$DownloadLink, [string]$Runtime, [InstallContext]$Ctx) { $urls = @( [VersionResolver]::GetProductVersionUrl($AzureFeed, $SpecificVersion, $DownloadLink, $true, $Runtime), [VersionResolver]::GetProductVersionUrl($AzureFeed, $SpecificVersion, $DownloadLink, $false, $Runtime) ) foreach ($url in $urls) { Write-LogEntry -Level Debug -Message ("Checking for the existence of $url") try { $response = [HttpUtils]::GetHttpResponse($url, $false, $false, $false, $Ctx) if ($response.StatusCode -eq 200) { $pv = $response.Content.ReadAsStringAsync().Result.Trim() if ($pv -ne $SpecificVersion) { Write-LogEntry -Level Info -Message ("Using alternate version $pv found in $url") } return $pv } } catch { Write-LogEntry -Level Debug -Message ("Could not read productVersion.txt at $url") } } if ([string]::IsNullOrEmpty($DownloadLink)) { return $SpecificVersion } $filename = $DownloadLink.Substring($DownloadLink.LastIndexOf("/") + 1) $filenameParts = $filename.Split('-') if ($filenameParts.Length -gt 2) { $pv = $filenameParts[2] Write-LogEntry -Level Debug -Message ("Extracted product version '$pv' from download link '$DownloadLink'.") return $pv } return $SpecificVersion } static [string] GetProductVersionUrl([string]$AzureFeed, [string]$SpecificVersion, [string]$DownloadLink, [bool]$Flattened, [string]$Runtime) { $majorVersion = $null if ($SpecificVersion -match '^(\d+)\.(.*)') { $majorVersion = $Matches[1] -as [int] } $pvFileName = 'productVersion.txt' if ($Flattened) { if (!$Runtime) { $pvFileName = 'sdk-productVersion.txt' } elseif ($Runtime -eq "dotnet") { $pvFileName = 'runtime-productVersion.txt' } else { $pvFileName = "$Runtime-productVersion.txt" } } if ([string]::IsNullOrEmpty($DownloadLink)) { if ($Runtime -eq "dotnet") { return "$AzureFeed/Runtime/$SpecificVersion/$pvFileName" } elseif ($Runtime -eq "aspnetcore") { return "$AzureFeed/aspnetcore/Runtime/$SpecificVersion/$pvFileName" } elseif ($Runtime -eq "windowsdesktop") { if ($null -ne $majorVersion -and $majorVersion -ge 5) { return "$AzureFeed/WindowsDesktop/$SpecificVersion/$pvFileName" } return "$AzureFeed/Runtime/$SpecificVersion/$pvFileName" } elseif (!$Runtime) { return "$AzureFeed/Sdk/$SpecificVersion/$pvFileName" } } return $DownloadLink.Substring(0, $DownloadLink.LastIndexOf("/")) + "/$pvFileName" } } class UrlResolver { static [string] GetAkaMSDownloadLink([string]$Channel, [string]$Quality, [bool]$Internal, [string]$Product, [string]$Architecture, [InstallContext]$Ctx) { if (![string]::IsNullOrEmpty($Quality) -and ($Channel -in "LTS", "STS")) { $Quality = "" Write-LogEntry -Level Warn -Message ("Specifying quality for STS or LTS channel is not supported, the quality will be ignored.") } $akaMsLink = "https://aka.ms/dotnet" if ($Internal) { $akaMsLink += "/internal" } $akaMsLink += "/$Channel" if (![string]::IsNullOrEmpty($Quality)) { $akaMsLink += "/$Quality" } $akaMsLink += "/$Product-win-$Architecture.zip" Write-LogEntry -Level Debug -Message ("Constructed aka.ms link: '$akaMsLink'.") $downloadLink = $null for ($maxRedirections = 9; $maxRedirections -ge 0; $maxRedirections--) { $Response = [HttpUtils]::GetHttpResponse($akaMsLink, $true, $true, $true, $Ctx) if ([string]::IsNullOrEmpty($Response)) { return $null } if ($Response.StatusCode -eq 301) { try { $downloadLink = $Response.Headers.GetValues("Location")[0] if ([string]::IsNullOrEmpty($downloadLink)) { return $null } $akaMsLink = $downloadLink continue } catch { return $null } } elseif ((($Response.StatusCode -lt 300) -or ($Response.StatusCode -ge 400)) -and (![string]::IsNullOrEmpty($downloadLink))) { return $downloadLink } return $null } return $null } static [DownloadLinkInfo] GetAkaMsLinkAndVersion([InstallContext]$Ctx) { $link = [UrlResolver]::GetAkaMSDownloadLink($Ctx.NormalizedChannel, $Ctx.NormalizedQuality, $Ctx.Internal, $Ctx.NormalizedProduct, $Ctx.CLIArchitecture, $Ctx) if ([string]::IsNullOrEmpty($link)) { if (![string]::IsNullOrEmpty($Ctx.NormalizedQuality)) { Write-LogEntry -Level Error -Message ("Failed to locate the latest version in channel '$($Ctx.NormalizedChannel)' with '$($Ctx.NormalizedQuality)' quality.") throw [DotnetInstallException]::new("aka.ms link resolution failure") } return $null } $pathParts = $link.Split('/') if ($pathParts.Length -ge 2) { $SpecificVersion = $pathParts[$pathParts.Length - 2] } else { Write-LogEntry -Level Error -Message ("Failed to extract the version from download link '$link'.") return $null } $EffectiveVersion = [VersionResolver]::GetProductVersion($null, $SpecificVersion, $link, $Ctx.Runtime, $Ctx) $info = [DownloadLinkInfo]::new() $info.DownloadLink = $link $info.SpecificVersion = $SpecificVersion $info.EffectiveVersion = $EffectiveVersion $info.Type = "aka.ms" return $info } static [string[]] GetFeedsToUse([InstallContext]$Ctx) { $feeds = @("https://builds.dotnet.microsoft.com/dotnet", "https://ci.dot.net/public") if (![string]::IsNullOrEmpty($Ctx.AzureFeed)) { $feeds = @($Ctx.AzureFeed) } if (![string]::IsNullOrEmpty($Ctx.UncachedFeed)) { $feeds = @($Ctx.UncachedFeed) } return $feeds } } class DotnetInstall { static [void] ValidateFeedCredential([InstallContext]$Ctx) { if ($Ctx.Internal -and [string]::IsNullOrWhitespace($Ctx.FeedCredential)) { $msg = "Provide credentials via -FeedCredential parameter." if ($Ctx.DryRun) { Write-LogEntry -Level Warn -Message ($msg) } else { throw [DotnetInstallException]::new($msg) } } if (![string]::IsNullOrWhitespace($Ctx.FeedCredential) -and $Ctx.FeedCredential[0] -ne '?') { $Ctx.FeedCredential = "?" + $Ctx.FeedCredential } } static [void] NormalizeParameters([InstallContext]$Ctx) { $Ctx.CLIArchitecture = [SystemUtils]::GetCLIArchitecture($Ctx.Architecture) # Quality if ([string]::IsNullOrEmpty($Ctx.Quality)) { $Ctx.NormalizedQuality = "" } else { switch ($Ctx.Quality.ToLowerInvariant()) { { $_ -in "daily", "preview" } { $Ctx.NormalizedQuality = $_ } { $_ -eq "ga" } { $Ctx.NormalizedQuality = "" } default { throw [DotnetInstallException]::new("'$($Ctx.Quality)' is not a supported value for -Quality option.") } } } # Channel if ([string]::IsNullOrEmpty($Ctx.Channel)) { $Ctx.NormalizedChannel = "" } else { if ($Ctx.Channel.Contains("Current")) { Write-LogEntry -Level Warn -Message ('Value "Current" is deprecated. Use "STS" instead.') } switch ($Ctx.Channel.ToLowerInvariant()) { { $_ -eq "lts" } { $Ctx.NormalizedChannel = "LTS" } { $_ -in "sts", "current" } { $Ctx.NormalizedChannel = "STS" } default { $Ctx.NormalizedChannel = $Ctx.Channel.ToLowerInvariant() } } } # Product & Runtime switch ($Ctx.Runtime) { { $_ -eq "dotnet" } { $Ctx.NormalizedProduct = "dotnet-runtime" $Ctx.AssetName = ".NET Core Runtime" $Ctx.DotnetPackageRelativePath = "shared\Microsoft.NETCore.App" } { $_ -eq "aspnetcore" } { $Ctx.NormalizedProduct = "aspnetcore-runtime" $Ctx.AssetName = "ASP.NET Core Runtime" $Ctx.DotnetPackageRelativePath = "shared\Microsoft.AspNetCore.App" } { $_ -eq "windowsdesktop" } { $Ctx.NormalizedProduct = "windowsdesktop-runtime" $Ctx.AssetName = ".NET Core Windows Desktop Runtime" $Ctx.DotnetPackageRelativePath = "shared\Microsoft.WindowsDesktop.App" } { [string]::IsNullOrEmpty($_) } { $Ctx.NormalizedProduct = "dotnet-sdk" $Ctx.AssetName = ".NET Core SDK" $Ctx.DotnetPackageRelativePath = "sdk" } default { throw [DotnetInstallException]::new("'$($Ctx.Runtime)' is not a supported value for -Runtime option.") } } [DotnetInstall]::ValidateFeedCredential($Ctx) } static [void] PrintDryRunOutput([InstallContext]$Ctx, [DownloadLinkInfo[]]$Links) { Write-LogEntry -Level Info -Message ("Payload URLs:") for ($i = 0; $i -lt $Links.Count; $i++) { Write-LogEntry -Level Info -Message ("URL #$i - $($Links[$i].Type): $($Links[$i].DownloadLink)") } $SpecificVersion = $Links[0].SpecificVersion $EffectiveVersion = $Links[0].EffectiveVersion $cmd = ".\$($Ctx.ScriptName) -Version `"$SpecificVersion`" -InstallDir `"$($Ctx.InstallRoot)`" -Architecture `"$($Ctx.CLIArchitecture)`"" if ($Ctx.Runtime -in "dotnet", "aspnetcore") { $cmd += " -Runtime `"$($Ctx.Runtime)`"" } foreach ($key in $Ctx.Invocation.BoundParameters.Keys) { if ($key -notin @("Architecture", "Channel", "DryRun", "InstallDir", "Runtime", "SharedRuntime", "Version", "Quality", "FeedCredential")) { $cmd += " -$key `"$($Ctx.Invocation.BoundParameters[$key])`"" } } if ($Ctx.Invocation.BoundParameters.ContainsKey("FeedCredential")) { $cmd += " -FeedCredential `"<feedCredential>`"" } Write-LogEntry -Level Info -Message ("Repeatable invocation: $cmd") if ($SpecificVersion -ne $EffectiveVersion) { Write-LogEntry -Level Info -Message ("NOTE: Due to finding a version manifest with this runtime, it would actually install with version '$EffectiveVersion'") } } static [void] Run([InstallContext]$Ctx) { Write-LogEntry -Level Info -Message ("Note that the intended use of this script is for Continuous Integration (CI) scenarios...") if ($Ctx.SharedRuntime -and (!$Ctx.Runtime)) { $Ctx.Runtime = "dotnet" } $OverrideNonVersionedFiles = !$Ctx.SkipNonVersionedFiles [PerfHelper]::MeasureAction("Product discovery", { [DotnetInstall]::NormalizeParameters($Ctx) }) $Ctx.InstallRoot = if ($Ctx.InstallDir -eq "<auto>") { [FileSystemUtils]::GetUserSharePath() } else { $Ctx.InstallDir } if (![FileSystemUtils]::TestUserWriteAccess($Ctx.InstallRoot)) { Write-LogEntry -Level Error -Message ("The current user doesn't have write access to the installation root '$($Ctx.InstallRoot)'") throw [DotnetInstallException]::new("Access Denied") } Write-LogEntry -Level Info -Message ("InstallRoot: $($Ctx.InstallRoot)") if ($Ctx.Version.ToLowerInvariant() -ne "latest" -and ![string]::IsNullOrEmpty($Ctx.Quality)) { throw [DotnetInstallException]::new("Quality and Version options are not allowed to be specified simultaneously.") } $DownloadLinks = @() # aka.ms strategy if ([string]::IsNullOrEmpty($Ctx.JSonFile) -and ($Ctx.Version -eq "latest")) { $akaLinkInfo = [UrlResolver]::GetAkaMsLinkAndVersion($Ctx) if ($null -ne $akaLinkInfo) { $DownloadLinks += $akaLinkInfo if (!$Ctx.DryRun -and [FileSystemUtils]::IsDotnetPackageInstalled($Ctx.InstallRoot, $Ctx.DotnetPackageRelativePath, $akaLinkInfo.EffectiveVersion)) { Write-LogEntry -Level Info -Message ("$($Ctx.AssetName) with version '$($akaLinkInfo.EffectiveVersion)' is already installed.") [SystemUtils]::PrependToPath($Ctx.InstallRoot, $Ctx.NoPath) return } } } # feed strategy if ([string]::IsNullOrEmpty($Ctx.NormalizedQuality) -and $DownloadLinks.Count -eq 0) { $feeds = [UrlResolver]::GetFeedsToUse($Ctx) foreach ($feed in $feeds) { try { $SpecificVersion = if (!$Ctx.JSonFile) { if ($Ctx.Version.ToLowerInvariant() -eq "latest") { ([VersionResolver]::GetFromLatestVersionFile($feed, $Ctx.Channel, $Ctx.Runtime, $Ctx)).Version } else { $Ctx.Version } } else { [VersionResolver]::ParseJsonFile($Ctx.JSonFile) } # Build primary download link $ProductVersion = [VersionResolver]::GetProductVersion($feed, $SpecificVersion, $null, $Ctx.Runtime, $Ctx) $Link = if ($Ctx.Runtime -eq "dotnet") { "$feed/Runtime/$SpecificVersion/dotnet-runtime-$ProductVersion-win-$($Ctx.CLIArchitecture).zip" } elseif ($Ctx.Runtime -eq "aspnetcore") { "$feed/aspnetcore/Runtime/$SpecificVersion/aspnetcore-runtime-$ProductVersion-win-$($Ctx.CLIArchitecture).zip" } elseif ($Ctx.Runtime -eq "windowsdesktop") { if ($SpecificVersion -match '^(\d+)\.(.*)$' -and [int]$Matches[1] -ge 5) { "$feed/WindowsDesktop/$SpecificVersion/windowsdesktop-runtime-$ProductVersion-win-$($Ctx.CLIArchitecture).zip" } else { "$feed/Runtime/$SpecificVersion/windowsdesktop-runtime-$ProductVersion-win-$($Ctx.CLIArchitecture).zip" } } else { "$feed/Sdk/$SpecificVersion/dotnet-sdk-$ProductVersion-win-$($Ctx.CLIArchitecture).zip" } $infoPrimary = [DownloadLinkInfo]::new() $infoPrimary.DownloadLink = $Link $infoPrimary.SpecificVersion = $SpecificVersion $infoPrimary.EffectiveVersion = $ProductVersion $infoPrimary.Type = "primary" $DownloadLinks += $infoPrimary # Build legacy download link $LegacyLink = if (!$Ctx.Runtime) { "$feed/Sdk/$SpecificVersion/dotnet-dev-win-$($Ctx.CLIArchitecture).$SpecificVersion.zip" } elseif ($Ctx.Runtime -eq "dotnet") { "$feed/Runtime/$SpecificVersion/dotnet-win-$($Ctx.CLIArchitecture).$SpecificVersion.zip" } else { $null } if (![string]::IsNullOrEmpty($LegacyLink)) { $infoLegacy = [DownloadLinkInfo]::new() $infoLegacy.DownloadLink = $LegacyLink $infoLegacy.SpecificVersion = $SpecificVersion $infoLegacy.EffectiveVersion = $ProductVersion $infoLegacy.Type = "legacy" $DownloadLinks += $infoLegacy } if (!$Ctx.DryRun -and [FileSystemUtils]::IsDotnetPackageInstalled($Ctx.InstallRoot, $Ctx.DotnetPackageRelativePath, $ProductVersion)) { Write-LogEntry -Level Info -Message ("$($Ctx.AssetName) with version '$ProductVersion' is already installed.") [SystemUtils]::PrependToPath($Ctx.InstallRoot, $Ctx.NoPath) return } } catch { Write-LogEntry -Level Debug -Message ("Failed to acquire download links from feed $feed.") } } } if ($DownloadLinks.Count -eq 0) { throw [DotnetInstallException]::new("Failed to resolve the exact version number.") } if ($Ctx.DryRun) { [DotnetInstall]::PrintDryRunOutput($Ctx, $DownloadLinks) return } [PerfHelper]::MeasureAction("Installation directory preparation", { [FileSystemUtils]::PrepareInstallDirectory($Ctx.InstallRoot) }) $DownloadSucceeded = $false $DownloadedLink = $null $ErrorMessages = @() foreach ($linkInfo in $DownloadLinks) { Write-LogEntry -Level Info -Message ("Downloading `"$($linkInfo.Type)`" link $($linkInfo.DownloadLink)") try { [PerfHelper]::MeasureAction("Package download", { [HttpUtils]::DownloadFile($linkInfo.DownloadLink, $Ctx.ZipPath, $Ctx) }) $DownloadSucceeded = $true $DownloadedLink = $linkInfo break } catch { $StatusCode = if ($_.Exception -is [DownloadException]) { $_.Exception.StatusCode } else { 0 } $ErrMsg = if ($_.Exception -is [DownloadException]) { $_.Exception.ErrorMessage } else { $_.Exception.Message } Write-LogEntry -Level Error -Message ("Download failed. Status: $StatusCode. Msg: $ErrMsg") $ErrorMessages += "Downloading from `"$($linkInfo.Type)`" link failed:`nUri: $($linkInfo.DownloadLink)`nStatusCode: $StatusCode`nError: $ErrMsg" [FileSystemUtils]::SafeRemoveFile($Ctx.ZipPath) } } if (!$DownloadSucceeded) { foreach ($err in $ErrorMessages) { Write-LogEntry -Level Error -Message ($err) } throw [DotnetInstallException]::new("Could not find `"$($Ctx.AssetName)`" with version = $($DownloadLinks[0].EffectiveVersion)") } Write-LogEntry -Level Info -Message ("Extracting the archive.") [PerfHelper]::MeasureAction("Package extraction", { [FileSystemUtils]::ExtractDotnetPackage($Ctx.ZipPath, $Ctx.InstallRoot, $OverrideNonVersionedFiles) }) $isAssetInstalled = $false if ($DownloadedLink.EffectiveVersion -match "rtm" -or $DownloadedLink.EffectiveVersion -match "servicing") { $ReleaseVersion = $DownloadedLink.EffectiveVersion.Split("-")[0] $isAssetInstalled = [FileSystemUtils]::IsDotnetPackageInstalled($Ctx.InstallRoot, $Ctx.DotnetPackageRelativePath, $ReleaseVersion) } if (!$isAssetInstalled) { $isAssetInstalled = [FileSystemUtils]::IsDotnetPackageInstalled($Ctx.InstallRoot, $Ctx.DotnetPackageRelativePath, $DownloadedLink.EffectiveVersion) } if (!$isAssetInstalled) { throw [DotnetInstallException]::new("`"$($Ctx.AssetName)`" with version = $($DownloadedLink.EffectiveVersion) failed to install with an unknown error.") } if (!$Ctx.KeepZip) { [FileSystemUtils]::SafeRemoveFile($Ctx.ZipPath) } [PerfHelper]::MeasureAction("Setting up shell environment", { [SystemUtils]::PrependToPath($Ctx.InstallRoot, $Ctx.NoPath) }) Write-LogEntry -Level Info -Message ("Note that the script does not ensure your Windows version is supported during the installation.") Write-LogEntry -Level Info -Message ("Installed version is $($DownloadedLink.EffectiveVersion)") Write-LogEntry -Level Info -Message ("Installation finished") } } } process { $context = [InstallContext]::new() $context.Channel = $Channel $context.Quality = $Quality $context.Version = $Version $context.Internal = $Internal.IsPresent $context.JSonFile = $JSonFile $context.InstallDir = $InstallDir $context.Architecture = $Architecture $context.Runtime = $Runtime $context.SharedRuntime = $SharedRuntime.IsPresent $context.DryRun = $DryRun.IsPresent $context.NoPath = $NoPath.IsPresent $context.AzureFeed = $AzureFeed $context.UncachedFeed = $UncachedFeed $context.FeedCredential = $FeedCredential $context.ProxyAddress = $ProxyAddress $context.ProxyUseDefaultCredentials = $ProxyUseDefaultCredentials.IsPresent $context.ProxyBypassList = $ProxyBypassList $context.SkipNonVersionedFiles = $SkipNonVersionedFiles.IsPresent $context.DownloadTimeout = $DownloadTimeout $context.KeepZip = $KeepZip.IsPresent $context.ZipPath = $ZipPath $context.Invocation = $MyInvocation [DotnetInstall]::Run($context) } end { if ($null -ne [Logger]::Default) { [Logger]::Default.Dispose() } $ErrorActionPreference = $ogeap $ProgressPreference = $ogpap } |