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 "
}
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
try {
$Response = GetHTTPResponse -Uri $Source
$Stream = $Response . Content . ReadAsStreamAsync ( ) . Result
$File = [ System.IO.File ] :: Create ( $OutPath )
$Stream . CopyTo ( $File )
$File . Close ( )
}
finally {
2022-02-20 05:07:00 +07:00
if ( $null -ne $Stream ) {
2020-06-13 00:46:25 +07:00
$Stream . Dispose ( )
}
}
}
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. "
}
else
{
Say-Verbose " The temporary file `" $Path `" does not exist, therefore is not removed. "
}
}
catch
{
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 {
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 {
Say-Warning " Failed to check the disk space. Installation will continue, but it may fail if you do not have enough disk space. "
}
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-05-15 19:09:29 +07:00
# MIInvwYJKoZIhvcNAQcCoIInsDCCJ6wCAQExDzANBglghkgBZQMEAgEFADB5Bgor
2020-06-13 00:46:25 +07:00
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
2023-05-15 19:09:29 +07:00
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBhfTi3SRn7+vyy
# uCXKPjhiawegWZ493EcaOEycbgkZcKCCDXYwggX0MIID3KADAgECAhMzAAACy7d1
# OfsCcUI2AAAAAALLMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
2020-06-13 00:46:25 +07:00
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
2023-05-15 19:09:29 +07:00
# bmcgUENBIDIwMTEwHhcNMjIwNTEyMjA0NTU5WhcNMjMwNTExMjA0NTU5WjB0MQsw
2020-06-13 00:46:25 +07:00
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
2023-05-15 19:09:29 +07:00
# AQC3sN0WcdGpGXPZIb5iNfFB0xZ8rnJvYnxD6Uf2BHXglpbTEfoe+mO//oLWkRxA
# wppditsSVOD0oglKbtnh9Wp2DARLcxbGaW4YanOWSB1LyLRpHnnQ5POlh2U5trg4
# 3gQjvlNZlQB3lL+zrPtbNvMA7E0Wkmo+Z6YFnsf7aek+KGzaGboAeFO4uKZjQXY5
# RmMzE70Bwaz7hvA05jDURdRKH0i/1yK96TDuP7JyRFLOvA3UXNWz00R9w7ppMDcN
# lXtrmbPigv3xE9FfpfmJRtiOZQKd73K72Wujmj6/Su3+DBTpOq7NgdntW2lJfX3X
# a6oe4F9Pk9xRhkwHsk7Ju9E/AgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUrg/nt/gj+BBLd1jZWYhok7v5/w4w
# RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW
# MBQGA1UEBRMNMjMwMDEyKzQ3MDUyODAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci
# tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG
# CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu
# Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0
# MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAJL5t6pVjIRlQ8j4dAFJ
# ZnMke3rRHeQDOPFxswM47HRvgQa2E1jea2aYiMk1WmdqWnYw1bal4IzRlSVf4czf
# zx2vjOIOiaGllW2ByHkfKApngOzJmAQ8F15xSHPRvNMmvpC3PFLvKMf3y5SyPJxh
# 922TTq0q5epJv1SgZDWlUlHL/Ex1nX8kzBRhHvc6D6F5la+oAO4A3o/ZC05OOgm4
# EJxZP9MqUi5iid2dw4Jg/HvtDpCcLj1GLIhCDaebKegajCJlMhhxnDXrGFLJfX8j
# 7k7LUvrZDsQniJZ3D66K+3SZTLhvwK7dMGVFuUUJUfDifrlCTjKG9mxsPDllfyck
# 4zGnRZv8Jw9RgE1zAghnU14L0vVUNOzi/4bE7wIsiRyIcCcVoXRneBA3n/frLXvd
# jDsbb2lpGu78+s1zbO5N0bhHWq4j5WMutrspBxEhqG2PSBjC5Ypi+jhtfu3+x76N
# mBvsyKuxx9+Hm/ALnlzKxr4KyMR3/z4IRMzA1QyppNk65Ui+jB14g+w4vole33M1
# pVqVckrmSebUkmjnCshCiH12IFgHZF7gRwE4YZrJ7QjxZeoZqHaKsQLRMp653beB
# fHfeva9zJPhBSdVcCW7x9q0c2HVPLJHX9YCUU714I+qtLpDGrdbZxD9mikPqL/To
# /1lDZ0ch8FtePhME7houuoPcMIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq
# 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
# /Xmfwb1tbWrJUnMTDXpQzTGCGZ8wghmbAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp
# Z25pbmcgUENBIDIwMTECEzMAAALLt3U5+wJxQjYAAAAAAsswDQYJYIZIAWUDBAIB
# BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO
# MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIFmuaTXYQ37AFvsEol24fdW+
# nRqHcc1fr+VQVdqhXc/vMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A
# cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB
# BQAEggEAjY5XW5Ly7TJ1OTbeIR98xU+2dmtw7L71ws+ICnQCGhj2xJDUK+5yrTfO
# 8C98l/P4ynFi33Dl8z2YElqUCuqEXbiCzz06lIL4NuibC5DV/X80ZmICR/NYd2v1
# ww7IH+7dpsHAowBBindCYpVwQ3Ea3kDWgsjPAinAysFFushSOnNWFvrF6vi2smrs
# smbrAAhEhSfLd1Pxxdw73hQ0YjM/D3F3opaybMQ0blpHhOaqtbiyYzvk0doIzBEc
# trSH4NDIc3yLNj5VbjSczpexE+hyQNY4xCtwco4bVtXhONUihv08AIKR8+sIaI7A
# mM/SWrrwGYSSSxydKqDei7biKG4jDqGCFykwghclBgorBgEEAYI3AwMBMYIXFTCC
# FxEGCSqGSIb3DQEHAqCCFwIwghb+AgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFZBgsq
# hkiG9w0BCRABBKCCAUgEggFEMIIBQAIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl
# AwQCAQUABCB6Hzt2gUb/WZK8fvVnOocriE4rYr6mscZi3gZnBCpiigIGZBr2iMZU
# GBMyMDIzMDMzMTE1MjEwNi41MTZaMASAAgH0oIHYpIHVMIHSMQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJl
# bGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJjAkBgNVBAsTHVRoYWxlcyBUU1MgRVNO
# OjA4NDItNEJFNi1DMjlBMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBT
# ZXJ2aWNloIIReDCCBycwggUPoAMCAQICEzMAAAGybkADf26plJIAAQAAAbIwDQYJ
# KoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x
# EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv
# bjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwHhcNMjIw
# OTIwMjAyMjAxWhcNMjMxMjE0MjAyMjAxWjCB0jELMAkGA1UEBhMCVVMxEzARBgNV
2022-11-15 23:00:14 +07:00
# BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv
2023-05-15 19:09:29 +07:00
# c29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9zb2Z0IElyZWxhbmQgT3Bl
# cmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjowODQyLTRC
# RTYtQzI5QTElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZTCC
# AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMqiZTIde/lQ4rC+Bml5f/Wu
# q/xKTxrfbG23HofmQ+qZAN4GyO73PF3y9OAfpt7Qf2jcldWOGUB+HzBuwllYyP3f
# x4MY8zvuAuB37FvoytnNC2DKnVrVlHOVcGUL9CnmhDNMA2/nskjIf2IoiG9J0qLY
# r8duvHdQJ9Li2Pq9guySb9mvUL60ogslCO9gkh6FiEDwMrwUr8Wja6jFpUTny8tg
# 0N0cnCN2w4fKkp5qZcbUYFYicLSb/6A7pHCtX6xnjqwhmJoib3vkKJyVxbuFLRhV
# XxH95b0LHeNhifn3jvo2j+/4QV10jEpXVW+iC9BsTtR69xvTjU51ZgP7BR4YDEWq
# 7JsylSOv5B5THTDXRf184URzFhTyb8OZQKY7mqMh7c8J8w1sEM4XDUF2UZNy829N
# VCzG2tfdEXZaHxF8RmxpQYBxyhZwY1rotuIS+gfN2eq+hkAT3ipGn8/KmDwDtzAb
# nfuXjApgeZqwgcYJ8pDJ+y/xU6ouzJz1Bve5TTihkiA7wQsQe6R60Zk9dPdNzw0M
# K5niRzuQZAt4GI96FhjhlUWcUZOCkv/JXM/OGu/rgSplYwdmPLzzfDtXyuy/GCU5
# I4l08g6iifXypMgoYkkceOAAz4vx1x0BOnZWfI3fSwqNUvoN7ncTT+MB4Vpvf1QB
# ppjBAQUuvui6eCG0MCVNAgMBAAGjggFJMIIBRTAdBgNVHQ4EFgQUmfIngFzZEZlP
# kjDOVluBSDDaanEwHwYDVR0jBBgwFoAUn6cVXQBeYl2D9OXSZacbUzUZ6XIwXwYD
# VR0fBFgwVjBUoFKgUIZOaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9j
# cmwvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3JsMGwG
# CCsGAQUFBwEBBGAwXjBcBggrBgEFBQcwAoZQaHR0cDovL3d3dy5taWNyb3NvZnQu
# Y29tL3BraW9wcy9jZXJ0cy9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIw
# MjAxMCgxKS5jcnQwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcD
# CDAOBgNVHQ8BAf8EBAMCB4AwDQYJKoZIhvcNAQELBQADggIBANxHtu3FzIabaDbW
# qswdKBlAhKXRCN+5CSMiv2TYa4i2QuWIm+99piwAhDhADfbqor1zyLi95Y6GQnvI
# WUgdeC7oL1ZtZye92zYK+EIfwYZmhS+CH4infAzUvscHZF3wlrJUfPUIDGVP0lCY
# Vse9mguvG0dqkY4ayQPEHOvJubgZZaOdg/N8dInd6fGeOc+0DoGzB+LieObJ2Q0A
# tEt3XN3iX8Cp6+dZTX8xwE/LvhRwPpb/+nKshO7TVuvenwdTwqB/LT6CNPaElwFe
# KxKrqRTPMbHeg+i+KnBLfwmhEXsMg2s1QX7JIxfvT96md0eiMjiMEO22LbOzmLMN
# d3LINowAnRBAJtX+3/e390B9sMGMHp+a1V+hgs62AopBl0p/00li30DN5wEQ5If3
# 5Zk7b/T6pEx6rJUDYCti7zCbikjKTanBnOc99zGMlej5X+fC/k5ExUCrOs3/VzGR
# CZt5LvVQSdWqq/QMzTEmim4sbzASK9imEkjNtZZyvC1CsUcD1voFktld4mKMjE+u
# DEV3IddD+DrRk94nVzNPSuZXewfVOnXHSeqG7xM3V7fl2aL4v1OhL2+JwO1Tx3B0
# irO1O9qbNdJk355bntd1RSVKgM22KFBHnoL7Js7pRhBiaKmVTQGoOb+j1Qa7q+ci
# xGo48Vh9k35BDsJS/DLoXFSPDl4mMIIHcTCCBVmgAwIBAgITMwAAABXF52ueAptJ
# mQAAAAAAFTANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
# Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29m
# dCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNh
# dGUgQXV0aG9yaXR5IDIwMTAwHhcNMjEwOTMwMTgyMjI1WhcNMzAwOTMwMTgzMjI1
# WjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD
# Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDCCAiIwDQYJKoZIhvcNAQEB
# BQADggIPADCCAgoCggIBAOThpkzntHIhC3miy9ckeb0O1YLT/e6cBwfSqWxOdcjK
# NVf2AX9sSuDivbk+F2Az/1xPx2b3lVNxWuJ+Slr+uDZnhUYjDLWNE893MsAQGOhg
# fWpSg0S3po5GawcU88V29YZQ3MFEyHFcUTE3oAo4bo3t1w/YJlN8OWECesSq/XJp
# rx2rrPY2vjUmZNqYO7oaezOtgFt+jBAcnVL+tuhiJdxqD89d9P6OU8/W7IVWTe/d
# vI2k45GPsjksUZzpcGkNyjYtcI4xyDUoveO0hyTD4MmPfrVUj9z6BVWYbWg7mka9
# 7aSueik3rMvrg0XnRm7KMtXAhjBcTyziYrLNueKNiOSWrAFKu75xqRdbZ2De+JKR
# Hh09/SDPc31BmkZ1zcRfNN0Sidb9pSB9fvzZnkXftnIv231fgLrbqn427DZM9itu
# qBJR6L8FA6PRc6ZNN3SUHDSCD/AQ8rdHGO2n6Jl8P0zbr17C89XYcz1DTsEzOUyO
# ArxCaC4Q6oRRRuLRvWoYWmEBc8pnol7XKHYC4jMYctenIPDC+hIK12NvDMk2ZItb
# oKaDIV1fMHSRlJTYuVD5C4lh8zYGNRiER9vcG9H9stQcxWv2XFJRXRLbJbqvUAV6
# bMURHXLvjflSxIUXk8A8FdsaN8cIFRg/eKtFtvUeh17aj54WcmnGrnu3tz5q4i6t
# AgMBAAGjggHdMIIB2TASBgkrBgEEAYI3FQEEBQIDAQABMCMGCSsGAQQBgjcVAgQW
# BBQqp1L+ZMSavoKRPEY1Kc8Q/y8E7jAdBgNVHQ4EFgQUn6cVXQBeYl2D9OXSZacb
# UzUZ6XIwXAYDVR0gBFUwUzBRBgwrBgEEAYI3TIN9AQEwQTA/BggrBgEFBQcCARYz
# aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9Eb2NzL1JlcG9zaXRvcnku
# aHRtMBMGA1UdJQQMMAoGCCsGAQUFBwMIMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIA
# QwBBMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNX2
# VsuP6KJcYmjRPZSQW9fOmhjEMFYGA1UdHwRPME0wS6BJoEeGRWh0dHA6Ly9jcmwu
# bWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dF8yMDEw
# LTA2LTIzLmNybDBaBggrBgEFBQcBAQROMEwwSgYIKwYBBQUHMAKGPmh0dHA6Ly93
# d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYt
# MjMuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQCdVX38Kq3hLB9nATEkW+Geckv8qW/q
# XBS2Pk5HZHixBpOXPTEztTnXwnE2P9pkbHzQdTltuw8x5MKP+2zRoZQYIu7pZmc6
# U03dmLq2HnjYNi6cqYJWAAOwBb6J6Gngugnue99qb74py27YP0h1AdkY3m2CDPVt
# I1TkeFN1JFe53Z/zjj3G82jfZfakVqr3lbYoVSfQJL1AoL8ZthISEV09J+BAljis
# 9/kpicO8F7BUhUKz/AyeixmJ5/ALaoHCgRlCGVJ1ijbCHcNhcy4sa3tuPywJeBTp
# kbKpW99Jo3QMvOyRgNI95ko+ZjtPu4b6MhrZlvSP9pEB9s7GdP32THJvEKt1MMU0
# sHrYUP4KWN1APMdUbZ1jdEgssU5HLcEUBHG/ZPkkvnNtyo4JvbMBV0lUZNlz138e
# W0QBjloZkWsNn6Qo3GcZKCS6OEuabvshVGtqRRFHqfG3rsjoiV5PndLQTHa1V1QJ
# sWkBRH58oWFsc/4Ku+xBZj1p/cvBQUl+fpO+y/g75LcVv7TOPqUxUYS8vwLBgqJ7
# Fx0ViY1w/ue10CgaiQuPNtq6TPmb/wrpNPgkNWcr4A245oyZ1uEi6vAnQj0llOZ0
# dFtq0Z4+7X6gMTN9vMvpe784cETRkPHIqzqKOghif9lwY1NNje6CbaUFEMFxBmoQ
# tB1VM1izoXBm8qGCAtQwggI9AgEBMIIBAKGB2KSB1TCB0jELMAkGA1UEBhMCVVMx
# EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT
# FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9zb2Z0IElyZWxh
# bmQgT3BlcmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjow
# ODQyLTRCRTYtQzI5QTElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vy
# dmljZaIjCgEBMAcGBSsOAwIaAxUAjhJ+EeySRfn2KCNsjn9cF9AUSTqggYMwgYCk
# fjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD
# Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQUFAAIF
# AOfRUdUwIhgPMjAyMzAzMzEyMDM0MjlaGA8yMDIzMDQwMTIwMzQyOVowdDA6Bgor
# BgEEAYRZCgQBMSwwKjAKAgUA59FR1QIBADAHAgEAAgIKJDAHAgEAAgIRLzAKAgUA
# 59KjVQIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAID
# B6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBBQUAA4GBAJlOESCa/uRR1x6GunE8
# K/WgHWTpSE31EITDOfTMvDcF4ptngCS5aOc4gfzmhNNehWfP6EOrgoSQzJYZ4YCh
# fYbHNMk56f18sq8t7y2hgR7KixcEo/4HVzeSdaOclHNc4Gn7kCGpMvpT3Xz9Lzc7
# UKWDZ0zkNKnbS8TZLNueVQwfMYIEDTCCBAkCAQEwgZMwfDELMAkGA1UEBhMCVVMx
# EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT
# FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUt
# U3RhbXAgUENBIDIwMTACEzMAAAGybkADf26plJIAAQAAAbIwDQYJYIZIAWUDBAIB
# BQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0BCQQx
# IgQgXhJRuHCXk3arJvifIY3DBe9Ce9EmlP1y6U4XkgL31DkwgfoGCyqGSIb3DQEJ
# EAIvMYHqMIHnMIHkMIG9BCBTeM485+E+t4PEVieUoFKX7PVyLo/nzu+htJPCG04+
# NTCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw
# DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
# JjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAABsm5A
# A39uqZSSAAEAAAGyMCIEIGGWlnNnYHrB5HguWG0/nJd/WvSrCogze+QCpenu3IM5
# MA0GCSqGSIb3DQEBCwUABIICADVOLTuNxeEnBOfZpb7Nv4uf91W/Ho5i99zenDSJ
# x5QHVs+bKXmgc3a7/SSsliAT3zygHc7cH4zARbCZePLTivByKmeG08Ka35eyR+FK
# awSNrI/X+eVIC6nw/egCwviBC1NAG8jHGkuScbHeiiGajvS6lp3ORML7UexMuE4w
# 9SEumoghljCLZMwCSvw+3WxhQoBEZroR8u+PID2RdD0vi85FjKPWcZZijVLqHeFi
# TnuFqwRCLTV0MV+dDCbjwXneIqV+AVlnqb9iDMr3ZhISlRcy9XJNpY5vQBj/wqUW
# vefrmpdz0LNkdtXYThPkyl3mha2KsoQi5SA9zSjlAjFgY3ppmXvi3Frbfqk+iL+f
# l/Qc4+B71jG4t28lTWKteJiHqo+6AUXK2rlAl0d74yvhO6N8lMMtXhdJc8JABYn1
# v2/KKZn5RvPFF8QP7Ac1saIe1+gUFNcsYOLaMm/xl8E6kefWwZnm5Rhm606g1AC/
# N5Wo08aAs0ymTPH91dEbmOURXLbA3vCyG7kbfgnhCs/j7oQHWaFDzEYuXDIA4ICT
# dxPUTltbq3OWdp0PAS8JSEKPQFaOoQEnPa4adrXWxMvOmel8IGqJiQ+BPOaLQG64
# Qu2tMkH/5szb1fsEnCe8SJmy5ESF+kmpnLBtJ17Y9o+9nJHF5ddFmvzy+LUaIqDN
# cOfH
2020-06-13 00:46:25 +07:00
# SIG # End signature block