2020-06-13 00:46:25 +07:00
#
# 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 .
2023-05-15 19:09:29 +07:00
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 .
2020-06-13 00:46:25 +07:00
. PARAMETER Channel
Default : LTS
Download from the Channel specified . Possible values :
2022-11-15 23:00:14 +07:00
- STS - the most recent Standard Term Support release
- LTS - the most recent Long Term Support release
2020-06-13 00:46:25 +07:00
- 2 -part version in a format A . B - represents a specific release
examples : 2.0 , 1.0
2021-06-08 16:27:57 +07:00
- 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
2022-11-15 23:00:14 +07:00
Warning : Value " Current " is deprecated for the Channel parameter . Use " STS " instead .
2021-06-08 16:27:57 +07:00
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 , signed , validated , preview , GA .
2022-11-15 23:00:14 +07:00
Works only in combination with channel . Not applicable for STS and LTS channels and will be ignored if those channels are used .
2021-06-08 16:27:57 +07:00
For SDK use channel in A . B . Cxx format : using quality together with channel in A . B format is not supported .
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 .
2020-06-13 00:46:25 +07:00
. PARAMETER Version
Default : latest
Represents a build version on specific channel . Possible values :
2022-07-29 06:22:35 +07:00
- latest - the latest build on specific channel
2020-06-13 00:46:25 +07:00
- 3 -part version in a format A . B . C - represents specific version of build
examples : 2.0 . 0 -preview2 - 006120 , 1.1 . 0
2021-06-08 16:27:57 +07:00
. 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 .
2020-06-13 00:46:25 +07:00
. 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 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
2022-02-20 05:07:00 +07:00
For internal use only .
Allows using a different storage to download SDK archives from .
This parameter is only used if $NoCdn is false .
2020-06-13 00:46:25 +07:00
. PARAMETER UncachedFeed
2022-02-20 05:07:00 +07:00
For internal use only .
Allows using a different storage to download SDK archives from .
This parameter is only used if $NoCdn is true .
2020-06-13 00:46:25 +07:00
. 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 .
2020-08-29 02:01:15 +07:00
. PARAMETER ProxyBypassList
If set with ProxyAddress , will provide the list of comma separated urls that will bypass the proxy
2020-06-13 00:46:25 +07:00
. 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 .
. PARAMETER JSonFile
Determines the SDK version from a user specified global . json file
Note : global . json must have a value for 'SDK:Version'
2021-08-26 16:44:10 +07:00
. PARAMETER DownloadTimeout
Determines timeout duration in seconds for dowloading of the SDK file
Default : 1200 seconds ( 20 minutes )
2020-06-13 00:46:25 +07:00
#>
[ cmdletbinding ( ) ]
param (
[ string ] $Channel = " LTS " ,
2021-06-08 16:27:57 +07:00
[ string ] $Quality ,
2020-06-13 00:46:25 +07:00
[ string ] $Version = " Latest " ,
2021-06-08 16:27:57 +07:00
[ switch ] $Internal ,
2020-06-13 00:46:25 +07:00
[ string ] $JSonFile ,
2021-08-26 16:44:10 +07:00
[ Alias ( 'i' ) ] [ string ] $InstallDir = " <auto> " ,
2020-06-13 00:46:25 +07:00
[ 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 ,
2022-02-20 05:07:00 +07:00
[ string ] $AzureFeed ,
[ string ] $UncachedFeed ,
2020-06-13 00:46:25 +07:00
[ string ] $FeedCredential ,
[ string ] $ProxyAddress ,
[ switch ] $ProxyUseDefaultCredentials ,
2020-08-29 02:01:15 +07:00
[ string[] ] $ProxyBypassList = @ ( ) ,
2020-06-13 00:46:25 +07:00
[ switch ] $SkipNonVersionedFiles ,
2021-08-26 16:44:10 +07:00
[ switch ] $NoCdn ,
[ int ] $DownloadTimeout = 1200
2020-06-13 00:46:25 +07:00
)
Set-StrictMode -Version Latest
$ErrorActionPreference = " Stop "
$ProgressPreference = " SilentlyContinue "
function Say($str ) {
2021-02-02 15:40:17 +07:00
try {
2020-08-29 02:01:15 +07:00
Write-Host " dotnet-install: $str "
}
2021-02-02 15:40:17 +07:00
catch {
2020-08-29 02:01:15 +07:00
# Some platforms cannot utilize Write-Host (Azure Functions, for instance). Fall back to Write-Output
Write-Output " dotnet-install: $str "
}
2020-06-13 00:46:25 +07:00
}
2021-02-02 15:40:17 +07:00
function Say-Warning($str ) {
try {
Write-Warning " dotnet-install: $str "
}
catch {
# Some platforms cannot utilize Write-Warning (Azure Functions, for instance). Fall back to Write-Output
Write-Output " dotnet-install: Warning: $str "
}
}
# Writes a line with error style settings.
# Use this function to show a human-readable comment along with an exception.
function Say-Error($str ) {
try {
# Write-Error is quite oververbose for the purpose of the function, let's write one line with error style settings.
$Host . UI . WriteErrorLine ( " dotnet-install: $str " )
}
catch {
Write-Output " dotnet-install: Error: $str "
}
}
2020-06-13 00:46:25 +07:00
function Say-Verbose($str ) {
2021-02-02 15:40:17 +07:00
try {
2020-08-29 02:01:15 +07:00
Write-Verbose " dotnet-install: $str "
}
2021-02-02 15:40:17 +07:00
catch {
2020-08-29 02:01:15 +07:00
# Some platforms cannot utilize Write-Verbose (Azure Functions, for instance). Fall back to Write-Output
Write-Output " dotnet-install: $str "
}
2020-06-13 00:46:25 +07:00
}
2023-05-15 19:09:29 +07:00
function Measure-Action($name , $block ) {
$time = Measure-Command $block
$totalSeconds = $time . TotalSeconds
Say-Verbose " ⏱ Action ' $name ' took $totalSeconds seconds "
}
2023-09-08 20:06:55 +07:00
function Get-Remote-File-Size($zipUri ) {
try {
$response = Invoke-WebRequest -Uri $zipUri -Method Head
$fileSize = $response . Headers [ " Content-Length " ]
if ( ( ! [ string ] :: IsNullOrEmpty ( $fileSize ) ) ) {
Say " Remote file $zipUri size is $fileSize bytes. "
return $fileSize
}
}
catch {
Say-Verbose " Content-Length header was not extracted for $zipUri . "
}
return $null
}
2020-06-13 00:46:25 +07:00
function Say-Invocation($Invocation ) {
$command = $Invocation . MyCommand ;
$args = ( ( $Invocation . BoundParameters . Keys | foreach { " - $_ `" $( $Invocation . BoundParameters [ $_ ] ) `" " } ) -join " " )
Say-Verbose " $command $args "
}
2021-08-26 16:44:10 +07:00
function Invoke-With-Retry([ScriptBlock]$ScriptBlock , [ System.Threading.CancellationToken ] $cancellationToken = [ System.Threading.CancellationToken ] :: None , [ int ] $MaxAttempts = 3 , [ int ] $SecondsBetweenAttempts = 1 ) {
2020-06-13 00:46:25 +07:00
$Attempts = 0
2022-07-29 06:22:35 +07:00
$local : startTime = $ ( get-date )
2020-06-13 00:46:25 +07:00
while ( $true ) {
try {
2021-02-02 15:40:17 +07:00
return & $ScriptBlock
2020-06-13 00:46:25 +07:00
}
catch {
$Attempts + +
2021-08-26 16:44:10 +07:00
if ( ( $Attempts -lt $MaxAttempts ) -and -not $cancellationToken . IsCancellationRequested ) {
2020-06-13 00:46:25 +07:00
Start-Sleep $SecondsBetweenAttempts
}
else {
2022-07-29 06:22:35 +07:00
$local : elapsedTime = $ ( get-date ) - $local : startTime
if ( ( $local : elapsedTime . TotalSeconds - $DownloadTimeout ) -gt 0 -and -not $cancellationToken . IsCancellationRequested ) {
throw New-Object System . TimeoutException ( " Failed to reach the server: connection timeout: default timeout is $DownloadTimeout second(s) " ) ;
}
throw ;
2020-06-13 00:46:25 +07:00
}
}
}
}
function Get-Machine-Architecture ( ) {
Say-Invocation $MyInvocation
2020-07-16 00:32:15 +07:00
# On PS x86, PROCESSOR_ARCHITECTURE reports x86 even on x64 systems.
# To get the correct architecture, we need to use PROCESSOR_ARCHITEW6432.
# PS x64 doesn't define this, so we fall back to PROCESSOR_ARCHITECTURE.
# Possible values: amd64, x64, x86, arm64, arm
2022-02-20 05:07:00 +07:00
if ( $ENV:PROCESSOR_ARCHITEW6432 -ne $null ) {
2020-07-16 00:32:15 +07:00
return $ENV:PROCESSOR_ARCHITEW6432
}
2022-08-31 22:40:05 +07:00
try {
if ( ( ( Get-CimInstance -ClassName CIM_OperatingSystem ) . OSArchitecture ) -like " ARM* " ) {
if ( [ Environment ] :: Is64BitOperatingSystem )
{
return " arm64 "
}
return " arm "
}
}
catch {
# Machine doesn't support Get-CimInstance
2022-07-29 06:22:35 +07:00
}
2020-06-13 00:46:25 +07:00
return $ENV:PROCESSOR_ARCHITECTURE
}
function Get-CLIArchitecture-From-Architecture([string]$Architecture ) {
Say-Invocation $MyInvocation
2022-02-20 05:07:00 +07:00
if ( $Architecture -eq " <auto> " ) {
$Architecture = Get-Machine -Architecture
}
2021-06-08 16:27:57 +07:00
switch ( $Architecture . ToLowerInvariant ( ) ) {
2020-06-13 00:46:25 +07:00
{ ( $_ -eq " amd64 " ) -or ( $_ -eq " x64 " ) } { return " x64 " }
{ $_ -eq " x86 " } { return " x86 " }
{ $_ -eq " arm " } { return " arm " }
{ $_ -eq " arm64 " } { return " arm64 " }
2021-02-02 15:40:17 +07:00
default { throw " Architecture ' $Architecture ' not supported. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues " }
2020-06-13 00:46:25 +07:00
}
}
2022-02-20 05:07:00 +07:00
function ValidateFeedCredential([string ] $FeedCredential )
{
if ( $Internal -and [ string ] :: IsNullOrWhitespace ( $FeedCredential ) ) {
$message = " Provide credentials via -FeedCredential parameter. "
if ( $DryRun ) {
Say-Warning " $message "
} else {
throw " $message "
}
}
#FeedCredential should start with "?", for it to be added to the end of the link.
#adding "?" at the beginning of the FeedCredential if needed.
if ( ( ! [ string ] :: IsNullOrWhitespace ( $FeedCredential ) ) -and ( $FeedCredential [ 0 ] -ne '?' ) ) {
$FeedCredential = " ? " + $FeedCredential
}
2021-06-08 16:27:57 +07:00
2022-02-20 05:07:00 +07:00
return $FeedCredential
}
2021-06-08 16:27:57 +07:00
function Get-NormalizedQuality([string]$Quality ) {
Say-Invocation $MyInvocation
if ( [ string ] :: IsNullOrEmpty ( $Quality ) ) {
return " "
}
switch ( $Quality ) {
{ @ ( " daily " , " signed " , " validated " , " preview " ) -contains $_ } { return $Quality . ToLowerInvariant ( ) }
#ga quality is available without specifying quality, so normalizing it to empty
{ $_ -eq " ga " } { return " " }
default { throw " ' $Quality ' is not a supported value for -Quality option. Supported values are: daily, signed, validated, preview, ga. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues. " }
}
}
function Get-NormalizedChannel([string]$Channel ) {
Say-Invocation $MyInvocation
if ( [ string ] :: IsNullOrEmpty ( $Channel ) ) {
return " "
}
2022-11-15 23:00:14 +07:00
if ( $Channel . Contains ( " Current " ) ) {
Say-Warning 'Value "Current" is deprecated for -Channel option. Use "STS" instead.'
}
2021-06-08 16:27:57 +07:00
if ( $Channel . StartsWith ( 'release/' ) ) {
Say-Warning 'Using branch name with -Channel option is no longer supported with newer releases. Use -Quality option with a channel in X.Y format instead, such as "-Channel 5.0 -Quality Daily."'
}
switch ( $Channel ) {
{ $_ -eq " lts " } { return " LTS " }
2022-11-15 23:00:14 +07:00
{ $_ -eq " sts " } { return " STS " }
{ $_ -eq " current " } { return " STS " }
2021-06-08 16:27:57 +07:00
default { return $Channel . ToLowerInvariant ( ) }
}
}
function Get-NormalizedProduct([string]$Runtime ) {
Say-Invocation $MyInvocation
switch ( $Runtime ) {
{ $_ -eq " dotnet " } { return " dotnet-runtime " }
{ $_ -eq " aspnetcore " } { return " aspnetcore-runtime " }
{ $_ -eq " windowsdesktop " } { return " windowsdesktop-runtime " }
{ [ string ] :: IsNullOrEmpty ( $_ ) } { return " dotnet-sdk " }
default { throw " ' $Runtime ' is not a supported value for -Runtime option, supported values are: dotnet, aspnetcore, windowsdesktop. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues. " }
}
}
2020-06-13 00:46:25 +07:00
# 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
2022-02-20 05:07:00 +07:00
function Get-Version-From-LatestVersion-File-Content([string]$VersionText ) {
2020-06-13 00:46:25 +07:00
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.
}
}
2021-06-08 16:27:57 +07:00
function GetHTTPResponse([Uri ] $Uri , [ bool ] $HeaderOnly , [ bool ] $DisableRedirect , [ bool ] $DisableFeedCredential )
2020-06-13 00:46:25 +07:00
{
2021-08-26 16:44:10 +07:00
$cts = New-Object System . Threading . CancellationTokenSource
$downloadScript = {
2020-06-13 00:46:25 +07:00
$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 ) ) ) {
2022-02-20 05:07:00 +07:00
if ( $null -ne $DefaultProxy . GetProxy ( $Uri ) ) {
$ProxyAddress = $DefaultProxy . GetProxy ( $Uri ) . OriginalString
} else {
$ProxyAddress = $null
}
2020-06-13 00:46:25 +07:00
$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... " )
}
}
2021-06-08 16:27:57 +07:00
$HttpClientHandler = New-Object System . Net . Http . HttpClientHandler
2020-06-13 00:46:25 +07:00
if ( $ProxyAddress ) {
2020-08-29 02:01:15 +07:00
$HttpClientHandler . Proxy = New-Object System . Net . WebProxy -Property @ {
Address = $ProxyAddress ;
UseDefaultCredentials = $ProxyUseDefaultCredentials ;
BypassList = $ProxyBypassList ;
}
2021-06-08 16:27:57 +07:00
}
if ( $DisableRedirect )
{
$HttpClientHandler . AllowAutoRedirect = $false
2020-06-13 00:46:25 +07:00
}
2021-06-08 16:27:57 +07:00
$HttpClient = New-Object System . Net . Http . HttpClient -ArgumentList $HttpClientHandler
2020-06-13 00:46:25 +07:00
# Default timeout for HttpClient is 100s. For a 50 MB download this assumes 500 KB/s average, any less will time out
2021-08-26 16:44:10 +07:00
# Defaulting to 20 minutes allows it to work over much slower connections.
$HttpClient . Timeout = New-TimeSpan -Seconds $DownloadTimeout
2021-06-08 16:27:57 +07:00
if ( $HeaderOnly ) {
$completionOption = [ System.Net.Http.HttpCompletionOption ] :: ResponseHeadersRead
}
else {
$completionOption = [ System.Net.Http.HttpCompletionOption ] :: ResponseContentRead
}
if ( $DisableFeedCredential ) {
$UriWithCredential = $Uri
}
else {
$UriWithCredential = " ${Uri} ${FeedCredential} "
}
$Task = $HttpClient . GetAsync ( " $UriWithCredential " , $completionOption ) . ConfigureAwait ( " false " ) ;
2021-02-02 15:40:17 +07:00
$Response = $Task . GetAwaiter ( ) . GetResult ( ) ;
2021-06-08 16:27:57 +07:00
if ( ( $null -eq $Response ) -or ( ( -not $HeaderOnly ) -and ( -not ( $Response . IsSuccessStatusCode ) ) ) ) {
2021-02-02 15:40:17 +07:00
# The feed credential is potentially sensitive info. Do not log FeedCredential to console output.
$DownloadException = [ System.Exception ] " Unable to download $Uri . "
if ( $null -ne $Response ) {
$DownloadException . Data [ " StatusCode " ] = [ int ] $Response . StatusCode
$DownloadException . Data [ " ErrorMessage " ] = " Unable to download $Uri . Returned HTTP status code: " + $DownloadException . Data [ " StatusCode " ]
2021-08-26 16:44:10 +07:00
if ( 404 -eq [ int ] $Response . StatusCode )
{
$cts . Cancel ( )
}
2020-06-13 00:46:25 +07:00
}
2021-02-02 15:40:17 +07:00
throw $DownloadException
2020-06-13 00:46:25 +07:00
}
2021-02-02 15:40:17 +07:00
return $Response
}
catch [ System.Net.Http.HttpRequestException ] {
$DownloadException = [ System.Exception ] " Unable to download $Uri . "
# Pick up the exception message and inner exceptions' messages if they exist
$CurrentException = $PSItem . Exception
$ErrorMsg = $CurrentException . Message + " `r `n "
while ( $CurrentException . InnerException ) {
$CurrentException = $CurrentException . InnerException
$ErrorMsg + = $CurrentException . Message + " `r `n "
}
# Check if there is an issue concerning TLS.
if ( $ErrorMsg -like " *SSL/TLS* " ) {
$ErrorMsg + = " Ensure that TLS 1.2 or higher is enabled to use this script. `r `n "
}
$DownloadException . Data [ " ErrorMessage " ] = $ErrorMsg
throw $DownloadException
2020-06-13 00:46:25 +07:00
}
finally {
2021-08-26 16:44:10 +07:00
if ( $null -ne $HttpClient ) {
2020-06-13 00:46:25 +07:00
$HttpClient . Dispose ( )
}
}
2021-08-26 16:44:10 +07:00
}
try {
return Invoke-With -Retry $downloadScript $cts . Token
}
finally
{
if ( $null -ne $cts )
{
$cts . Dispose ( )
}
}
2020-06-13 00:46:25 +07:00
}
2022-02-20 05:07:00 +07:00
function Get-Version-From-LatestVersion-File([string]$AzureFeed , [ string ] $Channel ) {
2020-06-13 00:46:25 +07:00
Say-Invocation $MyInvocation
$VersionFileUrl = $null
if ( $Runtime -eq " dotnet " ) {
2022-02-20 05:07:00 +07:00
$VersionFileUrl = " $AzureFeed /Runtime/ $Channel /latest.version "
2020-06-13 00:46:25 +07:00
}
elseif ( $Runtime -eq " aspnetcore " ) {
2022-02-20 05:07:00 +07:00
$VersionFileUrl = " $AzureFeed /aspnetcore/Runtime/ $Channel /latest.version "
2020-06-13 00:46:25 +07:00
}
elseif ( $Runtime -eq " windowsdesktop " ) {
2022-02-20 05:07:00 +07:00
$VersionFileUrl = " $AzureFeed /WindowsDesktop/ $Channel /latest.version "
2020-06-13 00:46:25 +07:00
}
elseif ( -not $Runtime ) {
2022-02-20 05:07:00 +07:00
$VersionFileUrl = " $AzureFeed /Sdk/ $Channel /latest.version "
2020-06-13 00:46:25 +07:00
}
else {
throw " Invalid value for `$ Runtime "
}
2021-06-08 16:27:57 +07:00
Say-Verbose " Constructed latest.version URL: $VersionFileUrl "
2020-06-13 00:46:25 +07:00
try {
$Response = GetHTTPResponse -Uri $VersionFileUrl
}
catch {
2022-02-20 05:07:00 +07:00
Say-Verbose " Failed to download latest.version file. "
2021-02-02 15:40:17 +07:00
throw
2020-06-13 00:46:25 +07:00
}
$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. " }
}
2022-02-20 05:07:00 +07:00
$VersionInfo = Get-Version -From -LatestVersion -File -Content $VersionText
2020-06-13 00:46:25 +07:00
return $VersionInfo
}
function Parse-Jsonfile-For-Version([string]$JSonFile ) {
Say-Invocation $MyInvocation
If ( -Not ( Test-Path $JSonFile ) ) {
throw " Unable to find ' $JSonFile ' "
}
try {
$JSonContent = Get-Content ( $JSonFile ) -Raw | ConvertFrom-Json | Select-Object -expand " sdk " -ErrorAction SilentlyContinue
}
catch {
2021-02-02 15:40:17 +07:00
Say-Error " Json file unreadable: ' $JSonFile ' "
throw
2020-06-13 00:46:25 +07:00
}
if ( $JSonContent ) {
try {
$JSonContent . PSObject . Properties | ForEach-Object {
$PropertyName = $_ . Name
if ( $PropertyName -eq " version " ) {
$Version = $_ . Value
Say-Verbose " Version = $Version "
}
}
}
catch {
2021-02-02 15:40:17 +07:00
Say-Error " Unable to parse the SDK node in ' $JSonFile ' "
throw
2020-06-13 00:46:25 +07:00
}
}
else {
throw " Unable to find the SDK node in ' $JSonFile ' "
}
If ( $Version -eq $null ) {
throw " Unable to find the SDK:version node in ' $JSonFile ' "
}
return $Version
}
function Get-Specific-Version-From-Version([string]$AzureFeed , [ string ] $Channel , [ string ] $Version , [ string ] $JSonFile ) {
Say-Invocation $MyInvocation
if ( -not $JSonFile ) {
2021-06-08 16:27:57 +07:00
if ( $Version . ToLowerInvariant ( ) -eq " latest " ) {
2022-02-20 05:07:00 +07:00
$LatestVersionInfo = Get-Version -From -LatestVersion -File -AzureFeed $AzureFeed -Channel $Channel
2021-02-02 15:40:17 +07:00
return $LatestVersionInfo . Version
}
else {
return $Version
2020-06-13 00:46:25 +07:00
}
}
else {
return Parse-Jsonfile -For -Version $JSonFile
}
}
function Get-Download-Link([string]$AzureFeed , [ string ] $SpecificVersion , [ string ] $CLIArchitecture ) {
Say-Invocation $MyInvocation
2021-02-02 15:40:17 +07:00
# If anything fails in this lookup it will default to $SpecificVersion
$SpecificProductVersion = Get-Product -Version -AzureFeed $AzureFeed -SpecificVersion $SpecificVersion
2020-06-13 00:46:25 +07:00
if ( $Runtime -eq " dotnet " ) {
2021-02-02 15:40:17 +07:00
$PayloadURL = " $AzureFeed /Runtime/ $SpecificVersion /dotnet-runtime- $SpecificProductVersion -win- $CLIArchitecture .zip "
2020-06-13 00:46:25 +07:00
}
elseif ( $Runtime -eq " aspnetcore " ) {
2021-02-02 15:40:17 +07:00
$PayloadURL = " $AzureFeed /aspnetcore/Runtime/ $SpecificVersion /aspnetcore-runtime- $SpecificProductVersion -win- $CLIArchitecture .zip "
2020-06-13 00:46:25 +07:00
}
elseif ( $Runtime -eq " windowsdesktop " ) {
2021-02-05 15:21:42 +07:00
# The windows desktop runtime is part of the core runtime layout prior to 5.0
2021-02-02 15:40:17 +07:00
$PayloadURL = " $AzureFeed /Runtime/ $SpecificVersion /windowsdesktop-runtime- $SpecificProductVersion -win- $CLIArchitecture .zip "
2021-02-05 15:21:42 +07:00
if ( $SpecificVersion -match '^(\d+)\.(.*)$' )
{
$majorVersion = [ int ] $Matches [ 1 ]
if ( $majorVersion -ge 5 )
{
$PayloadURL = " $AzureFeed /WindowsDesktop/ $SpecificVersion /windowsdesktop-runtime- $SpecificProductVersion -win- $CLIArchitecture .zip "
}
}
2020-06-13 00:46:25 +07:00
}
elseif ( -not $Runtime ) {
2021-02-02 15:40:17 +07:00
$PayloadURL = " $AzureFeed /Sdk/ $SpecificVersion /dotnet-sdk- $SpecificProductVersion -win- $CLIArchitecture .zip "
2020-06-13 00:46:25 +07:00
}
else {
throw " Invalid value for `$ Runtime "
}
Say-Verbose " Constructed primary named payload URL: $PayloadURL "
2021-02-02 15:40:17 +07:00
return $PayloadURL , $SpecificProductVersion
2020-06-13 00:46:25 +07:00
}
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
}
2021-06-08 16:27:57 +07:00
function Get-Product-Version([string]$AzureFeed , [ string ] $SpecificVersion , [ string ] $PackageDownloadLink ) {
2021-02-02 15:40:17 +07:00
Say-Invocation $MyInvocation
2021-06-08 16:27:57 +07:00
# Try to get the version number, using the productVersion.txt file located next to the installer file.
$ProductVersionTxtURLs = ( Get-Product -Version -Url $AzureFeed $SpecificVersion $PackageDownloadLink -Flattened $true ) ,
( Get-Product -Version -Url $AzureFeed $SpecificVersion $PackageDownloadLink -Flattened $false )
Foreach ( $ProductVersionTxtURL in $ProductVersionTxtURLs ) {
Say-Verbose " Checking for the existence of $ProductVersionTxtURL "
try {
$productVersionResponse = GetHTTPResponse ( $productVersionTxtUrl )
if ( $productVersionResponse . StatusCode -eq 200 ) {
$productVersion = $productVersionResponse . Content . ReadAsStringAsync ( ) . Result . Trim ( )
if ( $productVersion -ne $SpecificVersion )
{
Say " Using alternate version $productVersion found in $ProductVersionTxtURL "
}
return $productVersion
}
else {
Say-Verbose " Got StatusCode $( $productVersionResponse . StatusCode ) when trying to get productVersion.txt at $productVersionTxtUrl . "
2021-02-05 15:21:42 +07:00
}
2021-06-08 16:27:57 +07:00
}
catch {
Say-Verbose " Could not read productVersion.txt at $productVersionTxtUrl (Exception: ' $( $_ . Exception . Message ) '. ) "
2021-02-05 15:21:42 +07:00
}
2021-02-02 15:40:17 +07:00
}
2021-06-08 16:27:57 +07:00
# Getting the version number with productVersion.txt has failed. Try parsing the download link for a version number.
if ( [ string ] :: IsNullOrEmpty ( $PackageDownloadLink ) )
{
Say-Verbose " Using the default value ' $SpecificVersion ' as the product version. "
return $SpecificVersion
2021-02-02 15:40:17 +07:00
}
2021-06-08 16:27:57 +07:00
$productVersion = Get-ProductVersionFromDownloadLink $PackageDownloadLink $SpecificVersion
return $productVersion
}
2021-02-02 15:40:17 +07:00
2021-06-08 16:27:57 +07:00
function Get-Product-Version-Url([string]$AzureFeed , [ string ] $SpecificVersion , [ string ] $PackageDownloadLink , [ bool ] $Flattened ) {
Say-Invocation $MyInvocation
2021-02-02 15:40:17 +07:00
2021-06-08 16:27:57 +07:00
$majorVersion = $null
if ( $SpecificVersion -match '^(\d+)\.(.*)' ) {
$majorVersion = $Matches [ 1 ] -as [ int ]
}
2021-02-02 15:40:17 +07:00
2021-06-08 16:27:57 +07:00
$pvFileName = 'productVersion.txt'
if ( $Flattened ) {
if ( -not $Runtime ) {
$pvFileName = 'sdk-productVersion.txt'
}
elseif ( $Runtime -eq " dotnet " ) {
$pvFileName = 'runtime-productVersion.txt'
2021-02-02 15:40:17 +07:00
}
else {
2021-06-08 16:27:57 +07:00
$pvFileName = " $Runtime -productVersion.txt "
2021-02-02 15:40:17 +07:00
}
}
2021-06-08 16:27:57 +07:00
if ( [ string ] :: IsNullOrEmpty ( $PackageDownloadLink ) ) {
if ( $Runtime -eq " dotnet " ) {
$ProductVersionTxtURL = " $AzureFeed /Runtime/ $SpecificVersion / $pvFileName "
}
elseif ( $Runtime -eq " aspnetcore " ) {
$ProductVersionTxtURL = " $AzureFeed /aspnetcore/Runtime/ $SpecificVersion / $pvFileName "
}
elseif ( $Runtime -eq " windowsdesktop " ) {
# The windows desktop runtime is part of the core runtime layout prior to 5.0
$ProductVersionTxtURL = " $AzureFeed /Runtime/ $SpecificVersion / $pvFileName "
if ( $majorVersion -ne $null -and $majorVersion -ge 5 ) {
$ProductVersionTxtURL = " $AzureFeed /WindowsDesktop/ $SpecificVersion / $pvFileName "
}
}
elseif ( -not $Runtime ) {
$ProductVersionTxtURL = " $AzureFeed /Sdk/ $SpecificVersion / $pvFileName "
}
else {
throw " Invalid value ' $Runtime ' specified for `$ Runtime "
}
}
else {
$ProductVersionTxtURL = $PackageDownloadLink . Substring ( 0 , $PackageDownloadLink . LastIndexOf ( " / " ) ) + " / $pvFileName "
}
Say-Verbose " Constructed productVersion link: $ProductVersionTxtURL "
return $ProductVersionTxtURL
}
function Get-ProductVersionFromDownloadLink([string]$PackageDownloadLink , [ string ] $SpecificVersion )
{
Say-Invocation $MyInvocation
#product specific version follows the product name
#for filename 'dotnet-sdk-3.1.404-win-x64.zip': the product version is 3.1.400
$filename = $PackageDownloadLink . Substring ( $PackageDownloadLink . LastIndexOf ( " / " ) + 1 )
$filenameParts = $filename . Split ( '-' )
if ( $filenameParts . Length -gt 2 )
{
$productVersion = $filenameParts [ 2 ]
Say-Verbose " Extracted product version ' $productVersion ' from download link ' $PackageDownloadLink '. "
}
else {
Say-Verbose " Using the default value ' $SpecificVersion ' as the product version. "
$productVersion = $SpecificVersion
}
return $productVersion
2021-02-02 15:40:17 +07:00
}
2020-06-13 00:46:25 +07:00
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 " <auto> " ) {
return Get-User -Share -Path
}
return $InstallDir
}
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: DotnetPackagePath= $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 ) {
2022-02-20 05:07:00 +07:00
# example path with regex: shared/1.0.0-beta-12345/somepath
$match = [ regex ] :: match ( $path , " /\d+\.\d+[^/]+/ " )
2020-06-13 00:46:25 +07:00
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
2022-02-20 05:07:00 +07:00
if ( $null -ne $dir ) {
2020-06-13 00:46:25 +07:00
$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
2022-02-20 05:07:00 +07:00
if ( ( $null -eq $PathWithVersion ) -Or ( $DirectoriesToUnpack -contains $PathWithVersion ) ) {
2020-06-13 00:46:25 +07:00
$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 )
}
}
}
}
2022-07-29 06:22:35 +07:00
catch
{
Say-Error " Failed to extract package. Exception: $_ "
throw ;
}
2020-06-13 00:46:25 +07:00
finally {
2022-02-20 05:07:00 +07:00
if ( $null -ne $Zip ) {
2020-06-13 00:46:25 +07:00
$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
2023-09-08 20:06:55 +07:00
2020-06-13 00:46:25 +07:00
try {
$Response = GetHTTPResponse -Uri $Source
$Stream = $Response . Content . ReadAsStreamAsync ( ) . Result
$File = [ System.IO.File ] :: Create ( $OutPath )
$Stream . CopyTo ( $File )
$File . Close ( )
2023-09-08 20:06:55 +07:00
ValidateRemoteLocalFileSizes -LocalFileOutPath $OutPath -SourceUri $Source
2020-06-13 00:46:25 +07:00
}
finally {
2022-02-20 05:07:00 +07:00
if ( $null -ne $Stream ) {
2020-06-13 00:46:25 +07:00
$Stream . Dispose ( )
}
}
}
2023-09-08 20:06:55 +07:00
function ValidateRemoteLocalFileSizes([string]$LocalFileOutPath , $SourceUri ) {
try {
$remoteFileSize = Get-Remote -File -Size -zipUri $SourceUri
$fileSize = [ long ] ( Get-Item $LocalFileOutPath ) . Length
Say " Downloaded file $SourceUri size is $fileSize bytes. "
if ( ( ! [ string ] :: IsNullOrEmpty ( $remoteFileSize ) ) -and ! ( [ string ] :: IsNullOrEmpty ( $fileSize ) ) ) {
if ( $remoteFileSize -ne $fileSize ) {
Say " 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 {
Say " The remote and local file sizes are equal. "
}
}
else {
Say " Either downloaded or local package size can not be measured. One of them may be corrupted. "
}
}
catch {
Say " Either downloaded or local package size can not be measured. One of them may be corrupted. "
}
}
2021-02-02 15:40:17 +07:00
function SafeRemoveFile($Path ) {
try {
if ( Test-Path $Path ) {
Remove-Item $Path
Say-Verbose " The temporary file `" $Path `" was removed. "
}
2023-09-08 20:06:55 +07:00
else {
2021-02-02 15:40:17 +07:00
Say-Verbose " The temporary file `" $Path `" does not exist, therefore is not removed. "
}
}
2023-09-08 20:06:55 +07:00
catch {
2021-02-02 15:40:17 +07:00
Say-Warning " Failed to remove the temporary file: `" $Path `" , remove it manually. "
}
}
2022-02-20 05:07:00 +07:00
function Prepend-Sdk-InstallRoot-To-Path([string]$InstallRoot ) {
$BinPath = Get-Absolute -Path $ ( Join-Path -Path $InstallRoot -ChildPath " " )
2020-06-13 00:46:25 +07:00
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 "
}
}
2022-02-20 05:07:00 +07:00
function PrintDryRunOutput($Invocation , $DownloadLinks )
{
Say " Payload URLs: "
for ( $linkIndex = 0 ; $linkIndex -lt $DownloadLinks . count ; $linkIndex + + ) {
Say " URL # $linkIndex - $( $DownloadLinks [ $linkIndex ] . type ) : $( $DownloadLinks [ $linkIndex ] . downloadLink ) "
}
$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 $Invocation . BoundParameters . Keys ) {
if ( -not ( @ ( " Architecture " , " Channel " , " DryRun " , " InstallDir " , " Runtime " , " SharedRuntime " , " Version " , " Quality " , " FeedCredential " ) -contains $key ) ) {
$RepeatableCommand + = " - $key `" $( $Invocation . BoundParameters [ $key ] ) `" "
}
}
if ( $Invocation . BoundParameters . Keys -contains " FeedCredential " ) {
$RepeatableCommand + = " -FeedCredential `" <feedCredential> `" "
}
Say " Repeatable invocation: $RepeatableCommand "
if ( $SpecificVersion -ne $EffectiveVersion )
{
Say " NOTE: Due to finding a version manifest with this runtime, it would actually install with version ' $EffectiveVersion ' "
}
}
2021-06-08 16:27:57 +07:00
function Get-AkaMSDownloadLink([string]$Channel , [ string ] $Quality , [ bool ] $Internal , [ string ] $Product , [ string ] $Architecture ) {
Say-Invocation $MyInvocation
2022-11-15 23:00:14 +07:00
#quality is not supported for LTS or STS channel
if ( ! [ string ] :: IsNullOrEmpty ( $Quality ) -and ( @ ( " LTS " , " STS " ) -contains $Channel ) ) {
2021-06-08 16:27:57 +07:00
$Quality = " "
2022-11-15 23:00:14 +07:00
Say-Warning " Specifying quality for STS or LTS channel is not supported, the quality will be ignored. "
2021-06-08 16:27:57 +07:00
}
Say-Verbose " Retrieving primary payload URL from aka.ms link for channel: ' $Channel ', quality: ' $Quality ' product: ' $Product ', os: 'win', architecture: ' $Architecture '. "
#construct aka.ms link
$akaMsLink = " https://aka.ms/dotnet "
if ( $Internal ) {
$akaMsLink + = " /internal "
}
$akaMsLink + = " / $Channel "
if ( -not [ string ] :: IsNullOrEmpty ( $Quality ) ) {
$akaMsLink + = " / $Quality "
}
$akaMsLink + = " / $Product -win- $Architecture .zip "
Say-Verbose " Constructed aka.ms link: ' $akaMsLink '. "
$akaMsDownloadLink = $null
for ( $maxRedirections = 9 ; $maxRedirections -ge 0 ; $maxRedirections - - )
{
#get HTTP response
#do not pass credentials as a part of the $akaMsLink and do not apply credentials in the GetHTTPResponse function
#otherwise the redirect link would have credentials as well
#it would result in applying credentials twice to the resulting link and thus breaking it, and in echoing credentials to the output as a part of redirect link
$Response = GetHTTPResponse -Uri $akaMsLink -HeaderOnly $true -DisableRedirect $true -DisableFeedCredential $true
Say-Verbose " Received response: `n $Response "
if ( [ string ] :: IsNullOrEmpty ( $Response ) ) {
Say-Verbose " The link ' $akaMsLink ' is not valid: failed to get redirect location. The resource is not available. "
return $null
}
#if HTTP code is 301 (Moved Permanently), the redirect link exists
if ( $Response . StatusCode -eq 301 )
{
try {
$akaMsDownloadLink = $Response . Headers . GetValues ( " Location " ) [ 0 ]
if ( [ string ] :: IsNullOrEmpty ( $akaMsDownloadLink ) ) {
Say-Verbose " The link ' $akaMsLink ' is not valid: server returned 301 (Moved Permanently), but the headers do not contain the redirect location. "
return $null
}
Say-Verbose " The redirect location retrieved: ' $akaMsDownloadLink '. "
# This may yet be a link to another redirection. Attempt to retrieve the page again.
$akaMsLink = $akaMsDownloadLink
continue
}
catch {
Say-Verbose " The link ' $akaMsLink ' is not valid: failed to get redirect location. "
return $null
}
}
elseif ( ( ( $Response . StatusCode -lt 300 ) -or ( $Response . StatusCode -ge 400 ) ) -and ( -not [ string ] :: IsNullOrEmpty ( $akaMsDownloadLink ) ) )
{
# Redirections have ended.
return $akaMsDownloadLink
}
Say-Verbose " The link ' $akaMsLink ' is not valid: failed to retrieve the redirection location. "
return $null
}
Say-Verbose " Aka.ms links have redirected more than the maximum allowed redirections. This may be caused by a cyclic redirection of aka.ms links. "
return $null
}
2022-02-20 05:07:00 +07:00
function Get-AkaMsLink-And-Version([string ] $NormalizedChannel , [ string ] $NormalizedQuality , [ bool ] $Internal , [ string ] $ProductName , [ string ] $Architecture ) {
$AkaMsDownloadLink = Get-AkaMSDownloadLink -Channel $NormalizedChannel -Quality $NormalizedQuality -Internal $Internal -Product $ProductName -Architecture $Architecture
2021-06-08 16:27:57 +07:00
if ( [ string ] :: IsNullOrEmpty ( $AkaMsDownloadLink ) ) {
if ( -not [ string ] :: IsNullOrEmpty ( $NormalizedQuality ) ) {
# if quality is specified - exit with error - there is no fallback approach
2022-02-20 05:07:00 +07:00
Say-Error " Failed to locate the latest version in the channel ' $NormalizedChannel ' with ' $NormalizedQuality ' quality for ' $ProductName ', os: 'win', architecture: ' $Architecture '. "
2021-06-08 16:27:57 +07:00
Say-Error " Refer to: https://aka.ms/dotnet-os-lifecycle for information on .NET Core support. "
throw " aka.ms link resolution failure "
}
Say-Verbose " Falling back to latest.version file approach. "
2022-02-20 05:07:00 +07:00
return ( $null , $null , $null )
2021-06-08 16:27:57 +07:00
}
else {
Say-Verbose " Retrieved primary named payload URL from aka.ms link: ' $AkaMsDownloadLink '. "
Say-Verbose " Downloading using legacy url will not be attempted. "
#get version from the path
2022-02-20 05:07:00 +07:00
$pathParts = $AkaMsDownloadLink . Split ( '/' )
2021-06-08 16:27:57 +07:00
if ( $pathParts . Length -ge 2 ) {
$SpecificVersion = $pathParts [ $pathParts . Length - 2 ]
Say-Verbose " Version: ' $SpecificVersion '. "
}
else {
2022-02-20 05:07:00 +07:00
Say-Error " Failed to extract the version from download link ' $AkaMsDownloadLink '. "
return ( $null , $null , $null )
2021-06-08 16:27:57 +07:00
}
#retrieve effective (product) version
2022-02-20 05:07:00 +07:00
$EffectiveVersion = Get-Product -Version -SpecificVersion $SpecificVersion -PackageDownloadLink $AkaMsDownloadLink
2021-06-08 16:27:57 +07:00
Say-Verbose " Product version: ' $EffectiveVersion '. "
2022-02-20 05:07:00 +07:00
return ( $AkaMsDownloadLink , $SpecificVersion , $EffectiveVersion ) ;
2021-06-08 16:27:57 +07:00
}
}
2022-02-20 05:07:00 +07:00
function Get-Feeds-To-Use ( )
{
$feeds = @ (
" https://dotnetcli.azureedge.net/dotnet " ,
" https://dotnetbuilds.azureedge.net/public "
)
2021-06-08 16:27:57 +07:00
2022-02-20 05:07:00 +07:00
if ( -not [ string ] :: IsNullOrEmpty ( $AzureFeed ) ) {
$feeds = @ ( $AzureFeed )
}
2020-06-13 00:46:25 +07:00
2022-02-20 05:07:00 +07:00
if ( $NoCdn ) {
$feeds = @ (
" https://dotnetcli.blob.core.windows.net/dotnet " ,
" https://dotnetbuilds.blob.core.windows.net/public "
)
2020-06-13 00:46:25 +07:00
2022-02-20 05:07:00 +07:00
if ( -not [ string ] :: IsNullOrEmpty ( $UncachedFeed ) ) {
$feeds = @ ( $UncachedFeed )
}
2020-06-13 00:46:25 +07:00
}
2022-02-20 05:07:00 +07:00
return $feeds
}
function Resolve-AssetName-And-RelativePath([string ] $Runtime ) {
2020-06-13 00:46:25 +07:00
if ( $Runtime -eq " dotnet " ) {
2022-02-20 05:07:00 +07:00
$assetName = " .NET Core Runtime "
$dotnetPackageRelativePath = " shared\Microsoft.NETCore.App "
2020-06-13 00:46:25 +07:00
}
elseif ( $Runtime -eq " aspnetcore " ) {
2022-02-20 05:07:00 +07:00
$assetName = " ASP.NET Core Runtime "
$dotnetPackageRelativePath = " shared\Microsoft.AspNetCore.App "
2021-06-08 16:27:57 +07:00
}
2022-02-20 05:07:00 +07:00
elseif ( $Runtime -eq " windowsdesktop " ) {
$assetName = " .NET Core Windows Desktop Runtime "
$dotnetPackageRelativePath = " shared\Microsoft.WindowsDesktop.App "
2020-06-13 00:46:25 +07:00
}
2022-02-20 05:07:00 +07:00
elseif ( -not $Runtime ) {
$assetName = " .NET Core SDK "
$dotnetPackageRelativePath = " sdk "
2021-06-08 16:27:57 +07:00
}
2022-02-20 05:07:00 +07:00
else {
throw " Invalid value for `$ Runtime "
2021-02-02 15:40:17 +07:00
}
2022-02-20 05:07:00 +07:00
return ( $assetName , $dotnetPackageRelativePath )
2020-06-13 00:46:25 +07:00
}
2022-02-20 05:07:00 +07:00
function Prepare-Install-Directory {
2023-09-08 20:06:55 +07:00
$diskSpaceWarning = " Failed to check the disk space. Installation will continue, but it may fail if you do not have enough disk space. " ;
if ( $PSVersionTable . PSVersion . Major -lt 7 ) {
Say-Verbose $diskSpaceWarning
return
}
2022-02-20 05:07:00 +07:00
New-Item -ItemType Directory -Force -Path $InstallRoot | Out-Null
$installDrive = $ ( ( Get-Item $InstallRoot -Force ) . PSDrive . Name ) ;
$diskInfo = $null
2023-09-08 20:06:55 +07:00
try {
2022-02-20 05:07:00 +07:00
$diskInfo = Get-PSDrive -Name $installDrive
}
2023-09-08 20:06:55 +07:00
catch {
Say-Warning $diskSpaceWarning
2022-02-20 05:07:00 +07:00
}
2023-09-08 20:06:55 +07:00
# The check is relevant for PS version >= 7, the result can be irrelevant for older versions. See https://github.com/PowerShell/PowerShell/issues/12442.
2022-02-20 05:07:00 +07:00
if ( ( $null -ne $diskInfo ) -and ( $diskInfo . Free / 1 MB -le 100 ) ) {
throw " There is not enough disk space on drive ${installDrive} : "
}
2020-06-13 00:46:25 +07:00
}
2022-02-20 05:07:00 +07:00
2023-05-15 19:09:29 +07:00
Say-Verbose " Note that the intended use of this script is for Continuous Integration (CI) scenarios, where: "
Say-Verbose " - The SDK needs to be installed without user interaction and without admin rights. "
Say-Verbose " - The SDK installation doesn't need to persist across multiple CI runs. "
Say-Verbose " 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. `r `n "
2022-02-20 05:07:00 +07:00
if ( $SharedRuntime -and ( -not $Runtime ) ) {
$Runtime = " dotnet "
2020-06-13 00:46:25 +07:00
}
2022-02-20 05:07:00 +07:00
$OverrideNonVersionedFiles = ! $SkipNonVersionedFiles
2023-05-15 19:09:29 +07:00
Measure-Action " Product discovery " {
$script:CLIArchitecture = Get-CLIArchitecture -From -Architecture $Architecture
$script:NormalizedQuality = Get-NormalizedQuality $Quality
Say-Verbose " Normalized quality: ' $NormalizedQuality ' "
$script:NormalizedChannel = Get-NormalizedChannel $Channel
Say-Verbose " Normalized channel: ' $NormalizedChannel ' "
$script:NormalizedProduct = Get-NormalizedProduct $Runtime
Say-Verbose " Normalized product: ' $NormalizedProduct ' "
$script:FeedCredential = ValidateFeedCredential $FeedCredential
}
2022-02-20 05:07:00 +07:00
$InstallRoot = Resolve-Installation -Path $InstallDir
Say-Verbose " InstallRoot: $InstallRoot "
$ScriptName = $MyInvocation . MyCommand . Name
( $assetName , $dotnetPackageRelativePath ) = Resolve-AssetName -And -RelativePath -Runtime $Runtime
$feeds = Get-Feeds -To -Use
$DownloadLinks = @ ( )
2022-07-29 06:22:35 +07:00
if ( $Version . ToLowerInvariant ( ) -ne " latest " -and -not [ string ] :: IsNullOrEmpty ( $Quality ) ) {
2022-11-15 23:00:14 +07:00
throw " Quality and Version options are not allowed to be specified simultaneously. See https:// learn.microsoft.com/dotnet/core/tools/dotnet-install-script#options for details. "
2022-07-29 06:22:35 +07:00
}
2022-02-20 05:07:00 +07:00
# aka.ms links can only be used if the user did not request a specific version via the command line or a global.json file.
if ( [ string ] :: IsNullOrEmpty ( $JSonFile ) -and ( $Version -eq " latest " ) ) {
( $DownloadLink , $SpecificVersion , $EffectiveVersion ) = Get-AkaMsLink -And -Version $NormalizedChannel $NormalizedQuality $Internal $NormalizedProduct $CLIArchitecture
if ( $null -ne $DownloadLink ) {
$DownloadLinks + = New-Object PSObject -Property @ { downloadLink = " $DownloadLink " ; specificVersion = " $SpecificVersion " ; effectiveVersion = " $EffectiveVersion " ; type = 'aka.ms' }
Say-Verbose " Generated aka.ms link $DownloadLink with version $EffectiveVersion "
if ( -Not $DryRun ) {
Say-Verbose " Checking if the version $EffectiveVersion is already installed "
if ( Is-Dotnet -Package -Installed -InstallRoot $InstallRoot -RelativePathToPackage $dotnetPackageRelativePath -SpecificVersion $EffectiveVersion )
{
Say " $assetName with version ' $EffectiveVersion ' is already installed. "
Prepend-Sdk -InstallRoot -To -Path -InstallRoot $InstallRoot
return
}
}
}
2020-06-13 00:46:25 +07:00
}
2022-02-20 05:07:00 +07:00
# Primary and legacy links cannot be used if a quality was specified.
# If we already have an aka.ms link, no need to search the blob feeds.
if ( [ string ] :: IsNullOrEmpty ( $NormalizedQuality ) -and 0 -eq $DownloadLinks . count )
2021-02-02 15:40:17 +07:00
{
2022-02-20 05:07:00 +07:00
foreach ( $feed in $feeds ) {
try {
$SpecificVersion = Get-Specific -Version -From -Version -AzureFeed $feed -Channel $Channel -Version $Version -JSonFile $JSonFile
$DownloadLink , $EffectiveVersion = Get-Download -Link -AzureFeed $feed -SpecificVersion $SpecificVersion -CLIArchitecture $CLIArchitecture
$LegacyDownloadLink = Get-LegacyDownload -Link -AzureFeed $feed -SpecificVersion $SpecificVersion -CLIArchitecture $CLIArchitecture
$DownloadLinks + = New-Object PSObject -Property @ { downloadLink = " $DownloadLink " ; specificVersion = " $SpecificVersion " ; effectiveVersion = " $EffectiveVersion " ; type = 'primary' }
Say-Verbose " Generated primary link $DownloadLink with version $EffectiveVersion "
if ( -not [ string ] :: IsNullOrEmpty ( $LegacyDownloadLink ) ) {
$DownloadLinks + = New-Object PSObject -Property @ { downloadLink = " $LegacyDownloadLink " ; specificVersion = " $SpecificVersion " ; effectiveVersion = " $EffectiveVersion " ; type = 'legacy' }
Say-Verbose " Generated legacy link $LegacyDownloadLink with version $EffectiveVersion "
}
if ( -Not $DryRun ) {
Say-Verbose " Checking if the version $EffectiveVersion is already installed "
if ( Is-Dotnet -Package -Installed -InstallRoot $InstallRoot -RelativePathToPackage $dotnetPackageRelativePath -SpecificVersion $EffectiveVersion )
{
Say " $assetName with version ' $EffectiveVersion ' is already installed. "
Prepend-Sdk -InstallRoot -To -Path -InstallRoot $InstallRoot
return
}
}
}
catch
{
Say-Verbose " Failed to acquire download links from feed $feed . Exception: $_ "
}
}
2021-02-02 15:40:17 +07:00
}
2022-02-20 05:07:00 +07:00
if ( $DownloadLinks . count -eq 0 ) {
throw " Failed to resolve the exact version number. "
2020-06-13 00:46:25 +07:00
}
2022-02-20 05:07:00 +07:00
if ( $DryRun ) {
PrintDryRunOutput $MyInvocation $DownloadLinks
return
2021-06-08 16:27:57 +07:00
}
2023-05-15 19:09:29 +07:00
Measure-Action " Installation directory preparation " { Prepare-Install -Directory }
2020-06-13 00:46:25 +07:00
$ZipPath = [ System.IO.Path ] :: combine ( [ System.IO.Path ] :: GetTempPath ( ) , [ System.IO.Path ] :: GetRandomFileName ( ) )
Say-Verbose " Zip path: $ZipPath "
2022-02-20 05:07:00 +07:00
$DownloadSucceeded = $false
$DownloadedLink = $null
$ErrorMessages = @ ( )
2021-02-02 15:40:17 +07:00
2022-02-20 05:07:00 +07:00
foreach ( $link in $DownloadLinks )
{
Say-Verbose " Downloading `" $( $link . type ) `" link $( $link . downloadLink ) "
2021-02-02 15:40:17 +07:00
2022-02-20 05:07:00 +07:00
try {
2023-05-15 19:09:29 +07:00
Measure-Action " Package download " { DownloadFile -Source $link . downloadLink -OutPath $ZipPath }
2022-02-20 05:07:00 +07:00
Say-Verbose " Download succeeded. "
$DownloadSucceeded = $true
$DownloadedLink = $link
break
2021-02-02 15:40:17 +07:00
}
2022-02-20 05:07:00 +07:00
catch {
$StatusCode = $null
$ErrorMessage = $null
2021-02-02 15:40:17 +07:00
2022-02-20 05:07:00 +07:00
if ( $PSItem . Exception . Data . Contains ( " StatusCode " ) ) {
$StatusCode = $PSItem . Exception . Data [ " StatusCode " ]
}
if ( $PSItem . Exception . Data . Contains ( " ErrorMessage " ) ) {
$ErrorMessage = $PSItem . Exception . Data [ " ErrorMessage " ]
} else {
$ErrorMessage = $PSItem . Exception . Message
}
2021-02-02 15:40:17 +07:00
2022-02-20 05:07:00 +07:00
Say-Verbose " Download failed with status code $StatusCode . Error message: $ErrorMessage "
$ErrorMessages + = " Downloading from `" $( $link . type ) `" link has failed with error: `n Uri: $( $link . downloadLink ) `n StatusCode: $StatusCode `n Error: $ErrorMessage "
2021-02-02 15:40:17 +07:00
}
2022-02-20 05:07:00 +07:00
# This link failed. Clean up before trying the next one.
2021-02-02 15:40:17 +07:00
SafeRemoveFile -Path $ZipPath
2020-06-13 00:46:25 +07:00
}
2022-02-20 05:07:00 +07:00
if ( -not $DownloadSucceeded ) {
foreach ( $ErrorMessage in $ErrorMessages ) {
Say-Error $ErrorMessages
2021-02-02 15:40:17 +07:00
}
2022-02-20 05:07:00 +07:00
throw " Could not find `" $assetName `" with version = $( $DownloadLinks [ 0 ] . effectiveVersion ) `n Refer to: https://aka.ms/dotnet-os-lifecycle for information on .NET support "
2020-06-13 00:46:25 +07:00
}
2022-02-20 05:07:00 +07:00
Say " Extracting the archive. "
2023-05-15 19:09:29 +07:00
Measure-Action " Package extraction " { Extract-Dotnet -Package -ZipPath $ZipPath -OutPath $InstallRoot }
2020-06-13 00:46:25 +07:00
# Check if the SDK version is installed; if not, fail the installation.
$isAssetInstalled = $false
# if the version contains "RTM" or "servicing"; check if a 'release-type' SDK version is installed.
2022-02-20 05:07:00 +07:00
if ( $DownloadedLink . effectiveVersion -Match " rtm " -or $DownloadedLink . effectiveVersion -Match " servicing " ) {
$ReleaseVersion = $DownloadedLink . effectiveVersion . Split ( " - " ) [ 0 ]
2020-06-13 00:46:25 +07:00
Say-Verbose " Checking installation: version = $ReleaseVersion "
$isAssetInstalled = Is-Dotnet -Package -Installed -InstallRoot $InstallRoot -RelativePathToPackage $dotnetPackageRelativePath -SpecificVersion $ReleaseVersion
}
# Check if the SDK version is installed.
if ( ! $isAssetInstalled ) {
2022-02-20 05:07:00 +07:00
Say-Verbose " Checking installation: version = $( $DownloadedLink . effectiveVersion ) "
$isAssetInstalled = Is-Dotnet -Package -Installed -InstallRoot $InstallRoot -RelativePathToPackage $dotnetPackageRelativePath -SpecificVersion $DownloadedLink . effectiveVersion
2020-06-13 00:46:25 +07:00
}
2021-02-02 15:40:17 +07:00
# Version verification failed. More likely something is wrong either with the downloaded content or with the verification algorithm.
2020-06-13 00:46:25 +07:00
if ( ! $isAssetInstalled ) {
2022-02-20 05:07:00 +07:00
Say-Error " Failed to verify the version of installed `" $assetName `" . `n Installation source: $( $DownloadedLink . downloadLink ) . `n Installation location: $InstallRoot . `n Report the bug at https://github.com/dotnet/install-scripts/issues. "
throw " `" $assetName `" with version = $( $DownloadedLink . effectiveVersion ) failed to install with an unknown error. "
2020-06-13 00:46:25 +07:00
}
2021-02-02 15:40:17 +07:00
SafeRemoveFile -Path $ZipPath
2020-06-13 00:46:25 +07:00
2023-05-15 19:09:29 +07:00
Measure-Action " Setting up shell environment " { Prepend-Sdk -InstallRoot -To -Path -InstallRoot $InstallRoot }
2020-06-13 00:46:25 +07:00
2021-02-02 15:40:17 +07:00
Say " Note that the script does not resolve dependencies during installation. "
2022-11-15 23:00:14 +07:00
Say " To check the list of dependencies, go to https://learn.microsoft.com/dotnet/core/install/windows#dependencies "
2022-07-29 06:22:35 +07:00
Say " Installed version is $( $DownloadedLink . effectiveVersion ) "
2020-06-13 00:46:25 +07:00
Say " Installation finished "
# SIG # Begin signature block
2023-09-08 20:06:55 +07:00
# MIIoKgYJKoZIhvcNAQcCoIIoGzCCKBcCAQExDzANBglghkgBZQMEAgEFADB5Bgor
2020-06-13 00:46:25 +07:00
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
2023-09-08 20:06:55 +07:00
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDHUcYiJ7KWN+hZ
# Iks0/BD3PXFcvRIYlRRYfy3L+sVa1aCCDXYwggX0MIID3KADAgECAhMzAAADTrU8
# esGEb+srAAAAAANOMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
2020-06-13 00:46:25 +07:00
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
2023-09-08 20:06:55 +07:00
# bmcgUENBIDIwMTEwHhcNMjMwMzE2MTg0MzI5WhcNMjQwMzE0MTg0MzI5WjB0MQsw
2020-06-13 00:46:25 +07:00
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
2023-09-08 20:06:55 +07:00
# AQDdCKiNI6IBFWuvJUmf6WdOJqZmIwYs5G7AJD5UbcL6tsC+EBPDbr36pFGo1bsU
# p53nRyFYnncoMg8FK0d8jLlw0lgexDDr7gicf2zOBFWqfv/nSLwzJFNP5W03DF/1
# 1oZ12rSFqGlm+O46cRjTDFBpMRCZZGddZlRBjivby0eI1VgTD1TvAdfBYQe82fhm
# WQkYR/lWmAK+vW/1+bO7jHaxXTNCxLIBW07F8PBjUcwFxxyfbe2mHB4h1L4U0Ofa
# +HX/aREQ7SqYZz59sXM2ySOfvYyIjnqSO80NGBaz5DvzIG88J0+BNhOu2jl6Dfcq
# jYQs1H/PMSQIK6E7lXDXSpXzAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUnMc7Zn/ukKBsBiWkwdNfsN5pdwAw
2023-05-15 19:09:29 +07:00
# RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW
2023-09-08 20:06:55 +07:00
# MBQGA1UEBRMNMjMwMDEyKzUwMDUxNjAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci
2023-05-15 19:09:29 +07:00
# tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG
# CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu
# Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0
2023-09-08 20:06:55 +07:00
# MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAD21v9pHoLdBSNlFAjmk
# mx4XxOZAPsVxxXbDyQv1+kGDe9XpgBnT1lXnx7JDpFMKBwAyIwdInmvhK9pGBa31
# TyeL3p7R2s0L8SABPPRJHAEk4NHpBXxHjm4TKjezAbSqqbgsy10Y7KApy+9UrKa2
# kGmsuASsk95PVm5vem7OmTs42vm0BJUU+JPQLg8Y/sdj3TtSfLYYZAaJwTAIgi7d
# hzn5hatLo7Dhz+4T+MrFd+6LUa2U3zr97QwzDthx+RP9/RZnur4inzSQsG5DCVIM
# pA1l2NWEA3KAca0tI2l6hQNYsaKL1kefdfHCrPxEry8onJjyGGv9YKoLv6AOO7Oh
# JEmbQlz/xksYG2N/JSOJ+QqYpGTEuYFYVWain7He6jgb41JbpOGKDdE/b+V2q/gX
# UgFe2gdwTpCDsvh8SMRoq1/BNXcr7iTAU38Vgr83iVtPYmFhZOVM0ULp/kKTVoir
# IpP2KCxT4OekOctt8grYnhJ16QMjmMv5o53hjNFXOxigkQWYzUO+6w50g0FAeFa8
# 5ugCCB6lXEk21FFB1FdIHpjSQf+LP/W2OV/HfhC3uTPgKbRtXo83TZYEudooyZ/A
# Vu08sibZ3MkGOJORLERNwKm2G7oqdOv4Qj8Z0JrGgMzj46NFKAxkLSpE5oHQYP1H
# tPx1lPfD7iNSbJsP6LiUHXH1MIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq
2023-05-15 19:09:29 +07:00
# hkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x
# EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv
# bjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5
# IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEwOTA5WjB+MQswCQYDVQQG
# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG
# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQg
# Q29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
# CgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+laUKq4BjgaBEm6f8MMHt03
# a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc6Whe0t+bU7IKLMOv2akr
# rnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4Ddato88tt8zpcoRb0Rrrg
# OGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+lD3v++MrWhAfTVYoonpy
# 4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nkkDstrjNYxbc+/jLTswM9
# sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6A4aN91/w0FK/jJSHvMAh
# dCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmdX4jiJV3TIUs+UsS1Vz8k
# A/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL5zmhD+kjSbwYuER8ReTB
# w3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zdsGbiwZeBe+3W7UvnSSmn
# Eyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3T8HhhUSJxAlMxdSlQy90
# lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS4NaIjAsCAwEAAaOCAe0w
# ggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRIbmTlUAXTgqoXNzcitW2o
# ynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYD
# VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBDuRQFTuHqp8cx0SOJNDBa
# BgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2Ny
# bC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3JsMF4GCCsG
# AQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3dy5taWNyb3NvZnQuY29t
# L3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3J0MIGfBgNV
# HSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEFBQcCARYzaHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1hcnljcHMuaHRtMEAGCCsG
# AQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkAYwB5AF8AcwB0AGEAdABl
# AG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn8oalmOBUeRou09h0ZyKb
# C5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7v0epo/Np22O/IjWll11l
# hJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0bpdS1HXeUOeLpZMlEPXh6
# I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/KmtYSWMfCWluWpiW5IP0
# wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvyCInWH8MyGOLwxS3OW560
# STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBpmLJZiWhub6e3dMNABQam
# ASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJihsMdYzaXht/a8/jyFqGa
# J+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYbBL7fQccOKO7eZS/sl/ah
# XJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbSoqKfenoi+kiVH6v7RyOA
# 9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sLgOppO6/8MO0ETI7f33Vt
# Y5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtXcVZOSEXAQsmbdlsKgEhr
2023-09-08 20:06:55 +07:00
# /Xmfwb1tbWrJUnMTDXpQzTGCGgowghoGAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw
2023-05-15 19:09:29 +07:00
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp
2023-09-08 20:06:55 +07:00
# Z25pbmcgUENBIDIwMTECEzMAAANOtTx6wYRv6ysAAAAAA04wDQYJYIZIAWUDBAIB
2023-05-15 19:09:29 +07:00
# BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO
2023-09-08 20:06:55 +07:00
# MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIFNdom4kjzZaAu4LDOOngnbU
# 0Io5JE1Lpm7n8IKIslYUMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A
2023-05-15 19:09:29 +07:00
# cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB
2023-09-08 20:06:55 +07:00
# BQAEggEAm3iNB/GVCxaIY5Nu7sM6Mz+ug4TiAGimyoRDk4HHb5RRFSZ70scT3LZJ
# /2rtWpzcNzvnha8amGYGlG1pc1l6jbLRYllLfz1cSBDh85X06PDCLpWn1nR/DUV2
# 8Wn+CGPDrc+ER894mdIer6hrEBAk7npFGi+dMDA5yZBzuxeYFOosk2VPYswPnttE
# dgrF1otNqN8pu/dKh1T8z7bl9DQiTcL8Mx9/OH0Zj4fUG7SFY7lvj5nYv0ZP9eBD
# mNX2r0LFOJTyXj1An07rOdfWgjdxoHxte19Y0AlhVKV6lVjZ70lepAG2IWnBXfNR
# kE2SP+Rh9VEAaT847asFazfr01OgkaGCF5QwgheQBgorBgEEAYI3AwMBMYIXgDCC
# F3wGCSqGSIb3DQEHAqCCF20wghdpAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFSBgsq
# hkiG9w0BCRABBKCCAUEEggE9MIIBOQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl
# AwQCAQUABCDrxE/xOb1h+CZsQ81iX7zN2wDgPK6IEmuMRvDSOQkm5AIGZNTJ6U2k
# GBMyMDIzMDgzMTA5NDkxMi4zOTNaMASAAgH0oIHRpIHOMIHLMQswCQYDVQQGEwJV
2023-05-15 19:09:29 +07:00
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
2023-09-08 20:06:55 +07:00
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1l
# cmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046QTAwMC0w
# NUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2Wg
# ghHqMIIHIDCCBQigAwIBAgITMwAAAdB3CKrvoxfG3QABAAAB0DANBgkqhkiG9w0B
# AQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD
# VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAeFw0yMzA1MjUxOTEy
# MTRaFw0yNDAyMDExOTEyMTRaMIHLMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz
# aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv
# cnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25z
# MScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046QTAwMC0wNUUwLUQ5NDcxJTAjBgNV
# BAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggIiMA0GCSqGSIb3DQEB
# AQUAA4ICDwAwggIKAoICAQDfMlfn35fvM0XAUSmI5qiG0UxPi25HkSyBgzk3zpYO
# 311d1OEEFz0QpAK23s1dJFrjB5gD+SMw5z6EwxC4CrXU9KaQ4WNHqHrhWftpgo3M
# kJex9frmO9MldUfjUG56sIW6YVF6YjX+9rT1JDdCDHbo5nZiasMigGKawGb2HqD7
# /kjRR67RvVh7Q4natAVu46Zf5MLviR0xN5cNG20xwBwgttaYEk5XlULaBH5OnXz2
# eWoIx+SjDO7Bt5BuABWY8SvmRQfByT2cppEzTjt/fs0xp4B1cAHVDwlGwZuv9Rfc
# 3nddxgFrKA8MWHbJF0+aWUUYIBR8Fy2guFVHoHeOze7IsbyvRrax//83gYqo8c5Z
# /1/u7kjLcTgipiyZ8XERsLEECJ5ox1BBLY6AjmbgAzDdNl2Leej+qIbdBr/SUvKE
# C+Xw4xjFMOTUVWKWemt2khwndUfBNR7Nzu1z9L0Wv7TAY/v+v6pNhAeohPMCFJc+
# ak6uMD8TKSzWFjw5aADkmD9mGuC86yvSKkII4MayzoUdseT0nfk8Y0fPjtdw2Wne
# jl6zLHuYXwcDau2O1DMuoiedNVjTF37UEmYT+oxC/OFXUGPDEQt9tzgbR9g8HLtU
# fEeWOsOED5xgb5rwyfvIss7H/cdHFcIiIczzQgYnsLyEGepoZDkKhSMR5eCB6Kcv
# /QIDAQABo4IBSTCCAUUwHQYDVR0OBBYEFDPhAYWS0oA+lOtITfjJtyl0knRRMB8G
# A1UdIwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8GA1UdHwRYMFYwVKBSoFCG
# Tmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY3Jvc29mdCUy
# MFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBsBggrBgEFBQcBAQRgMF4w
# XAYIKwYBBQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2Vy
# dHMvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3J0MAwG
# A1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwDgYDVR0PAQH/BAQD
# AgeAMA0GCSqGSIb3DQEBCwUAA4ICAQCXh+ckCkZaA06SNW+qxtS9gHQp4x7G+gdi
# kngKItEr8otkXIrmWPYrarRWBlY91lqGiilHyIlZ3iNBUbaNEmaKAGMZ5YcS7IZU
# KPaq1jU0msyl+8og0t9C/Z26+atx3vshHrFQuSgwTHZVpzv7k8CYnBYoxdhI1uGh
# qH595mqLvtMsxEN/1so7U+b3U6LCry5uwwcz5+j8Oj0GUX3b+iZg+As0xTN6T0Qa
# 8BNec/LwcyqYNEaMkW2VAKrmhvWH8OCDTcXgONnnABQHBfXK/fLAbHFGS1XNOtr6
# 2/iaHBGAkrCGl6Bi8Pfws6fs+w+sE9r3hX9Vg0gsRMoHRuMaiXsrGmGsuYnLn3Aw
# TguMatw9R8U5vJtWSlu1CFO5P0LEvQQiMZ12sQSsQAkNDTs9rTjVNjjIUgoZ6XPM
# xlcPIDcjxw8bfeb4y4wAxM2RRoWcxpkx+6IIf2L+b7gLHtBxXCWJ5bMW7WwUC2Ll
# tburUwBv0SgjpDtbEqw/uDgWBerCT+Zty3Nc967iGaQjyYQH6H/h9Xc8smm2n6Vj
# ySRx2swnW3hr6Qx63U/xY9HL6FNhrGiFED7ZRKrnwvvXvMVQUIEkB7GUEeN6heY8
# gHLt0jLV3yzDiQA8R8p5YGgGAVt9MEwgAJNY1iHvH/8vzhJSZFNkH8svRztO/i3T
# vKrjb8ZxwjCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkAAAAAABUwDQYJKoZI
# hvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw
2023-05-15 19:09:29 +07:00
# DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
2023-09-08 20:06:55 +07:00
# MjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAy
# MDEwMB4XDTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVowfDELMAkGA1UEBhMC
# VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV
# BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRp
# bWUtU3RhbXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC
# AQDk4aZM57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX9gF/bErg4r25Phdg
# M/9cT8dm95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1qUoNEt6aORmsHFPPF
# dvWGUNzBRMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8dq6z2Nr41JmTamDu6
# GnszrYBbfowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byNpOORj7I5LFGc6XBp
# Dco2LXCOMcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2krnopN6zL64NF50Zu
# yjLVwIYwXE8s4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4dPf0gz3N9QZpGdc3E
# XzTdEonW/aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgSUei/BQOj0XOmTTd0
# lBw0gg/wEPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8QmguEOqEUUbi0b1q
# GFphAXPKZ6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6CmgyFdXzB0kZSU2LlQ
# +QuJYfM2BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzFER1y7435UsSFF5PA
# PBXbGjfHCBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIurQIDAQABo4IB3TCCAdkw
# EgYJKwYBBAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQUKqdS/mTEmr6CkTxG
# NSnPEP8vBO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMFwGA1UdIARV
# MFMwUQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWlj
# cm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0bTATBgNVHSUEDDAK
# BggrBgEFBQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMC
# AYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvX
# zpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20v
# cGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYI
# KwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDANBgkqhkiG
# 9w0BAQsFAAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwUtj5OR2R4sQaTlz0x
# M7U518JxNj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN3Zi6th542DYunKmC
# VgADsAW+iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU5HhTdSRXud2f8449
# xvNo32X2pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5KYnDvBewVIVCs/wM
# nosZiefwC2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGyqVvfSaN0DLzskYDS
# PeZKPmY7T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB62FD+CljdQDzHVG2d
# Y3RILLFORy3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltEAY5aGZFrDZ+kKNxn
# GSgkujhLmm77IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFpAUR+fKFhbHP+Crvs
# QWY9af3LwUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcdFYmNcP7ntdAoGokL
# jzbaukz5m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRbatGePu1+oDEzfbzL
# 6Xu/OHBE0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQdVTNYs6FwZvKhggNN
# MIICNQIBATCB+aGB0aSBzjCByzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp
# bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw
# b3JhdGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2EgT3BlcmF0aW9uczEn
# MCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOkEwMDAtMDVFMC1EOTQ3MSUwIwYDVQQD
# ExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMKAQEwBwYFKw4DAhoDFQC8
# t8hT8KKUX91lU5FqRP9Cfu9MiaCBgzCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w
# IFBDQSAyMDEwMA0GCSqGSIb3DQEBCwUAAgUA6JpNUTAiGA8yMDIzMDgzMDIzMjA0
# OVoYDzIwMjMwODMxMjMyMDQ5WjB0MDoGCisGAQQBhFkKBAExLDAqMAoCBQDomk1R
# AgEAMAcCAQACAi/YMAcCAQACAhMgMAoCBQDom57RAgEAMDYGCisGAQQBhFkKBAIx
# KDAmMAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSChCjAIAgEAAgMBhqAwDQYJKoZI
# hvcNAQELBQADggEBABfAz49s/rKewqtgdD0cCiYtstIRDezA0cov48r4IjK4bRiH
# DU+sww2Y5Y8pyGpHHMRL7ywzc1atWMeL38qCAWKAkMWsjGKm0vDIVGkOA/Ty5Mey
# jimi1hmBXSQoZ2dE0Z0armM5nIeq3ugfEAQYtq4oJN8R7KwnAftIL5irLS3Jujkc
# YMbn6QIu6xqLC5DH3J+OnnCZCBTCrkqzQepkc6yw6Q6E6YeUJPW/ng/7ml2j0bee
# HAob/NBLbHydE35BR5zYddu74cgK/6ItPmvlHWSyGTNlX+arpenUF3LqQUXImm/k
# 2sCSHOMcFjlUftHyPboPubKfT52zOLwxNPKReVwxggQNMIIECQIBATCBkzB8MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNy
# b3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAdB3CKrvoxfG3QABAAAB0DAN
# BglghkgBZQMEAgEFAKCCAUowGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMC8G
# CSqGSIb3DQEJBDEiBCB1uaAhxt4skZpmIzqB8pxzfce/ihVfsRm97TkRn3KQ4DCB
# +gYLKoZIhvcNAQkQAi8xgeowgecwgeQwgb0EIAiVQAZftNP/Md1E2Yw+fBXa9w6f
# jmTZ5WAerrTSPwnXMIGYMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldh
# c2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBD
# b3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIw
# MTACEzMAAAHQdwiq76MXxt0AAQAAAdAwIgQgQ5NcGtl+/jKwjFgj8YxK+Ah4320D
# hGYNFu891nRIP3QwDQYJKoZIhvcNAQELBQAEggIAwsoBZ8/QAMcVMj5/0iCqF6a0
# MlUrAyJzFFbuebgMelr1XmRweuaok6BsKqZoeNSsPDAkvl8mAOZ75iTTFBBG8iou
# Sc3i9HQkZYmZ1scZU1kUSYBhgla9BYyU6E/6bvTVRpOakaRyxHEPGVn5i6TjwhIE
# wsnqkchyqDsrX2YSQVBLZr6LaoY1r2iYrsNehb7LRMv+RqPid6OEupO0spD26z91
# xcO2IIk8xXdTWELwWIQA1CwN9TYp8B6Do4wMvIwin4iaphtV8q8rnEY4wNPhNFJJ
# 1i8eR4yFbAF0feIEQThTR/CHN41yDQaENf9PcOQqyWNp5RkDhEicXXjTZAIZsQQd
# x8RsRSCASXL5ciaARozvvKB1pAE7bhAuoRdqk+m1w7OvHkY1pBDRR2RqQ+f3uWtL
# cjJkJlkolEORHTSPAOcCawBYUpJ6URkZGC0XxfC0pKwhK8UgrjQGxHzpUPgQvAwm
# wbz1ZeLqEiFzn4svjEqoWpPb4nuPzK8HWLvAlBWx5EDDofXFufOvV4PjRkZI1ReY
# NhDk5f0j+mi5fsmgJWJUDTvQ4kFINKgarrXs+zdmv1dhowLZhW5siIsaaUIt3Y8C
# ijKKd9b9RyH+DI1aSV4xFzOXnD7GAeSmaN9iwi+PUuNxpvB9O4VmmU4QvYySpc4p
# e5LhJLnu80XzASguiFQ=
2020-06-13 00:46:25 +07:00
# SIG # End signature block