diff --git a/.agents/skills/bundle-dependency-prs/SKILL.md b/.agents/skills/bundle-dependency-prs/SKILL.md
new file mode 100644
index 0000000..3fec668
--- /dev/null
+++ b/.agents/skills/bundle-dependency-prs/SKILL.md
@@ -0,0 +1,46 @@
+---
+name: bundle-dependency-prs
+description: Fix broken dependency update PRs and aggregate the ones that work into one PR.
+disable-model-invocation: true
+---
+
+# Instructions
+
+You have two goals:
+
+1. Get all dependency PRs to a state where their PR checks pass.
+2. Aggregate dependency PRs with passing checks into just one PR.
+
+You can identify dependency update PRs by those authored by `dependabot` or `renovate`.
+
+You'll find instructions for building and validating the repo in the [CONTRIBUTING.md](../../../CONTRIBUTING.md) doc.
+Always validate your changes locally before pushing them to the remote repository.
+
+When writing PR bodies or comments, avoid unmatched markdown code fences. Keep markdown well-formed.
+
+For purposes of assessing PR readiness by its PR checks, consider docfx related checks to be irrelevant.
+If a docfx check fails but all other checks succeed, then that is a 'successful' dependency update PR.
+
+## Fix up dependency PRs with failing checks
+
+Before aggregating PRs, first try to fix any individual dependency update PRs with failing build/test checks.
+
+1. For the dependency PRs with failing build or test PR checks, check out their source branch and fix any issues.
+2. Push your fixes as fresh commits to the individual dependency PRs.
+ If pushing to a repo in the `microsoft` org when the PR is authored by `renovate`, follow-up the push with a PR comment that says exactly this: "/azp run" which triggers PR checks to re-run.
+3. If you can't fix a particular PR, add a comment to the PR describing your attempt and outcome.
+
+## Group dependency PRs that are ready to go
+
+Your next goal is to collect all the dependency updates that are ready to go into a single PR.
+
+1. Prepare a local branch called `bulkDepUpdates`.
+ 1. Consider that a remote branch by the same name may already exist. If it does, base your local branch on it.
+ 2. Merge `origin/main` into this branch.
+ 3. Resolve any conflicts.
+2. For the dependency PRs whose build and test PR checks already pass, merge them into the `bulkDepUpdates` branch.
+ Consider that your local branch may have already merged an equivalent PR in the past (from a past run). If so, you should skip merging that PR.
+ Resolve any conflicts.
+ Build and run tests to validate your branch.
+3. Push the branch.
+4. Create a PR, if one does not already exist.
diff --git a/.github/skills/update-library-template/SKILL.md b/.agents/skills/update-library-template/SKILL.md
similarity index 100%
rename from .github/skills/update-library-template/SKILL.md
rename to .agents/skills/update-library-template/SKILL.md
diff --git a/.github/skills/update-library-template/repo-merge-notes.md b/.agents/skills/update-library-template/repo-merge-notes.md
similarity index 100%
rename from .github/skills/update-library-template/repo-merge-notes.md
rename to .agents/skills/update-library-template/repo-merge-notes.md
diff --git a/.github/skills/update-library-template/template-release-notes.md b/.agents/skills/update-library-template/template-release-notes.md
similarity index 100%
rename from .github/skills/update-library-template/template-release-notes.md
rename to .agents/skills/update-library-template/template-release-notes.md
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index a5037f9..30cea5a 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,21 +3,21 @@
"isRoot": true,
"tools": {
"powershell": {
- "version": "7.6.2",
+ "version": "7.6.3",
"commands": [
"pwsh"
],
"rollForward": false
},
"dotnet-coverage": {
- "version": "18.7.0",
+ "version": "18.9.0",
"commands": [
"dotnet-coverage"
],
"rollForward": false
},
"nbgv": {
- "version": "3.9.50",
+ "version": "3.10.91",
"commands": [
"nbgv"
],
@@ -31,7 +31,7 @@
"rollForward": false
},
"nerdbank.dotnetrepotools": {
- "version": "1.4.1",
+ "version": "1.5.15",
"commands": [
"repo"
],
diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml
index 3c032fa..c0436e8 100644
--- a/.github/workflows/copilot-setup-steps.yml
+++ b/.github/workflows/copilot-setup-steps.yml
@@ -26,7 +26,7 @@ jobs:
# You can define any steps you want, and they will run before the agent starts.
# If you do not check out your code, Copilot will do this for you.
steps:
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0 # avoid shallow clone so nbgv can do its work.
- name: ⚙ Install prerequisites
diff --git a/.github/workflows/libtemplate-update.yml b/.github/workflows/libtemplate-update.yml
index 78cd2ed..44cec91 100644
--- a/.github/workflows/libtemplate-update.yml
+++ b/.github/workflows/libtemplate-update.yml
@@ -17,7 +17,7 @@ jobs:
contents: write
pull-requests: write
steps:
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0 # avoid shallow clone so nbgv can do its work.
diff --git a/.github/copilot-instructions.md b/AGENTS.md
similarity index 100%
rename from .github/copilot-instructions.md
rename to AGENTS.md
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ff095b4..231f105 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -45,7 +45,11 @@ This repository can be built on Windows, Linux, and OSX.
Building, testing, and packing this repository can be done by using the standard dotnet CLI commands (e.g. `dotnet build`, `dotnet test`, `dotnet pack`, etc.).
-[pwsh]: https://learn.microsoft.com/powershell/scripting/install/installing-powershell
+## Testing
+
+You can use `dotnet test` to build and/or test the repo.
+
+There may be tests that are known to be unstable or have special requirements. These can be avoided by running tests using the [dotnet-test-cloud.ps1](tools/dotnet-test-cloud.ps1) script *after* running `dotnet build`.
## Releases
@@ -102,3 +106,5 @@ git checkout origin/main
# resolve any conflicts, then commit the merge commit.
git push origin -u HEAD
```
+
+[pwsh]: https://learn.microsoft.com/powershell/scripting/install/installing-powershell
diff --git a/Directory.Packages.props b/Directory.Packages.props
index ecdfcc4..53d57bb 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -4,7 +4,7 @@
true
true
- 2.2.3
+ 2.3.2
2.0.226
@@ -17,9 +17,10 @@
-
+
+
@@ -33,7 +34,7 @@
-
+
diff --git a/azure-pipelines/archive-sourcecode.yml b/azure-pipelines/archive-sourcecode.yml
index 86aaad3..94f5ed3 100644
--- a/azure-pipelines/archive-sourcecode.yml
+++ b/azure-pipelines/archive-sourcecode.yml
@@ -52,7 +52,7 @@ extends:
pool:
name: AzurePipelines-EO
demands:
- - ImageOverride -equals 1ESPT-Ubuntu22.04
+ - ImageOverride -equals 1ESPT-Ubuntu24.04
os: Linux
steps:
diff --git a/azure-pipelines/libtemplate-update.yml b/azure-pipelines/libtemplate-update.yml
index 388338d..ef72abf 100644
--- a/azure-pipelines/libtemplate-update.yml
+++ b/azure-pipelines/libtemplate-update.yml
@@ -52,7 +52,7 @@ extends:
pool:
name: AzurePipelines-EO
demands:
- - ImageOverride -equals 1ESPT-Ubuntu22.04
+ - ImageOverride -equals 1ESPT-Ubuntu24.04
os: Linux
steps:
- checkout: self
diff --git a/azure-pipelines/official.yml b/azure-pipelines/official.yml
index 39427c6..2aa0842 100644
--- a/azure-pipelines/official.yml
+++ b/azure-pipelines/official.yml
@@ -81,7 +81,7 @@ extends:
linuxPool:
name: AzurePipelines-EO
demands:
- - ImageOverride -equals 1ESPT-Ubuntu22.04
+ - ImageOverride -equals 1ESPT-Ubuntu24.04
os: Linux
macOSPool:
name: Azure Pipelines
diff --git a/azure-pipelines/prepare-insertion-stages.yml b/azure-pipelines/prepare-insertion-stages.yml
index 9f934f9..9abd377 100644
--- a/azure-pipelines/prepare-insertion-stages.yml
+++ b/azure-pipelines/prepare-insertion-stages.yml
@@ -51,7 +51,7 @@ stages:
pool:
name: AzurePipelines-EO
demands:
- - ImageOverride -equals 1ESPT-Ubuntu22.04
+ - ImageOverride -equals 1ESPT-Ubuntu22.04 # Do NOT upgrade this job to Ubuntu24 until this is fixed: https://portal.microsofticm.com/imp/v5/incidents/details/830879871/summary
os: Linux
templateContext:
outputs:
diff --git a/azure-pipelines/release.yml b/azure-pipelines/release.yml
index 662c07e..243e890 100644
--- a/azure-pipelines/release.yml
+++ b/azure-pipelines/release.yml
@@ -38,7 +38,7 @@ extends:
pool:
name: AzurePipelines-EO
demands:
- - ImageOverride -equals 1ESPT-Ubuntu22.04
+ - ImageOverride -equals 1ESPT-Ubuntu22.04 # Do NOT upgrade this job to Ubuntu24 until this is fixed: https://portal.microsofticm.com/imp/v5/incidents/details/830879871/summary
os: Linux
templateContext:
outputs:
@@ -61,7 +61,7 @@ extends:
pool:
name: AzurePipelines-EO
demands:
- - ImageOverride -equals 1ESPT-Ubuntu22.04
+ - ImageOverride -equals 1ESPT-Ubuntu24.04
os: Linux
templateContext:
type: releaseJob
diff --git a/azure-pipelines/unofficial.yml b/azure-pipelines/unofficial.yml
index 313c918..6be04dc 100644
--- a/azure-pipelines/unofficial.yml
+++ b/azure-pipelines/unofficial.yml
@@ -94,7 +94,7 @@ extends:
linuxPool:
name: AzurePipelines-EO
demands:
- - ImageOverride -equals 1ESPT-Ubuntu22.04
+ - ImageOverride -equals 1ESPT-Ubuntu24.04
os: Linux
macOSPool:
name: Azure Pipelines
diff --git a/azure-pipelines/vs-insertion.yml b/azure-pipelines/vs-insertion.yml
index 2cbdb03..155f0b5 100644
--- a/azure-pipelines/vs-insertion.yml
+++ b/azure-pipelines/vs-insertion.yml
@@ -37,8 +37,8 @@ extends:
stages:
- stage: insertion
jobs:
- - job: insertion
- displayName: VS insertion
+ - job: upload
+ displayName: Upload VS payload
pool: VSEng-MicroBuildVSStable
templateContext:
outputParentDirectory: $(Pipeline.Workspace)/CI
@@ -64,6 +64,19 @@ extends:
packageParentPath: $(Pipeline.Workspace)/CI/VSInsertion-Windows
allowPackageConflicts: true
publishVstsFeed: VS
+
+ - job: insert
+ dependsOn: upload
+ displayName: VS insertion
+ pool: VSEngSS-MicroBuild2022-1ES
+ templateContext:
+ outputParentDirectory: $(Pipeline.Workspace)/CI
+ steps:
+ - checkout: none
+ - template: azure-pipelines/release-deployment-prep.yml@self
+ - download: CI
+ artifact: VSInsertion-Windows
+ displayName: 🔻 Download VSInsertion-Windows artifact
- task: MicroBuildInsertVsPayload@5
displayName: 🏭 Insert VS Payload
inputs:
diff --git a/azure-pipelines/vs-validation.yml b/azure-pipelines/vs-validation.yml
index 35aa140..d7e21bd 100644
--- a/azure-pipelines/vs-validation.yml
+++ b/azure-pipelines/vs-validation.yml
@@ -55,7 +55,7 @@ extends:
linuxPool:
name: AzurePipelines-EO
demands:
- - ImageOverride -equals 1ESPT-Ubuntu22.04
+ - ImageOverride -equals 1ESPT-Ubuntu24.04
os: Linux
macOSPool:
name: Azure Pipelines
@@ -73,8 +73,8 @@ extends:
- stage: insertion
displayName: VS insertion
jobs:
- - job: insertion
- displayName: VS insertion
+ - job: upload
+ displayName: Upload VS payload
pool: VSEng-MicroBuildVSStable
steps:
- checkout: self
@@ -102,6 +102,23 @@ extends:
packageParentPath: $(Pipeline.Workspace)/VSInsertion-Windows
allowPackageConflicts: true
publishVstsFeed: VS
+
+ - job: insertion
+ displayName: VS insertion
+ dependsOn: upload
+ pool: VSEngSS-MicroBuild2022-1ES
+ steps:
+ - checkout: self
+ clean: true
+ fetchDepth: 1
+ - download: current
+ artifact: Variables-Windows
+ displayName: 🔻 Download Variables-Windows artifact
+ - powershell: $(Pipeline.Workspace)/Variables-Windows/_define.ps1
+ displayName: ⚙️ Set pipeline variables based on artifacts
+ - download: current
+ artifact: VSInsertion-Windows
+ displayName: 🔻 Download VSInsertion-Windows artifact
- task: MicroBuildInsertVsPayload@5
displayName: 🏭 Insert VS Payload
inputs:
diff --git a/global.json b/global.json
index b3a3862..059462a 100644
--- a/global.json
+++ b/global.json
@@ -1,6 +1,6 @@
{
"sdk": {
- "version": "10.0.300",
+ "version": "10.0.302",
"rollForward": "patch",
"allowPrerelease": false
},
diff --git a/tools/GitHubActions.ps1 b/tools/GitHubActions.ps1
new file mode 100644
index 0000000..c732a0f
--- /dev/null
+++ b/tools/GitHubActions.ps1
@@ -0,0 +1,41 @@
+function Add-GitHubActionsEnvVariable {
+ param(
+ [string]$Path = $env:GITHUB_ENV,
+ [Parameter(Mandatory = $true)]
+ [string]$Name,
+ [Parameter(Mandatory = $true)]
+ [AllowEmptyString()]
+ [string]$Value
+ )
+
+ if ([string]::IsNullOrWhiteSpace($Path)) {
+ throw "GitHub Actions GITHUB_ENV file path must not be empty."
+ }
+
+ if ([string]::IsNullOrWhiteSpace($Name)) {
+ throw "GitHub Actions environment variable name must not be empty."
+ }
+
+ $utf8NoBom = [System.Text.UTF8Encoding]::new($false)
+ $delimiter = [guid]::NewGuid().ToString('N')
+ [System.IO.File]::AppendAllText($Path, "$Name<<$delimiter`n$Value`n$delimiter`n", $utf8NoBom)
+}
+
+function Add-GitHubActionsPath {
+ param(
+ [string]$Path = $env:GITHUB_PATH,
+ [Parameter(Mandatory = $true)]
+ [string]$Value
+ )
+
+ if ([string]::IsNullOrWhiteSpace($Path)) {
+ throw "GitHub Actions GITHUB_PATH file path must not be empty."
+ }
+
+ if ([string]::IsNullOrWhiteSpace($Value)) {
+ throw "GitHub Actions path entry must not be empty."
+ }
+
+ $utf8NoBom = [System.Text.UTF8Encoding]::new($false)
+ [System.IO.File]::AppendAllText($Path, "$Value`n", $utf8NoBom)
+}
diff --git a/tools/Install-DotNetSdk.ps1 b/tools/Install-DotNetSdk.ps1
index d9d35bb..bec37c9 100644
--- a/tools/Install-DotNetSdk.ps1
+++ b/tools/Install-DotNetSdk.ps1
@@ -284,18 +284,15 @@ if ($IncludeX86) {
}
if ($IsMacOS -or $IsLinux) {
- $DownloadUri = "https://raw.githubusercontent.com/dotnet/install-scripts/a3fbd0fd625032bac207f1f590e5353fe26faa59/src/dotnet-install.sh"
- $DotNetInstallScriptPath = "$DotNetInstallScriptRoot/dotnet-install.sh"
+ $DotNetInstallScriptPath = "$PSScriptRoot/dotnet-install.sh"
} else {
- $DownloadUri = "https://raw.githubusercontent.com/dotnet/install-scripts/a3fbd0fd625032bac207f1f590e5353fe26faa59/src/dotnet-install.ps1"
- $DotNetInstallScriptPath = "$DotNetInstallScriptRoot/dotnet-install.ps1"
+ $DotNetInstallScriptPath = "$PSScriptRoot/dotnet-install.ps1"
}
+# Verify the cached script exists
if (-not (Test-Path $DotNetInstallScriptPath)) {
- Invoke-WebRequest -Uri $DownloadUri -OutFile $DotNetInstallScriptPath -UseBasicParsing
- if ($IsMacOS -or $IsLinux) {
- chmod +x $DotNetInstallScriptPath
- }
+ Write-Error "Cached dotnet-install script not found at $DotNetInstallScriptPath. Run tools/Update-DotNetInstallScript.ps1 to download it."
+ exit 1
}
# In case the script we invoke is in a directory with spaces, wrap it with single quotes.
diff --git a/tools/Set-EnvVars.ps1 b/tools/Set-EnvVars.ps1
index 2bd3ca4..c85d694 100644
--- a/tools/Set-EnvVars.ps1
+++ b/tools/Set-EnvVars.ps1
@@ -12,13 +12,17 @@
The CmdEnvScriptPath environment variable may be optionally set to a path to a cmd shell script to be created (or appended to if it already exists) that will set the environment variables in cmd.exe that are set within the PowerShell environment.
This is used by init.cmd in order to reapply any new environment variables to the parent cmd.exe process that were set in the powershell child process.
#>
-[CmdletBinding(SupportsShouldProcess=$true)]
+[CmdletBinding(SupportsShouldProcess = $true)]
Param(
- [Parameter(Mandatory=$true, Position=1)]
+ [Parameter(Mandatory = $true, Position = 1)]
$Variables,
[string[]]$PrependPath
)
+if ($env:GITHUB_ACTIONS) {
+ . "$PSScriptRoot\GitHubActions.ps1"
+}
+
if ($Variables.Count -eq 0) {
return $true
}
@@ -27,7 +31,8 @@ $cmdInstructions = !$env:TF_BUILD -and !$env:GITHUB_ACTIONS -and !$env:CmdEnvScr
if ($cmdInstructions) {
Write-Warning "Environment variables have been set that will be lost because you're running under cmd.exe"
Write-Host "Environment variables that must be set manually:" -ForegroundColor Blue
-} else {
+}
+else {
Write-Host "Environment variables set:" -ForegroundColor Blue
Write-Host ($Variables | Out-String)
if ($PrependPath) {
@@ -44,7 +49,7 @@ if ($env:GITHUB_ACTIONS) {
}
$CmdEnvScript = ''
-$Variables.GetEnumerator() |% {
+$Variables.GetEnumerator() | % {
Set-Item -LiteralPath env:$($_.Key) -Value $_.Value
# If we're running in a cloud CI, set these environment variables so they propagate.
@@ -52,7 +57,7 @@ $Variables.GetEnumerator() |% {
Write-Host "##vso[task.setvariable variable=$($_.Key);]$($_.Value)"
}
if ($env:GITHUB_ACTIONS) {
- Add-Content -LiteralPath $env:GITHUB_ENV -Value "$($_.Key)=$($_.Value)"
+ Add-GitHubActionsEnvVariable -Name $_.Key -Value ([string]$_.Value)
}
if ($cmdInstructions) {
@@ -68,7 +73,7 @@ if ($IsMacOS -or $IsLinux) {
}
if ($PrependPath) {
- $PrependPath |% {
+ $PrependPath | % {
$newPathValue = "$_$pathDelimiter$env:PATH"
Set-Item -LiteralPath env:PATH -Value $newPathValue
if ($cmdInstructions) {
@@ -79,7 +84,7 @@ if ($PrependPath) {
Write-Host "##vso[task.prependpath]$_"
}
if ($env:GITHUB_ACTIONS) {
- Add-Content -LiteralPath $env:GITHUB_PATH -Value $_
+ Add-GitHubActionsPath -Value $_
}
$CmdEnvScript += "SET PATH=$_$pathDelimiter%PATH%"
diff --git a/tools/Update-DotNetInstallScript.ps1 b/tools/Update-DotNetInstallScript.ps1
new file mode 100644
index 0000000..bc0cba6
--- /dev/null
+++ b/tools/Update-DotNetInstallScript.ps1
@@ -0,0 +1,36 @@
+#!/usr/bin/env pwsh
+
+<#
+.SYNOPSIS
+ Updates the cached dotnet-install scripts from the dotnet/install-scripts GitHub repository.
+.DESCRIPTION
+ Downloads the latest dotnet-install.ps1 and dotnet-install.sh scripts from
+ https://github.com/dotnet/install-scripts and caches them locally to avoid GitHub API rate limiting.
+ Run this script periodically to get the latest installation scripts.
+#>
+[CmdletBinding(SupportsShouldProcess = $true)]
+Param()
+
+$ScriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
+$DownloadBaseUri = "https://raw.githubusercontent.com/dotnet/install-scripts/main/src"
+
+$scripts = @('dotnet-install.ps1', 'dotnet-install.sh')
+
+foreach ($script in $scripts) {
+ $Uri = "$DownloadBaseUri/$script"
+ $OutFile = Join-Path $ScriptRoot $script
+
+ Write-Host "Updating $script from GitHub..."
+ try {
+ if ($PSCmdlet.ShouldProcess($OutFile, "Update from $Uri")) {
+ Invoke-WebRequest -Uri $Uri -OutFile $OutFile -UseBasicParsing
+ Write-Host "✓ Successfully updated $script" -ForegroundColor Green
+ }
+ }
+ catch {
+ Write-Error "Failed to update ${script}: $_"
+ exit 1
+ }
+}
+
+Write-Host "All cached scripts have been updated." -ForegroundColor Green
diff --git a/tools/dotnet-install.ps1 b/tools/dotnet-install.ps1
new file mode 100644
index 0000000..e62264b
--- /dev/null
+++ b/tools/dotnet-install.ps1
@@ -0,0 +1,1356 @@
+#
+# 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.
+
+ Note that the intended use of this script is for Continuous Integration (CI) scenarios, where:
+ - The SDK needs to be installed without user interaction and without admin rights.
+ - The SDK installation doesn't need to persist across multiple CI runs.
+ To set up a development environment or to run apps, use installers rather than this script. Visit https://dotnet.microsoft.com/download to get the installer.
+
+.PARAMETER Channel
+ Default: LTS
+ Download from the Channel specified. Possible values:
+ - STS - the most recent Standard Term Support release
+ - LTS - the most recent Long Term Support release
+ - 2-part version in a format A.B - represents a specific release
+ examples: 2.0, 1.0
+ - 3-part version in a format A.B.Cxx - represents a specific SDK release
+ examples: 5.0.1xx, 5.0.2xx
+ Supported since 5.0 release
+ Warning: Value "Current" is deprecated for the Channel parameter. Use "STS" instead.
+ Note: The version parameter overrides the channel parameter when any version other than 'latest' is used.
+.PARAMETER Quality
+ Download the latest build of specified quality in the channel. The possible values are: daily, preview, GA.
+ Works only in combination with channel. Not applicable for STS and LTS channels and will be ignored if those channels are used.
+ 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.
+.PARAMETER Version
+ Default: latest
+ Represents a build version on specific channel. Possible values:
+ - latest - the latest build on specific channel
+ - 3-part version in a format A.B.C - represents specific version of build
+ examples: 2.0.0-preview2-006120, 1.1.0
+.PARAMETER Internal
+ Download internal builds. Requires providing credentials via -FeedCredential parameter.
+.PARAMETER FeedCredential
+ Token to access Azure feed. Used as a query string to append to the Azure feed.
+ This parameter typically is not specified.
+.PARAMETER InstallDir
+ Default: %LocalAppData%\Microsoft\dotnet
+ Path to where to install dotnet. Note that binaries will be placed directly in a given directory.
+.PARAMETER Architecture
+ Default: - this value represents currently running OS architecture
+ Architecture of dotnet binaries to be installed.
+ Possible values are: , amd64, x64, x86, arm64, arm
+.PARAMETER SharedRuntime
+ This parameter is obsolete and may be removed in a future version of this script.
+ The recommended alternative is '-Runtime dotnet'.
+ Installs just the shared runtime bits, not the entire SDK.
+.PARAMETER Runtime
+ Installs just a shared runtime, not the entire SDK.
+ Possible values:
+ - dotnet - the Microsoft.NETCore.App shared runtime
+ - aspnetcore - the Microsoft.AspNetCore.App shared runtime
+ - windowsdesktop - the Microsoft.WindowsDesktop.App shared runtime
+.PARAMETER DryRun
+ If set it will not perform installation but instead display what command line to use to consistently install
+ currently requested version of dotnet cli. In example if you specify version 'latest' it will display a link
+ with specific version so that this command can be used deterministically in a build script.
+ It also displays binaries location if you prefer to install or download it yourself.
+.PARAMETER NoPath
+ By default this script will set environment variable PATH for the current process to the binaries folder inside installation folder.
+ If set it will display binaries location but not set any environment variable.
+.PARAMETER Verbose
+ Displays diagnostics information.
+.PARAMETER AzureFeed
+ Default: https://builds.dotnet.microsoft.com/dotnet
+ For internal use only.
+ Allows using a different storage to download SDK archives from.
+.PARAMETER UncachedFeed
+ For internal use only.
+ Allows using a different storage to download SDK archives from.
+.PARAMETER ProxyAddress
+ If set, the installer will use the proxy when making web requests
+.PARAMETER ProxyUseDefaultCredentials
+ Default: false
+ Use default credentials, when using proxy address.
+.PARAMETER ProxyBypassList
+ If set with ProxyAddress, will provide the list of comma separated urls that will bypass the proxy
+.PARAMETER SkipNonVersionedFiles
+ Default: false
+ Skips installing non-versioned files if they already exist, such as dotnet.exe.
+.PARAMETER JSonFile
+ Determines the SDK version from a user specified global.json file
+ Note: global.json must have a value for 'SDK:Version'
+.PARAMETER DownloadTimeout
+ Determines timeout duration in seconds for downloading of the SDK file
+ Default: 1200 seconds (20 minutes)
+.PARAMETER KeepZip
+ If set, downloaded file is kept
+.PARAMETER ZipPath
+ Use that path to store installer, generated by default
+.EXAMPLE
+ dotnet-install.ps1 -Version 7.0.401
+ Installs the .NET SDK version 7.0.401
+.EXAMPLE
+ dotnet-install.ps1 -Channel 8.0 -Quality GA
+ Installs the latest GA (general availability) version of the .NET 8.0 SDK
+#>
+[cmdletbinding()]
+param(
+ [string]$Channel = "LTS",
+ [string]$Quality,
+ [string]$Version = "Latest",
+ [switch]$Internal,
+ [string]$JSonFile,
+ [Alias('i')][string]$InstallDir = "",
+ [string]$Architecture = "",
+ [string]$Runtime,
+ [Obsolete("This parameter may be removed in a future version of this script. The recommended alternative is '-Runtime dotnet'.")]
+ [switch]$SharedRuntime,
+ [switch]$DryRun,
+ [switch]$NoPath,
+ [string]$AzureFeed,
+ [string]$UncachedFeed,
+ [string]$FeedCredential,
+ [string]$ProxyAddress,
+ [switch]$ProxyUseDefaultCredentials,
+ [string[]]$ProxyBypassList = @(),
+ [switch]$SkipNonVersionedFiles,
+ [int]$DownloadTimeout = 1200,
+ [switch]$KeepZip,
+ [string]$ZipPath = [System.IO.Path]::combine([System.IO.Path]::GetTempPath(), [System.IO.Path]::GetRandomFileName()),
+ [switch]$Help
+)
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = "Stop"
+$ProgressPreference = "SilentlyContinue"
+
+function Say($str) {
+ try {
+ Write-Host "dotnet-install: $str"
+ }
+ catch {
+ # Some platforms cannot utilize Write-Host (Azure Functions, for instance). Fall back to Write-Output
+ Write-Output "dotnet-install: $str"
+ }
+}
+
+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 verbose 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"
+ }
+}
+
+function Say-Verbose($str) {
+ try {
+ Write-Verbose "dotnet-install: $str"
+ }
+ catch {
+ # Some platforms cannot utilize Write-Verbose (Azure Functions, for instance). Fall back to Write-Output
+ Write-Output "dotnet-install: $str"
+ }
+}
+
+function Measure-Action($name, $block) {
+ $time = Measure-Command $block
+ $totalSeconds = $time.TotalSeconds
+ Say-Verbose "Action '$name' took $totalSeconds seconds"
+}
+
+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
+}
+
+function Say-Invocation($Invocation) {
+ $command = $Invocation.MyCommand;
+ $args = (($Invocation.BoundParameters.Keys | foreach { "-$_ `"$($Invocation.BoundParameters[$_])`"" }) -join " ")
+ Say-Verbose "$command $args"
+}
+
+function Invoke-With-Retry([ScriptBlock]$ScriptBlock, [System.Threading.CancellationToken]$cancellationToken = [System.Threading.CancellationToken]::None, [int]$MaxAttempts = 3, [int]$SecondsBetweenAttempts = 1) {
+ $Attempts = 0
+ $local:startTime = $(get-date)
+
+ while ($true) {
+ try {
+ return & $ScriptBlock
+ }
+ catch {
+ $Attempts++
+ if (($Attempts -lt $MaxAttempts) -and -not $cancellationToken.IsCancellationRequested) {
+ Start-Sleep $SecondsBetweenAttempts
+ }
+ else {
+ $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;
+ }
+ }
+ }
+}
+
+function Get-Machine-Architecture() {
+ Say-Invocation $MyInvocation
+
+ # 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
+ if ( $ENV:PROCESSOR_ARCHITEW6432 -ne $null ) {
+ return $ENV:PROCESSOR_ARCHITEW6432
+ }
+
+ try {
+ if ( ((Get-CimInstance -ClassName CIM_OperatingSystem).OSArchitecture) -like "ARM*") {
+ if ( [Environment]::Is64BitOperatingSystem ) {
+ return "arm64"
+ }
+ return "arm"
+ }
+ }
+ catch {
+ # Machine doesn't support Get-CimInstance
+ }
+
+ return $ENV:PROCESSOR_ARCHITECTURE
+}
+
+function Get-CLIArchitecture-From-Architecture([string]$Architecture) {
+ Say-Invocation $MyInvocation
+
+ if ($Architecture -eq "") {
+ $Architecture = Get-Machine-Architecture
+ }
+
+ switch ($Architecture.ToLowerInvariant()) {
+ { ($_ -eq "amd64") -or ($_ -eq "x64") } { return "x64" }
+ { $_ -eq "x86" } { return "x86" }
+ { $_ -eq "arm" } { return "arm" }
+ { $_ -eq "arm64" } { return "arm64" }
+ default { throw "Architecture '$Architecture' not supported. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues" }
+ }
+}
+
+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
+ }
+
+ return $FeedCredential
+}
+function Get-NormalizedQuality([string]$Quality) {
+ Say-Invocation $MyInvocation
+
+ if ([string]::IsNullOrEmpty($Quality)) {
+ return ""
+ }
+
+ switch ($Quality) {
+ { @("daily", "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, 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 ""
+ }
+
+ if ($Channel.Contains("Current")) {
+ Say-Warning 'Value "Current" is deprecated for -Channel option. Use "STS" instead.'
+ }
+
+ 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" }
+ { $_ -eq "sts" } { return "STS" }
+ { $_ -eq "current" } { return "STS" }
+ 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." }
+ }
+}
+
+
+# The version text returned from the feeds is a 1-line or 2-line string:
+# For the SDK and the dotnet runtime (2 lines):
+# Line 1: # commit_hash
+# Line 2: # 4-part version
+# For the aspnetcore runtime (1 line):
+# Line 1: # 4-part version
+function Get-Version-From-LatestVersion-File-Content([string]$VersionText) {
+ Say-Invocation $MyInvocation
+
+ $Data = -split $VersionText
+
+ $VersionInfo = @{
+ CommitHash = $(if ($Data.Count -gt 1) { $Data[0] })
+ Version = $Data[-1] # last line is always the version number.
+ }
+ return $VersionInfo
+}
+
+function Load-Assembly([string] $Assembly) {
+ try {
+ Add-Type -Assembly $Assembly | Out-Null
+ }
+ catch {
+ # On Nano Server, Powershell Core Edition is used. Add-Type is unable to resolve base class assemblies because they are not GAC'd.
+ # Loading the base class assemblies is not unnecessary as the types will automatically get resolved.
+ }
+}
+
+function GetHTTPResponse([Uri] $Uri, [bool]$HeaderOnly, [bool]$DisableRedirect, [bool]$DisableFeedCredential) {
+ $cts = New-Object System.Threading.CancellationTokenSource
+
+ $downloadScript = {
+
+ $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))) {
+ if ($null -ne $DefaultProxy.GetProxy($Uri)) {
+ $ProxyAddress = $DefaultProxy.GetProxy($Uri).OriginalString
+ }
+ else {
+ $ProxyAddress = $null
+ }
+ $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...")
+ }
+ }
+
+ $HttpClientHandler = New-Object System.Net.Http.HttpClientHandler
+ if ($ProxyAddress) {
+ $HttpClientHandler.Proxy = New-Object System.Net.WebProxy -Property @{
+ Address = $ProxyAddress;
+ UseDefaultCredentials = $ProxyUseDefaultCredentials;
+ BypassList = $ProxyBypassList;
+ }
+ }
+ if ($DisableRedirect) {
+ $HttpClientHandler.AllowAutoRedirect = $false
+ }
+ $HttpClient = New-Object System.Net.Http.HttpClient -ArgumentList $HttpClientHandler
+
+ # Default timeout for HttpClient is 100s. For a 50 MB download this assumes 500 KB/s average, any less will time out
+ # Defaulting to 20 minutes allows it to work over much slower connections.
+ $HttpClient.Timeout = New-TimeSpan -Seconds $DownloadTimeout
+
+ 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");
+ $Response = $Task.GetAwaiter().GetResult();
+
+ if (($null -eq $Response) -or ((-not $HeaderOnly) -and (-not ($Response.IsSuccessStatusCode)))) {
+ # 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"]
+
+ if (404 -eq [int] $Response.StatusCode) {
+ $cts.Cancel()
+ }
+ }
+
+ throw $DownloadException
+ }
+
+ 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
+ }
+ finally {
+ if ($null -ne $HttpClient) {
+ $HttpClient.Dispose()
+ }
+ }
+ }
+
+ try {
+ return Invoke-With-Retry $downloadScript $cts.Token
+ }
+ finally {
+ if ($null -ne $cts) {
+ $cts.Dispose()
+ }
+ }
+}
+
+function Get-Version-From-LatestVersion-File([string]$AzureFeed, [string]$Channel) {
+ Say-Invocation $MyInvocation
+
+ $VersionFileUrl = $null
+ if ($Runtime -eq "dotnet") {
+ $VersionFileUrl = "$AzureFeed/Runtime/$Channel/latest.version"
+ }
+ elseif ($Runtime -eq "aspnetcore") {
+ $VersionFileUrl = "$AzureFeed/aspnetcore/Runtime/$Channel/latest.version"
+ }
+ elseif ($Runtime -eq "windowsdesktop") {
+ $VersionFileUrl = "$AzureFeed/WindowsDesktop/$Channel/latest.version"
+ }
+ elseif (-not $Runtime) {
+ $VersionFileUrl = "$AzureFeed/Sdk/$Channel/latest.version"
+ }
+ else {
+ throw "Invalid value for `$Runtime"
+ }
+
+ Say-Verbose "Constructed latest.version URL: $VersionFileUrl"
+
+ try {
+ $Response = GetHTTPResponse -Uri $VersionFileUrl
+ }
+ catch {
+ Say-Verbose "Failed to download latest.version file."
+ throw
+ }
+ $StringContent = $Response.Content.ReadAsStringAsync().Result
+
+ switch ($Response.Content.Headers.ContentType) {
+ { ($_ -eq "application/octet-stream") } { $VersionText = $StringContent }
+ { ($_ -eq "text/plain") } { $VersionText = $StringContent }
+ { ($_ -eq "text/plain; charset=UTF-8") } { $VersionText = $StringContent }
+ default { throw "``$Response.Content.Headers.ContentType`` is an unknown .version file content type." }
+ }
+
+ $VersionInfo = Get-Version-From-LatestVersion-File-Content $VersionText
+
+ 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 {
+ Say-Error "Json file unreadable: '$JSonFile'"
+ throw
+ }
+ if ($JSonContent) {
+ try {
+ $JSonContent.PSObject.Properties | ForEach-Object {
+ $PropertyName = $_.Name
+ if ($PropertyName -eq "version") {
+ $Version = $_.Value
+ Say-Verbose "Version = $Version"
+ }
+ }
+ }
+ catch {
+ Say-Error "Unable to parse the SDK node in '$JSonFile'"
+ throw
+ }
+ }
+ 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) {
+ if ($Version.ToLowerInvariant() -eq "latest") {
+ $LatestVersionInfo = Get-Version-From-LatestVersion-File -AzureFeed $AzureFeed -Channel $Channel
+ return $LatestVersionInfo.Version
+ }
+ else {
+ return $Version
+ }
+ }
+ else {
+ return Parse-Jsonfile-For-Version $JSonFile
+ }
+}
+
+function Get-Download-Link([string]$AzureFeed, [string]$SpecificVersion, [string]$CLIArchitecture) {
+ Say-Invocation $MyInvocation
+
+ # If anything fails in this lookup it will default to $SpecificVersion
+ $SpecificProductVersion = Get-Product-Version -AzureFeed $AzureFeed -SpecificVersion $SpecificVersion
+
+ if ($Runtime -eq "dotnet") {
+ $PayloadURL = "$AzureFeed/Runtime/$SpecificVersion/dotnet-runtime-$SpecificProductVersion-win-$CLIArchitecture.zip"
+ }
+ elseif ($Runtime -eq "aspnetcore") {
+ $PayloadURL = "$AzureFeed/aspnetcore/Runtime/$SpecificVersion/aspnetcore-runtime-$SpecificProductVersion-win-$CLIArchitecture.zip"
+ }
+ elseif ($Runtime -eq "windowsdesktop") {
+ # The windows desktop runtime is part of the core runtime layout prior to 5.0
+ $PayloadURL = "$AzureFeed/Runtime/$SpecificVersion/windowsdesktop-runtime-$SpecificProductVersion-win-$CLIArchitecture.zip"
+ if ($SpecificVersion -match '^(\d+)\.(.*)$') {
+ $majorVersion = [int]$Matches[1]
+ if ($majorVersion -ge 5) {
+ $PayloadURL = "$AzureFeed/WindowsDesktop/$SpecificVersion/windowsdesktop-runtime-$SpecificProductVersion-win-$CLIArchitecture.zip"
+ }
+ }
+ }
+ elseif (-not $Runtime) {
+ $PayloadURL = "$AzureFeed/Sdk/$SpecificVersion/dotnet-sdk-$SpecificProductVersion-win-$CLIArchitecture.zip"
+ }
+ else {
+ throw "Invalid value for `$Runtime"
+ }
+
+ Say-Verbose "Constructed primary named payload URL: $PayloadURL"
+
+ return $PayloadURL, $SpecificProductVersion
+}
+
+function Get-LegacyDownload-Link([string]$AzureFeed, [string]$SpecificVersion, [string]$CLIArchitecture) {
+ Say-Invocation $MyInvocation
+
+ if (-not $Runtime) {
+ $PayloadURL = "$AzureFeed/Sdk/$SpecificVersion/dotnet-dev-win-$CLIArchitecture.$SpecificVersion.zip"
+ }
+ elseif ($Runtime -eq "dotnet") {
+ $PayloadURL = "$AzureFeed/Runtime/$SpecificVersion/dotnet-win-$CLIArchitecture.$SpecificVersion.zip"
+ }
+ else {
+ return $null
+ }
+
+ Say-Verbose "Constructed legacy named payload URL: $PayloadURL"
+
+ return $PayloadURL
+}
+
+function Get-Product-Version([string]$AzureFeed, [string]$SpecificVersion, [string]$PackageDownloadLink) {
+ Say-Invocation $MyInvocation
+
+ # 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."
+ }
+ }
+ catch {
+ Say-Verbose "Could not read productVersion.txt at $productVersionTxtUrl (Exception: '$($_.Exception.Message)'. )"
+ }
+ }
+
+ # 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
+ }
+
+ $productVersion = Get-ProductVersionFromDownloadLink $PackageDownloadLink $SpecificVersion
+ return $productVersion
+}
+
+function Get-Product-Version-Url([string]$AzureFeed, [string]$SpecificVersion, [string]$PackageDownloadLink, [bool]$Flattened) {
+ Say-Invocation $MyInvocation
+
+ $majorVersion = $null
+ if ($SpecificVersion -match '^(\d+)\.(.*)') {
+ $majorVersion = $Matches[1] -as [int]
+ }
+
+ $pvFileName = 'productVersion.txt'
+ if ($Flattened) {
+ if (-not $Runtime) {
+ $pvFileName = 'sdk-productVersion.txt'
+ }
+ elseif ($Runtime -eq "dotnet") {
+ $pvFileName = 'runtime-productVersion.txt'
+ }
+ else {
+ $pvFileName = "$Runtime-productVersion.txt"
+ }
+ }
+
+ 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
+}
+
+function Get-User-Share-Path() {
+ Say-Invocation $MyInvocation
+
+ $InstallRoot = $env:DOTNET_INSTALL_DIR
+ if (!$InstallRoot) {
+ $InstallRoot = "$env:LocalAppData\Microsoft\dotnet"
+ }
+ elseif ($InstallRoot -like "$env:ProgramFiles\dotnet\?*") {
+ Say-Warning "The install root specified by the environment variable DOTNET_INSTALL_DIR points to the sub folder of $env:ProgramFiles\dotnet which is the default dotnet install root using .NET SDK installer. It is better to keep aligned with .NET SDK installer."
+ }
+ return $InstallRoot
+}
+
+function Resolve-Installation-Path([string]$InstallDir) {
+ Say-Invocation $MyInvocation
+
+ if ($InstallDir -eq "") {
+ return Get-User-Share-Path
+ }
+ return $InstallDir
+}
+
+function Test-User-Write-Access([string]$InstallDir) {
+ try {
+ $tempFileName = [guid]::NewGuid().ToString()
+ $tempFilePath = Join-Path -Path $InstallDir -ChildPath $tempFileName
+ New-Item -Path $tempFilePath -ItemType File -Force
+ Remove-Item $tempFilePath -Force
+ return $true
+ }
+ catch {
+ return $false
+ }
+}
+
+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) {
+ # example path with regex: shared/1.0.0-beta-12345/somepath
+ $match = [regex]::match($path, "/\d+\.\d+[^/]+/")
+ if ($match.Success) {
+ return $entry.FullName.Substring(0, $match.Index + $match.Length)
+ }
+
+ return $null
+}
+
+function Get-List-Of-Directories-And-Versions-To-Unpack-From-Dotnet-Package([System.IO.Compression.ZipArchive]$Zip, [string]$OutPath) {
+ Say-Invocation $MyInvocation
+
+ $ret = @()
+ foreach ($entry in $Zip.Entries) {
+ $dir = Get-Path-Prefix-With-Version $entry.FullName
+ if ($null -ne $dir) {
+ $path = Get-Absolute-Path $(Join-Path -Path $OutPath -ChildPath $dir)
+ if (-Not (Test-Path $path -PathType Container)) {
+ $ret += $dir
+ }
+ }
+ }
+
+ $ret = $ret | Sort-Object | Get-Unique
+
+ $values = ($ret | foreach { "$_" }) -join ";"
+ Say-Verbose "Directories to unpack: $values"
+
+ return $ret
+}
+
+# Example zip content and extraction algorithm:
+# Rule: files if extracted are always being extracted to the same relative path locally
+# .\
+# a.exe # file does not exist locally, extract
+# b.dll # file exists locally, override only if $OverrideFiles set
+# aaa\ # same rules as for files
+# ...
+# abc\1.0.0\ # directory contains version and exists locally
+# ... # do not extract content under versioned part
+# abc\asd\ # same rules as for files
+# ...
+# def\ghi\1.0.1\ # directory contains version and does not exist locally
+# ... # extract content
+function Extract-Dotnet-Package([string]$ZipPath, [string]$OutPath) {
+ Say-Invocation $MyInvocation
+
+ Load-Assembly -Assembly System.IO.Compression.FileSystem
+ Set-Variable -Name Zip
+ try {
+ $Zip = [System.IO.Compression.ZipFile]::OpenRead($ZipPath)
+
+ $DirectoriesToUnpack = Get-List-Of-Directories-And-Versions-To-Unpack-From-Dotnet-Package -Zip $Zip -OutPath $OutPath
+
+ foreach ($entry in $Zip.Entries) {
+ $PathWithVersion = Get-Path-Prefix-With-Version $entry.FullName
+ if (($null -eq $PathWithVersion) -Or ($DirectoriesToUnpack -contains $PathWithVersion)) {
+ $DestinationPath = Get-Absolute-Path $(Join-Path -Path $OutPath -ChildPath $entry.FullName)
+ $DestinationDir = Split-Path -Parent $DestinationPath
+ $OverrideFiles = $OverrideNonVersionedFiles -Or (-Not (Test-Path $DestinationPath))
+ if ((-Not $DestinationPath.EndsWith("\")) -And $OverrideFiles) {
+ New-Item -ItemType Directory -Force -Path $DestinationDir | Out-Null
+ [System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $DestinationPath, $OverrideNonVersionedFiles)
+ }
+ }
+ }
+ }
+ catch {
+ Say-Error "Failed to extract package. Exception: $_"
+ throw;
+ }
+ finally {
+ if ($null -ne $Zip) {
+ $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()
+
+ ValidateRemoteLocalFileSizes -LocalFileOutPath $OutPath -SourceUri $Source
+ }
+ finally {
+ if ($null -ne $Stream) {
+ $Stream.Dispose()
+ }
+ }
+}
+
+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."
+ }
+}
+
+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."
+ }
+}
+
+function Prepend-Sdk-InstallRoot-To-Path([string]$InstallRoot) {
+ $BinPath = Get-Absolute-Path $(Join-Path -Path $InstallRoot -ChildPath "")
+ 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"
+ }
+}
+
+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 `"`""
+ }
+ 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'"
+ }
+}
+
+function Get-AkaMSDownloadLink([string]$Channel, [string]$Quality, [bool]$Internal, [string]$Product, [string]$Architecture) {
+ Say-Invocation $MyInvocation
+
+ #quality is not supported for LTS or STS channel
+ if (![string]::IsNullOrEmpty($Quality) -and (@("LTS", "STS") -contains $Channel)) {
+ $Quality = ""
+ Say-Warning "Specifying quality for STS or LTS channel is not supported, the quality will be ignored."
+ }
+ 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
+
+}
+
+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
+
+ if ([string]::IsNullOrEmpty($AkaMsDownloadLink)) {
+ if (-not [string]::IsNullOrEmpty($NormalizedQuality)) {
+ # if quality is specified - exit with error - there is no fallback approach
+ Say-Error "Failed to locate the latest version in the channel '$NormalizedChannel' with '$NormalizedQuality' quality for '$ProductName', os: 'win', architecture: '$Architecture'."
+ 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."
+ return ($null, $null, $null)
+ }
+ 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
+ $pathParts = $AkaMsDownloadLink.Split('/')
+ if ($pathParts.Length -ge 2) {
+ $SpecificVersion = $pathParts[$pathParts.Length - 2]
+ Say-Verbose "Version: '$SpecificVersion'."
+ }
+ else {
+ Say-Error "Failed to extract the version from download link '$AkaMsDownloadLink'."
+ return ($null, $null, $null)
+ }
+
+ #retrieve effective (product) version
+ $EffectiveVersion = Get-Product-Version -SpecificVersion $SpecificVersion -PackageDownloadLink $AkaMsDownloadLink
+ Say-Verbose "Product version: '$EffectiveVersion'."
+
+ return ($AkaMsDownloadLink, $SpecificVersion, $EffectiveVersion);
+ }
+}
+
+function Get-Feeds-To-Use() {
+ $feeds = @(
+ "https://builds.dotnet.microsoft.com/dotnet"
+ "https://ci.dot.net/public"
+ )
+
+ if (-not [string]::IsNullOrEmpty($AzureFeed)) {
+ $feeds = @($AzureFeed)
+ }
+
+ if (-not [string]::IsNullOrEmpty($UncachedFeed)) {
+ $feeds = @($UncachedFeed)
+ }
+
+ Write-Verbose "Initialized feeds: $feeds"
+
+ return $feeds
+}
+
+function Resolve-AssetName-And-RelativePath([string] $Runtime) {
+
+ if ($Runtime -eq "dotnet") {
+ $assetName = ".NET Core Runtime"
+ $dotnetPackageRelativePath = "shared\Microsoft.NETCore.App"
+ }
+ elseif ($Runtime -eq "aspnetcore") {
+ $assetName = "ASP.NET Core Runtime"
+ $dotnetPackageRelativePath = "shared\Microsoft.AspNetCore.App"
+ }
+ elseif ($Runtime -eq "windowsdesktop") {
+ $assetName = ".NET Core Windows Desktop Runtime"
+ $dotnetPackageRelativePath = "shared\Microsoft.WindowsDesktop.App"
+ }
+ elseif (-not $Runtime) {
+ $assetName = ".NET Core SDK"
+ $dotnetPackageRelativePath = "sdk"
+ }
+ else {
+ throw "Invalid value for `$Runtime"
+ }
+
+ return ($assetName, $dotnetPackageRelativePath)
+}
+
+function Prepare-Install-Directory {
+ $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
+ }
+
+ 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 $diskSpaceWarning
+ }
+
+ # The check is relevant for PS version >= 7, the result can be irrelevant for older versions. See https://github.com/PowerShell/PowerShell/issues/12442.
+ if ( ($null -ne $diskInfo) -and ($diskInfo.Free / 1MB -le 100)) {
+ throw "There is not enough disk space on drive ${installDrive}:"
+ }
+}
+
+if ($Help) {
+ Get-Help $PSCommandPath -Examples
+ exit
+}
+
+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"
+
+if ($SharedRuntime -and (-not $Runtime)) {
+ $Runtime = "dotnet"
+}
+
+$OverrideNonVersionedFiles = !$SkipNonVersionedFiles
+
+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
+}
+
+$InstallRoot = Resolve-Installation-Path $InstallDir
+if (-not (Test-User-Write-Access $InstallRoot)) {
+ Say-Error "The current user doesn't have write access to the installation root '$InstallRoot' to install .NET. Please try specifying a different installation directory using the -InstallDir parameter, or ensure the selected directory has the appropriate permissions."
+ throw
+}
+Say-Verbose "InstallRoot: $InstallRoot"
+$ScriptName = $MyInvocation.MyCommand.Name
+($assetName, $dotnetPackageRelativePath) = Resolve-AssetName-And-RelativePath -Runtime $Runtime
+
+$feeds = Get-Feeds-To-Use
+$DownloadLinks = @()
+
+if ($Version.ToLowerInvariant() -ne "latest" -and -not [string]::IsNullOrEmpty($Quality)) {
+ 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."
+}
+
+# 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
+ }
+ }
+ }
+}
+
+# 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) {
+ 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: $_"
+ }
+ }
+}
+
+if ($DownloadLinks.count -eq 0) {
+ throw "Failed to resolve the exact version number."
+}
+
+if ($DryRun) {
+ PrintDryRunOutput $MyInvocation $DownloadLinks
+ return
+}
+
+Measure-Action "Installation directory preparation" { Prepare-Install-Directory }
+
+Say-Verbose "Zip path: $ZipPath"
+
+$DownloadSucceeded = $false
+$DownloadedLink = $null
+$ErrorMessages = @()
+
+foreach ($link in $DownloadLinks) {
+ Say-Verbose "Downloading `"$($link.type)`" link $($link.downloadLink)"
+
+ try {
+ Measure-Action "Package download" { DownloadFile -Source $link.downloadLink -OutPath $ZipPath }
+ Say-Verbose "Download succeeded."
+ $DownloadSucceeded = $true
+ $DownloadedLink = $link
+ break
+ }
+ catch {
+ $StatusCode = $null
+ $ErrorMessage = $null
+
+ 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
+ }
+
+ Say-Verbose "Download failed with status code $StatusCode. Error message: $ErrorMessage"
+ $ErrorMessages += "Downloading from `"$($link.type)`" link has failed with error:`nUri: $($link.downloadLink)`nStatusCode: $StatusCode`nError: $ErrorMessage"
+ }
+
+ # This link failed. Clean up before trying the next one.
+ SafeRemoveFile -Path $ZipPath
+}
+
+if (-not $DownloadSucceeded) {
+ foreach ($ErrorMessage in $ErrorMessages) {
+ Say-Error $ErrorMessages
+ }
+
+ throw "Could not find `"$assetName`" with version = $($DownloadLinks[0].effectiveVersion)`nRefer to: https://aka.ms/dotnet-os-lifecycle for information on .NET support"
+}
+
+Say "Extracting the archive."
+Measure-Action "Package extraction" { Extract-Dotnet-Package -ZipPath $ZipPath -OutPath $InstallRoot }
+
+# 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.
+if ($DownloadedLink.effectiveVersion -Match "rtm" -or $DownloadedLink.effectiveVersion -Match "servicing") {
+ $ReleaseVersion = $DownloadedLink.effectiveVersion.Split("-")[0]
+ 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) {
+ Say-Verbose "Checking installation: version = $($DownloadedLink.effectiveVersion)"
+ $isAssetInstalled = Is-Dotnet-Package-Installed -InstallRoot $InstallRoot -RelativePathToPackage $dotnetPackageRelativePath -SpecificVersion $DownloadedLink.effectiveVersion
+}
+
+# Version verification failed. More likely something is wrong either with the downloaded content or with the verification algorithm.
+if (!$isAssetInstalled) {
+ Say-Error "Failed to verify the version of installed `"$assetName`".`nInstallation source: $($DownloadedLink.downloadLink).`nInstallation location: $InstallRoot.`nReport the bug at https://github.com/dotnet/install-scripts/issues."
+ throw "`"$assetName`" with version = $($DownloadedLink.effectiveVersion) failed to install with an unknown error."
+}
+
+if (-not $KeepZip) {
+ SafeRemoveFile -Path $ZipPath
+}
+
+Measure-Action "Setting up shell environment" { Prepend-Sdk-InstallRoot-To-Path -InstallRoot $InstallRoot }
+
+Say "Note that the script does not ensure your Windows version is supported during the installation."
+Say "To check the list of supported versions, go to https://learn.microsoft.com/dotnet/core/install/windows#supported-versions"
+Say "Installed version is $($DownloadedLink.effectiveVersion)"
+Say "Installation finished"
diff --git a/tools/dotnet-install.sh b/tools/dotnet-install.sh
new file mode 100755
index 0000000..6180745
--- /dev/null
+++ b/tools/dotnet-install.sh
@@ -0,0 +1,1888 @@
+#!/usr/bin/env bash
+# Copyright (c) .NET Foundation and contributors. All rights reserved.
+# Licensed under the MIT license. See LICENSE file in the project root for full license information.
+#
+
+# Stop script on NZEC
+set -e
+# Stop script if unbound variable found (use ${var:-} if intentional)
+set -u
+# By default cmd1 | cmd2 returns exit code of cmd2 regardless of cmd1 success
+# This is causing it to fail
+set -o pipefail
+
+# Use in the the functions: eval $invocation
+invocation='say_verbose "Calling: ${yellow:-}${FUNCNAME[0]} ${green:-}$*${normal:-}"'
+
+# standard output may be used as a return value in the functions
+# we need a way to write text on the screen in the functions so that
+# it won't interfere with the return value.
+# Exposing stream 3 as a pipe to standard output of the script itself
+exec 3>&1
+
+# Setup some colors to use. These need to work in fairly limited shells, like the Ubuntu Docker container where there are only 8 colors.
+# See if stdout is a terminal
+if [ -t 1 ] && command -v tput > /dev/null; then
+ # see if it supports colors
+ ncolors=$(tput colors || echo 0)
+ if [ -n "$ncolors" ] && [ $ncolors -ge 8 ]; then
+ bold="$(tput bold || echo)"
+ normal="$(tput sgr0 || echo)"
+ black="$(tput setaf 0 || echo)"
+ red="$(tput setaf 1 || echo)"
+ green="$(tput setaf 2 || echo)"
+ yellow="$(tput setaf 3 || echo)"
+ blue="$(tput setaf 4 || echo)"
+ magenta="$(tput setaf 5 || echo)"
+ cyan="$(tput setaf 6 || echo)"
+ white="$(tput setaf 7 || echo)"
+ fi
+fi
+
+say_warning() {
+ printf "%b\n" "${yellow:-}dotnet_install: Warning: $1${normal:-}" >&3
+}
+
+say_err() {
+ printf "%b\n" "${red:-}dotnet_install: Error: $1${normal:-}" >&2
+}
+
+say() {
+ # using stream 3 (defined in the beginning) to not interfere with stdout of functions
+ # which may be used as return value
+ printf "%b\n" "${cyan:-}dotnet-install:${normal:-} $1" >&3
+}
+
+say_verbose() {
+ if [ "$verbose" = true ]; then
+ say "$1"
+ fi
+}
+
+# This platform list is finite - if the SDK/Runtime has supported Linux distribution-specific assets,
+# then and only then should the Linux distribution appear in this list.
+# Adding a Linux distribution to this list does not imply distribution-specific support.
+get_legacy_os_name_from_platform() {
+ eval $invocation
+
+ platform="$1"
+ case "$platform" in
+ "centos.7")
+ echo "centos"
+ return 0
+ ;;
+ "debian.8")
+ echo "debian"
+ return 0
+ ;;
+ "debian.9")
+ echo "debian.9"
+ return 0
+ ;;
+ "fedora.23")
+ echo "fedora.23"
+ return 0
+ ;;
+ "fedora.24")
+ echo "fedora.24"
+ return 0
+ ;;
+ "fedora.27")
+ echo "fedora.27"
+ return 0
+ ;;
+ "fedora.28")
+ echo "fedora.28"
+ return 0
+ ;;
+ "opensuse.13.2")
+ echo "opensuse.13.2"
+ return 0
+ ;;
+ "opensuse.42.1")
+ echo "opensuse.42.1"
+ return 0
+ ;;
+ "opensuse.42.3")
+ echo "opensuse.42.3"
+ return 0
+ ;;
+ "rhel.7"*)
+ echo "rhel"
+ return 0
+ ;;
+ "ubuntu.14.04")
+ echo "ubuntu"
+ return 0
+ ;;
+ "ubuntu.16.04")
+ echo "ubuntu.16.04"
+ return 0
+ ;;
+ "ubuntu.16.10")
+ echo "ubuntu.16.10"
+ return 0
+ ;;
+ "ubuntu.18.04")
+ echo "ubuntu.18.04"
+ return 0
+ ;;
+ "alpine.3.4.3")
+ echo "alpine"
+ return 0
+ ;;
+ esac
+ return 1
+}
+
+get_legacy_os_name() {
+ eval $invocation
+
+ local uname=$(uname)
+ if [ "$uname" = "Darwin" ]; then
+ echo "osx"
+ return 0
+ elif [ -n "$runtime_id" ]; then
+ echo $(get_legacy_os_name_from_platform "${runtime_id%-*}" || echo "${runtime_id%-*}")
+ return 0
+ else
+ if [ -e /etc/os-release ]; then
+ . /etc/os-release
+ os=$(get_legacy_os_name_from_platform "$ID${VERSION_ID:+.${VERSION_ID}}" || echo "")
+ if [ -n "$os" ]; then
+ echo "$os"
+ return 0
+ fi
+ fi
+ fi
+
+ say_verbose "Distribution specific OS name and version could not be detected: UName = $uname"
+ return 1
+}
+
+get_linux_platform_name() {
+ eval $invocation
+
+ if [ -n "$runtime_id" ]; then
+ echo "${runtime_id%-*}"
+ return 0
+ else
+ if [ -e /etc/os-release ]; then
+ . /etc/os-release
+ echo "$ID${VERSION_ID:+.${VERSION_ID}}"
+ return 0
+ elif [ -e /etc/redhat-release ]; then
+ local redhatRelease=$(&1 || true) | grep -q musl
+}
+
+get_current_os_name() {
+ eval $invocation
+
+ local uname=$(uname)
+ if [ "$uname" = "Darwin" ]; then
+ echo "osx"
+ return 0
+ elif [ "$uname" = "FreeBSD" ]; then
+ echo "freebsd"
+ return 0
+ elif [ "$uname" = "Linux" ]; then
+ local linux_platform_name=""
+ linux_platform_name="$(get_linux_platform_name)" || true
+
+ if [ "$linux_platform_name" = "rhel.6" ]; then
+ echo $linux_platform_name
+ return 0
+ elif is_musl_based_distro; then
+ echo "linux-musl"
+ return 0
+ elif [ "$linux_platform_name" = "linux-musl" ]; then
+ echo "linux-musl"
+ return 0
+ else
+ echo "linux"
+ return 0
+ fi
+ fi
+
+ say_err "OS name could not be detected: UName = $uname"
+ return 1
+}
+
+machine_has() {
+ eval $invocation
+
+ command -v "$1" > /dev/null 2>&1
+ return $?
+}
+
+check_min_reqs() {
+ local hasMinimum=false
+ if machine_has "curl"; then
+ hasMinimum=true
+ elif machine_has "wget"; then
+ hasMinimum=true
+ fi
+
+ if [ "$hasMinimum" = "false" ]; then
+ say_err "curl (recommended) or wget are required to download dotnet. Install missing prerequisite to proceed."
+ return 1
+ fi
+ return 0
+}
+
+# args:
+# input - $1
+to_lowercase() {
+ #eval $invocation
+
+ echo "$1" | tr '[:upper:]' '[:lower:]'
+ return 0
+}
+
+# args:
+# input - $1
+remove_trailing_slash() {
+ #eval $invocation
+
+ local input="${1:-}"
+ echo "${input%/}"
+ return 0
+}
+
+# args:
+# input - $1
+remove_beginning_slash() {
+ #eval $invocation
+
+ local input="${1:-}"
+ echo "${input#/}"
+ return 0
+}
+
+# args:
+# root_path - $1
+# child_path - $2 - this parameter can be empty
+combine_paths() {
+ eval $invocation
+
+ # TODO: Consider making it work with any number of paths. For now:
+ if [ ! -z "${3:-}" ]; then
+ say_err "combine_paths: Function takes two parameters."
+ return 1
+ fi
+
+ local root_path="$(remove_trailing_slash "$1")"
+ local child_path="$(remove_beginning_slash "${2:-}")"
+ say_verbose "combine_paths: root_path=$root_path"
+ say_verbose "combine_paths: child_path=$child_path"
+ echo "$root_path/$child_path"
+ return 0
+}
+
+get_machine_architecture() {
+ eval $invocation
+
+ if command -v uname > /dev/null; then
+ CPUName=$(uname -m)
+ case $CPUName in
+ armv1*|armv2*|armv3*|armv4*|armv5*|armv6*)
+ echo "armv6-or-below"
+ return 0
+ ;;
+ armv*l)
+ echo "arm"
+ return 0
+ ;;
+ aarch64|arm64)
+ if [ "$(getconf LONG_BIT)" -lt 64 ]; then
+ # This is 32-bit OS running on 64-bit CPU (for example Raspberry Pi OS)
+ echo "arm"
+ return 0
+ fi
+ echo "arm64"
+ return 0
+ ;;
+ s390x)
+ echo "s390x"
+ return 0
+ ;;
+ ppc64le)
+ echo "ppc64le"
+ return 0
+ ;;
+ loongarch64)
+ echo "loongarch64"
+ return 0
+ ;;
+ riscv64)
+ echo "riscv64"
+ return 0
+ ;;
+ powerpc|ppc)
+ echo "ppc"
+ return 0
+ ;;
+ esac
+ fi
+
+ # Always default to 'x64'
+ echo "x64"
+ return 0
+}
+
+# args:
+# architecture - $1
+get_normalized_architecture_from_architecture() {
+ eval $invocation
+
+ local architecture="$(to_lowercase "$1")"
+
+ if [[ $architecture == \ ]]; then
+ machine_architecture="$(get_machine_architecture)"
+ if [[ "$machine_architecture" == "armv6-or-below" ]]; then
+ say_err "Architecture \`$machine_architecture\` not supported. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues"
+ return 1
+ fi
+
+ echo $machine_architecture
+ return 0
+ fi
+
+ case "$architecture" in
+ amd64|x64)
+ echo "x64"
+ return 0
+ ;;
+ arm)
+ echo "arm"
+ return 0
+ ;;
+ arm64)
+ echo "arm64"
+ return 0
+ ;;
+ s390x)
+ echo "s390x"
+ return 0
+ ;;
+ ppc64le)
+ echo "ppc64le"
+ return 0
+ ;;
+ loongarch64)
+ echo "loongarch64"
+ return 0
+ ;;
+ esac
+
+ say_err "Architecture \`$architecture\` not supported. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues"
+ return 1
+}
+
+# args:
+# version - $1
+# channel - $2
+# architecture - $3
+get_normalized_architecture_for_specific_sdk_version() {
+ eval $invocation
+
+ local is_version_support_arm64="$(is_arm64_supported "$1")"
+ local is_channel_support_arm64="$(is_arm64_supported "$2")"
+ local architecture="$3";
+ local osname="$(get_current_os_name)"
+
+ if [ "$osname" == "osx" ] && [ "$architecture" == "arm64" ] && { [ "$is_version_support_arm64" = false ] || [ "$is_channel_support_arm64" = false ]; }; then
+ #check if rosetta is installed
+ if [ "$(/usr/bin/pgrep oahd >/dev/null 2>&1;echo $?)" -eq 0 ]; then
+ say_verbose "Changing user architecture from '$architecture' to 'x64' because .NET SDKs prior to version 6.0 do not support arm64."
+ echo "x64"
+ return 0;
+ else
+ say_err "Architecture \`$architecture\` is not supported for .NET SDK version \`$version\`. Please install Rosetta to allow emulation of the \`$architecture\` .NET SDK on this platform"
+ return 1
+ fi
+ fi
+
+ echo "$architecture"
+ return 0
+}
+
+# args:
+# version or channel - $1
+is_arm64_supported() {
+ # Extract the major version by splitting on the dot
+ major_version="${1%%.*}"
+
+ # Check if the major version is a valid number and less than 6
+ case "$major_version" in
+ [0-9]*)
+ if [ "$major_version" -lt 6 ]; then
+ echo false
+ return 0
+ fi
+ ;;
+ esac
+
+ echo true
+ return 0
+}
+
+# args:
+# user_defined_os - $1
+get_normalized_os() {
+ eval $invocation
+
+ local osname="$(to_lowercase "$1")"
+ if [ ! -z "$osname" ]; then
+ case "$osname" in
+ osx | freebsd | rhel.6 | linux-musl | linux)
+ echo "$osname"
+ return 0
+ ;;
+ macos)
+ osname='osx'
+ echo "$osname"
+ return 0
+ ;;
+ *)
+ say_err "'$user_defined_os' is not a supported value for --os option, supported values are: osx, macos, linux, linux-musl, freebsd, rhel.6. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues."
+ return 1
+ ;;
+ esac
+ else
+ osname="$(get_current_os_name)" || return 1
+ fi
+ echo "$osname"
+ return 0
+}
+
+# args:
+# quality - $1
+get_normalized_quality() {
+ eval $invocation
+
+ local quality="$(to_lowercase "$1")"
+ if [ ! -z "$quality" ]; then
+ case "$quality" in
+ daily | preview)
+ echo "$quality"
+ return 0
+ ;;
+ ga)
+ #ga quality is available without specifying quality, so normalizing it to empty
+ return 0
+ ;;
+ *)
+ say_err "'$quality' is not a supported value for --quality option. Supported values are: daily, preview, ga. If you think this is a bug, report it at https://github.com/dotnet/install-scripts/issues."
+ return 1
+ ;;
+ esac
+ fi
+ return 0
+}
+
+# args:
+# channel - $1
+get_normalized_channel() {
+ eval $invocation
+
+ local channel="$(to_lowercase "$1")"
+
+ if [[ $channel == current ]]; then
+ say_warning 'Value "Current" is deprecated for -Channel option. Use "STS" instead.'
+ fi
+
+ if [[ $channel == release/* ]]; then
+ 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.';
+ fi
+
+ if [ ! -z "$channel" ]; then
+ case "$channel" in
+ lts)
+ echo "LTS"
+ return 0
+ ;;
+ sts)
+ echo "STS"
+ return 0
+ ;;
+ current)
+ echo "STS"
+ return 0
+ ;;
+ *)
+ echo "$channel"
+ return 0
+ ;;
+ esac
+ fi
+
+ return 0
+}
+
+# args:
+# runtime - $1
+get_normalized_product() {
+ eval $invocation
+
+ local product=""
+ local runtime="$(to_lowercase "$1")"
+ if [[ "$runtime" == "dotnet" ]]; then
+ product="dotnet-runtime"
+ elif [[ "$runtime" == "aspnetcore" ]]; then
+ product="aspnetcore-runtime"
+ elif [ -z "$runtime" ]; then
+ product="dotnet-sdk"
+ fi
+ echo "$product"
+ return 0
+}
+
+# The version text returned from the feeds is a 1-line or 2-line string:
+# For the SDK and the dotnet runtime (2 lines):
+# Line 1: # commit_hash
+# Line 2: # 4-part version
+# For the aspnetcore runtime (1 line):
+# Line 1: # 4-part version
+
+# args:
+# version_text - stdin
+get_version_from_latestversion_file_content() {
+ eval $invocation
+
+ cat | tail -n 1 | sed 's/\r$//'
+ return 0
+}
+
+# args:
+# install_root - $1
+# relative_path_to_package - $2
+# specific_version - $3
+is_dotnet_package_installed() {
+ eval $invocation
+
+ local install_root="$1"
+ local relative_path_to_package="$2"
+ local specific_version="${3//[$'\t\r\n']}"
+
+ local dotnet_package_path="$(combine_paths "$(combine_paths "$install_root" "$relative_path_to_package")" "$specific_version")"
+ say_verbose "is_dotnet_package_installed: dotnet_package_path=$dotnet_package_path"
+
+ if [ -d "$dotnet_package_path" ]; then
+ return 0
+ else
+ return 1
+ fi
+}
+
+# args:
+# downloaded file - $1
+# remote_file_size - $2
+validate_remote_local_file_sizes()
+{
+ eval $invocation
+
+ local downloaded_file="$1"
+ local remote_file_size="$2"
+ local file_size=''
+
+ if [[ "$OSTYPE" == "linux-gnu"* ]]; then
+ file_size="$(stat -c '%s' "$downloaded_file")"
+ elif [[ "$OSTYPE" == "darwin"* ]]; then
+ # hardcode in order to avoid conflicts with GNU stat
+ file_size="$(/usr/bin/stat -f '%z' "$downloaded_file")"
+ fi
+
+ if [ -n "$file_size" ]; then
+ say "Downloaded file size is $file_size bytes."
+
+ if [ -n "$remote_file_size" ] && [ -n "$file_size" ]; then
+ if [ "$remote_file_size" -ne "$file_size" ]; then
+ say "The remote and local file sizes are not equal. The remote file size is $remote_file_size bytes and the local size is $file_size bytes. The local package may be corrupted."
+ else
+ say "The remote and local file sizes are equal."
+ fi
+ fi
+
+ else
+ say "Either downloaded or local package size can not be measured. One of them may be corrupted."
+ fi
+}
+
+# args:
+# azure_feed - $1
+# channel - $2
+# normalized_architecture - $3
+get_version_from_latestversion_file() {
+ eval $invocation
+
+ local azure_feed="$1"
+ local channel="$2"
+ local normalized_architecture="$3"
+
+ local version_file_url=null
+ if [[ "$runtime" == "dotnet" ]]; then
+ version_file_url="$azure_feed/Runtime/$channel/latest.version"
+ elif [[ "$runtime" == "aspnetcore" ]]; then
+ version_file_url="$azure_feed/aspnetcore/Runtime/$channel/latest.version"
+ elif [ -z "$runtime" ]; then
+ version_file_url="$azure_feed/Sdk/$channel/latest.version"
+ else
+ say_err "Invalid value for \$runtime"
+ return 1
+ fi
+ say_verbose "get_version_from_latestversion_file: latest url: $version_file_url"
+
+ download "$version_file_url" || return $?
+ return 0
+}
+
+# args:
+# json_file - $1
+parse_globaljson_file_for_version() {
+ eval $invocation
+
+ local json_file="$1"
+ if [ ! -f "$json_file" ]; then
+ say_err "Unable to find \`$json_file\`"
+ return 1
+ fi
+
+ sdk_section=$(cat "$json_file" | tr -d "\r" | awk '/"sdk"/,/}/')
+ if [ -z "$sdk_section" ]; then
+ say_err "Unable to parse the SDK node in \`$json_file\`"
+ return 1
+ fi
+
+ sdk_list=$(echo $sdk_section | awk -F"[{}]" '{print $2}')
+ sdk_list=${sdk_list//[\" ]/}
+ sdk_list=${sdk_list//,/$'\n'}
+
+ local version_info=""
+ while read -r line; do
+ IFS=:
+ while read -r key value; do
+ if [[ "$key" == "version" ]]; then
+ version_info=$value
+ fi
+ done <<< "$line"
+ done <<< "$sdk_list"
+ if [ -z "$version_info" ]; then
+ say_err "Unable to find the SDK:version node in \`$json_file\`"
+ return 1
+ fi
+
+ unset IFS;
+ echo "$version_info"
+ return 0
+}
+
+# args:
+# azure_feed - $1
+# channel - $2
+# normalized_architecture - $3
+# version - $4
+# json_file - $5
+get_specific_version_from_version() {
+ eval $invocation
+
+ local azure_feed="$1"
+ local channel="$2"
+ local normalized_architecture="$3"
+ local version="$(to_lowercase "$4")"
+ local json_file="$5"
+
+ if [ -z "$json_file" ]; then
+ if [[ "$version" == "latest" ]]; then
+ local version_info
+ version_info="$(get_version_from_latestversion_file "$azure_feed" "$channel" "$normalized_architecture" false)" || return 1
+ say_verbose "get_specific_version_from_version: version_info=$version_info"
+ echo "$version_info" | get_version_from_latestversion_file_content
+ return 0
+ else
+ echo "$version"
+ return 0
+ fi
+ else
+ local version_info
+ version_info="$(parse_globaljson_file_for_version "$json_file")" || return 1
+ echo "$version_info"
+ return 0
+ fi
+}
+
+# args:
+# azure_feed - $1
+# channel - $2
+# normalized_architecture - $3
+# specific_version - $4
+# normalized_os - $5
+construct_download_link() {
+ eval $invocation
+
+ local azure_feed="$1"
+ local channel="$2"
+ local normalized_architecture="$3"
+ local specific_version="${4//[$'\t\r\n']}"
+ local specific_product_version="$(get_specific_product_version "$1" "$4")"
+ local osname="$5"
+
+ local download_link=null
+ if [[ "$runtime" == "dotnet" ]]; then
+ download_link="$azure_feed/Runtime/$specific_version/dotnet-runtime-$specific_product_version-$osname-$normalized_architecture.tar.gz"
+ elif [[ "$runtime" == "aspnetcore" ]]; then
+ download_link="$azure_feed/aspnetcore/Runtime/$specific_version/aspnetcore-runtime-$specific_product_version-$osname-$normalized_architecture.tar.gz"
+ elif [ -z "$runtime" ]; then
+ download_link="$azure_feed/Sdk/$specific_version/dotnet-sdk-$specific_product_version-$osname-$normalized_architecture.tar.gz"
+ else
+ return 1
+ fi
+
+ echo "$download_link"
+ return 0
+}
+
+# args:
+# azure_feed - $1
+# specific_version - $2
+# download link - $3 (optional)
+get_specific_product_version() {
+ # If we find a 'productVersion.txt' at the root of any folder, we'll use its contents
+ # to resolve the version of what's in the folder, superseding the specified version.
+ # if 'productVersion.txt' is missing but download link is already available, product version will be taken from download link
+ eval $invocation
+
+ local azure_feed="$1"
+ local specific_version="${2//[$'\t\r\n']}"
+ local package_download_link=""
+ if [ $# -gt 2 ]; then
+ local package_download_link="$3"
+ fi
+ local specific_product_version=null
+
+ # Try to get the version number, using the productVersion.txt file located next to the installer file.
+ local download_links=($(get_specific_product_version_url "$azure_feed" "$specific_version" true "$package_download_link")
+ $(get_specific_product_version_url "$azure_feed" "$specific_version" false "$package_download_link"))
+
+ for download_link in "${download_links[@]}"
+ do
+ say_verbose "Checking for the existence of $download_link"
+
+ if machine_has "curl"
+ then
+ if ! specific_product_version=$(curl -s --fail "${download_link}${feed_credential}" 2>&1); then
+ continue
+ else
+ echo "${specific_product_version//[$'\t\r\n']}"
+ return 0
+ fi
+
+ elif machine_has "wget"
+ then
+ specific_product_version=$(wget -qO- "${download_link}${feed_credential}" 2>&1)
+ if [ $? = 0 ]; then
+ echo "${specific_product_version//[$'\t\r\n']}"
+ return 0
+ fi
+ fi
+ done
+
+ # Getting the version number with productVersion.txt has failed. Try parsing the download link for a version number.
+ say_verbose "Failed to get the version using productVersion.txt file. Download link will be parsed instead."
+ specific_product_version="$(get_product_specific_version_from_download_link "$package_download_link" "$specific_version")"
+ echo "${specific_product_version//[$'\t\r\n']}"
+ return 0
+}
+
+# args:
+# azure_feed - $1
+# specific_version - $2
+# is_flattened - $3
+# download link - $4 (optional)
+get_specific_product_version_url() {
+ eval $invocation
+
+ local azure_feed="$1"
+ local specific_version="$2"
+ local is_flattened="$3"
+ local package_download_link=""
+ if [ $# -gt 3 ]; then
+ local package_download_link="$4"
+ fi
+
+ local pvFileName="productVersion.txt"
+ if [ "$is_flattened" = true ]; then
+ if [ -z "$runtime" ]; then
+ pvFileName="sdk-productVersion.txt"
+ elif [[ "$runtime" == "dotnet" ]]; then
+ pvFileName="runtime-productVersion.txt"
+ else
+ pvFileName="$runtime-productVersion.txt"
+ fi
+ fi
+
+ local download_link=null
+
+ if [ -z "$package_download_link" ]; then
+ if [[ "$runtime" == "dotnet" ]]; then
+ download_link="$azure_feed/Runtime/$specific_version/${pvFileName}"
+ elif [[ "$runtime" == "aspnetcore" ]]; then
+ download_link="$azure_feed/aspnetcore/Runtime/$specific_version/${pvFileName}"
+ elif [ -z "$runtime" ]; then
+ download_link="$azure_feed/Sdk/$specific_version/${pvFileName}"
+ else
+ return 1
+ fi
+ else
+ download_link="${package_download_link%/*}/${pvFileName}"
+ fi
+
+ say_verbose "Constructed productVersion link: $download_link"
+ echo "$download_link"
+ return 0
+}
+
+# args:
+# download link - $1
+# specific version - $2
+get_product_specific_version_from_download_link()
+{
+ eval $invocation
+
+ local download_link="$1"
+ local specific_version="$2"
+ local specific_product_version=""
+
+ if [ -z "$download_link" ]; then
+ echo "$specific_version"
+ return 0
+ fi
+
+ #get filename
+ filename="${download_link##*/}"
+
+ #product specific version follows the product name
+ #for filename 'dotnet-sdk-3.1.404-linux-x64.tar.gz': the product version is 3.1.404
+ IFS='-'
+ read -ra filename_elems <<< "$filename"
+ count=${#filename_elems[@]}
+ if [[ "$count" -gt 2 ]]; then
+ specific_product_version="${filename_elems[2]}"
+ else
+ specific_product_version=$specific_version
+ fi
+ unset IFS;
+ echo "$specific_product_version"
+ return 0
+}
+
+# args:
+# azure_feed - $1
+# channel - $2
+# normalized_architecture - $3
+# specific_version - $4
+construct_legacy_download_link() {
+ eval $invocation
+
+ local azure_feed="$1"
+ local channel="$2"
+ local normalized_architecture="$3"
+ local specific_version="${4//[$'\t\r\n']}"
+
+ local distro_specific_osname
+ distro_specific_osname="$(get_legacy_os_name)" || return 1
+
+ local legacy_download_link=null
+ if [[ "$runtime" == "dotnet" ]]; then
+ legacy_download_link="$azure_feed/Runtime/$specific_version/dotnet-$distro_specific_osname-$normalized_architecture.$specific_version.tar.gz"
+ elif [ -z "$runtime" ]; then
+ legacy_download_link="$azure_feed/Sdk/$specific_version/dotnet-dev-$distro_specific_osname-$normalized_architecture.$specific_version.tar.gz"
+ else
+ return 1
+ fi
+
+ echo "$legacy_download_link"
+ return 0
+}
+
+get_user_install_path() {
+ eval $invocation
+
+ if [ ! -z "${DOTNET_INSTALL_DIR:-}" ]; then
+ echo "$DOTNET_INSTALL_DIR"
+ else
+ echo "$HOME/.dotnet"
+ fi
+ return 0
+}
+
+# args:
+# install_dir - $1
+resolve_installation_path() {
+ eval $invocation
+
+ local install_dir=$1
+ if [ "$install_dir" = "" ]; then
+ local user_install_path="$(get_user_install_path)"
+ say_verbose "resolve_installation_path: user_install_path=$user_install_path"
+ echo "$user_install_path"
+ return 0
+ fi
+
+ echo "$install_dir"
+ return 0
+}
+
+# args:
+# relative_or_absolute_path - $1
+get_absolute_path() {
+ eval $invocation
+
+ local relative_or_absolute_path=$1
+ echo "$(cd "$(dirname "$1")" && pwd -P)/$(basename "$1")"
+ return 0
+}
+
+# args:
+# override - $1 (boolean, true or false)
+get_cp_options() {
+ eval $invocation
+
+ local override="$1"
+ local override_switch=""
+
+ if [ "$override" = false ]; then
+ override_switch="-n"
+
+ # create temporary files to check if 'cp -u' is supported
+ tmp_dir="$(mktemp -d)"
+ tmp_file="$tmp_dir/testfile"
+ tmp_file2="$tmp_dir/testfile2"
+
+ touch "$tmp_file"
+
+ # use -u instead of -n if it's available
+ if cp -u "$tmp_file" "$tmp_file2" 2>/dev/null; then
+ override_switch="-u"
+ fi
+
+ # clean up
+ rm -f "$tmp_file" "$tmp_file2"
+ rm -rf "$tmp_dir"
+ fi
+
+ echo "$override_switch"
+}
+
+# args:
+# input_files - stdin
+# root_path - $1
+# out_path - $2
+# override - $3
+copy_files_or_dirs_from_list() {
+ eval $invocation
+
+ local root_path="$(remove_trailing_slash "$1")"
+ local out_path="$(remove_trailing_slash "$2")"
+ local override="$3"
+ local override_switch="$(get_cp_options "$override")"
+
+ cat | uniq | while read -r file_path; do
+ local path="$(remove_beginning_slash "${file_path#$root_path}")"
+ local target="$out_path/$path"
+ if [ "$override" = true ] || (! ([ -d "$target" ] || [ -e "$target" ])); then
+ mkdir -p "$out_path/$(dirname "$path")"
+ if [ -d "$target" ]; then
+ rm -rf "$target"
+ fi
+ cp -R $override_switch "$root_path/$path" "$target"
+ fi
+ done
+}
+
+# args:
+# zip_uri - $1
+get_remote_file_size() {
+ local zip_uri="$1"
+
+ if machine_has "curl"; then
+ file_size=$(curl -sI "$zip_uri" | grep -i content-length | awk '{ num = $2 + 0; print num }')
+ elif machine_has "wget"; then
+ file_size=$(wget --spider --server-response -O /dev/null "$zip_uri" 2>&1 | grep -i 'Content-Length:' | awk '{ num = $2 + 0; print num }')
+ else
+ say "Neither curl nor wget is available on this system."
+ return
+ fi
+
+ if [ -n "$file_size" ]; then
+ say "Remote file $zip_uri size is $file_size bytes."
+ echo "$file_size"
+ else
+ say_verbose "Content-Length header was not extracted for $zip_uri."
+ echo ""
+ fi
+}
+
+# args:
+# zip_path - $1
+# out_path - $2
+# remote_file_size - $3
+extract_dotnet_package() {
+ eval $invocation
+
+ local zip_path="$1"
+ local out_path="$2"
+ local remote_file_size="$3"
+
+ local temp_out_path="$(mktemp -d "$temporary_file_template")"
+
+ local failed=false
+ tar -xzf "$zip_path" -C "$temp_out_path" > /dev/null || failed=true
+
+ local folders_with_version_regex='^.*/[0-9]+\.[0-9]+[^/]+/'
+ find "$temp_out_path" -type f | grep -Eo "$folders_with_version_regex" | sort | copy_files_or_dirs_from_list "$temp_out_path" "$out_path" false
+ find "$temp_out_path" -type f | grep -Ev "$folders_with_version_regex" | copy_files_or_dirs_from_list "$temp_out_path" "$out_path" "$override_non_versioned_files"
+
+ validate_remote_local_file_sizes "$zip_path" "$remote_file_size"
+
+ rm -rf "$temp_out_path"
+ if [ -z ${keep_zip+x} ]; then
+ rm -f "$zip_path" && say_verbose "Temporary archive file $zip_path was removed"
+ fi
+
+ if [ "$failed" = true ]; then
+ say_err "Extraction failed"
+ return 1
+ fi
+ return 0
+}
+
+# args:
+# remote_path - $1
+# disable_feed_credential - $2
+get_http_header()
+{
+ eval $invocation
+ local remote_path="$1"
+ local disable_feed_credential="$2"
+
+ local failed=false
+ local response
+ if machine_has "curl"; then
+ get_http_header_curl $remote_path $disable_feed_credential || failed=true
+ elif machine_has "wget"; then
+ get_http_header_wget $remote_path $disable_feed_credential || failed=true
+ else
+ failed=true
+ fi
+ if [ "$failed" = true ]; then
+ say_verbose "Failed to get HTTP header: '$remote_path'."
+ return 1
+ fi
+ return 0
+}
+
+# args:
+# remote_path - $1
+# disable_feed_credential - $2
+get_http_header_curl() {
+ eval $invocation
+ local remote_path="$1"
+ local disable_feed_credential="$2"
+
+ remote_path_with_credential="$remote_path"
+ if [ "$disable_feed_credential" = false ]; then
+ remote_path_with_credential+="$feed_credential"
+ fi
+
+ curl_options="-I -sSL --retry 5 --retry-delay 2 --connect-timeout 15 "
+ curl $curl_options "$remote_path_with_credential" 2>&1 || return 1
+ return 0
+}
+
+# args:
+# remote_path - $1
+# disable_feed_credential - $2
+get_http_header_wget() {
+ eval $invocation
+ local remote_path="$1"
+ local disable_feed_credential="$2"
+ local wget_options="-q -S --spider --tries 5 "
+
+ local wget_options_extra=''
+
+ # Test for options that aren't supported on all wget implementations.
+ if [[ $(wget -h 2>&1 | grep -E 'waitretry|connect-timeout') ]]; then
+ wget_options_extra="--waitretry 2 --connect-timeout 15 "
+ else
+ say "wget extra options are unavailable for this environment"
+ fi
+
+ remote_path_with_credential="$remote_path"
+ if [ "$disable_feed_credential" = false ]; then
+ remote_path_with_credential+="$feed_credential"
+ fi
+
+ wget $wget_options $wget_options_extra "$remote_path_with_credential" 2>&1
+
+ return $?
+}
+
+# args:
+# remote_path - $1
+# [out_path] - $2 - stdout if not provided
+download() {
+ eval $invocation
+
+ local remote_path="$1"
+ local out_path="${2:-}"
+
+ if [[ "$remote_path" != "http"* ]]; then
+ cp "$remote_path" "$out_path"
+ return $?
+ fi
+
+ local failed=false
+ local attempts=0
+ while [ $attempts -lt 3 ]; do
+ attempts=$((attempts+1))
+ failed=false
+ if machine_has "curl"; then
+ downloadcurl "$remote_path" "$out_path" || failed=true
+ elif machine_has "wget"; then
+ downloadwget "$remote_path" "$out_path" || failed=true
+ else
+ say_err "Missing dependency: neither curl nor wget was found."
+ exit 1
+ fi
+
+ if [ "$failed" = false ] || [ $attempts -ge 3 ] || { [ -n "${http_code-}" ] && [ "${http_code}" = "404" ]; }; then
+ break
+ fi
+
+ say "Download attempt #$attempts has failed: ${http_code-} ${download_error_msg-}"
+ say "Attempt #$((attempts+1)) will start in $((attempts*10)) seconds."
+ sleep $((attempts*10))
+ done
+
+ if [ "$failed" = true ]; then
+ say_verbose "Download failed: $remote_path"
+ return 1
+ fi
+ return 0
+}
+
+# Updates global variables $http_code and $download_error_msg
+downloadcurl() {
+ eval $invocation
+ unset http_code
+ unset download_error_msg
+ local remote_path="$1"
+ local out_path="${2:-}"
+ # Append feed_credential as late as possible before calling curl to avoid logging feed_credential
+ # Avoid passing URI with credentials to functions: note, most of them echoing parameters of invocation in verbose output.
+ local remote_path_with_credential="${remote_path}${feed_credential}"
+ local curl_options="--retry 20 --retry-delay 2 --connect-timeout 15 -sSL -f --create-dirs "
+ local curl_exit_code=0;
+ if [ -z "$out_path" ]; then
+ curl_output=$(curl $curl_options "$remote_path_with_credential" 2>&1)
+ curl_exit_code=$?
+ echo "$curl_output"
+ else
+ curl_output=$(curl $curl_options -o "$out_path" "$remote_path_with_credential" 2>&1)
+ curl_exit_code=$?
+ fi
+
+ # Regression in curl causes curl with --retry to return a 0 exit code even when it fails to download a file - https://github.com/curl/curl/issues/17554
+ if [ $curl_exit_code -eq 0 ] && echo "$curl_output" | grep -q "^curl: ([0-9]*) "; then
+ curl_exit_code=$(echo "$curl_output" | sed 's/curl: (\([0-9]*\)).*/\1/')
+ fi
+
+ if [ $curl_exit_code -gt 0 ]; then
+ download_error_msg="Unable to download $remote_path."
+ # Check for curl timeout codes
+ if [[ $curl_exit_code == 7 || $curl_exit_code == 28 ]]; then
+ download_error_msg+=" Failed to reach the server: connection timeout."
+ else
+ local disable_feed_credential=false
+ local response=$(get_http_header_curl $remote_path $disable_feed_credential)
+ http_code=$( echo "$response" | awk '/^HTTP/{print $2}' | tail -1 )
+ if [[ ! -z $http_code && $http_code != 2* ]]; then
+ download_error_msg+=" Returned HTTP status code: $http_code."
+ fi
+ fi
+ say_verbose "$download_error_msg"
+ return 1
+ fi
+ return 0
+}
+
+
+# Updates global variables $http_code and $download_error_msg
+downloadwget() {
+ eval $invocation
+ unset http_code
+ unset download_error_msg
+ local remote_path="$1"
+ local out_path="${2:-}"
+ # Append feed_credential as late as possible before calling wget to avoid logging feed_credential
+ local remote_path_with_credential="${remote_path}${feed_credential}"
+ local wget_options="--tries 20 "
+
+ local wget_options_extra=''
+ local wget_result=''
+
+ # Test for options that aren't supported on all wget implementations.
+ if [[ $(wget -h 2>&1 | grep -E 'waitretry|connect-timeout') ]]; then
+ wget_options_extra="--waitretry 2 --connect-timeout 15 "
+ else
+ say "wget extra options are unavailable for this environment"
+ fi
+
+ if [ -z "$out_path" ]; then
+ wget -q $wget_options $wget_options_extra -O - "$remote_path_with_credential" 2>&1
+ wget_result=$?
+ else
+ wget $wget_options $wget_options_extra -O "$out_path" "$remote_path_with_credential" 2>&1
+ wget_result=$?
+ fi
+
+ if [[ $wget_result != 0 ]]; then
+ local disable_feed_credential=false
+ local response=$(get_http_header_wget $remote_path $disable_feed_credential)
+ http_code=$( echo "$response" | awk '/^ HTTP/{print $2}' | tail -1 )
+ download_error_msg="Unable to download $remote_path."
+ if [[ ! -z $http_code && $http_code != 2* ]]; then
+ download_error_msg+=" Returned HTTP status code: $http_code."
+ # wget exit code 4 stands for network-issue
+ elif [[ $wget_result == 4 ]]; then
+ download_error_msg+=" Failed to reach the server: connection timeout."
+ fi
+ say_verbose "$download_error_msg"
+ return 1
+ fi
+
+ return 0
+}
+
+get_download_link_from_aka_ms() {
+ eval $invocation
+
+ #quality is not supported for LTS or STS channel
+ #STS maps to current
+ if [[ ! -z "$normalized_quality" && ("$normalized_channel" == "LTS" || "$normalized_channel" == "STS") ]]; then
+ normalized_quality=""
+ say_warning "Specifying quality for STS or LTS channel is not supported, the quality will be ignored."
+ fi
+
+ say_verbose "Retrieving primary payload URL from aka.ms for channel: '$normalized_channel', quality: '$normalized_quality', product: '$normalized_product', os: '$normalized_os', architecture: '$normalized_architecture'."
+
+ #construct aka.ms link
+ aka_ms_link="https://aka.ms/dotnet"
+ if [ "$internal" = true ]; then
+ aka_ms_link="$aka_ms_link/internal"
+ fi
+ aka_ms_link="$aka_ms_link/$normalized_channel"
+ if [[ ! -z "$normalized_quality" ]]; then
+ aka_ms_link="$aka_ms_link/$normalized_quality"
+ fi
+ aka_ms_link="$aka_ms_link/$normalized_product-$normalized_os-$normalized_architecture.tar.gz"
+ say_verbose "Constructed aka.ms link: '$aka_ms_link'."
+
+ #get HTTP response
+ #do not pass credentials as a part of the $aka_ms_link and do not apply credentials in the get_http_header 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
+ disable_feed_credential=true
+ response="$(get_http_header $aka_ms_link $disable_feed_credential)"
+
+ say_verbose "Received response: $response"
+ # Get results of all the redirects.
+ http_codes=$( echo "$response" | awk '$1 ~ /^HTTP/ {print $2}' )
+ # They all need to be 301, otherwise some links are broken (except for the last, which is not a redirect but 200 or 404).
+ broken_redirects=$( echo "$http_codes" | sed '$d' | grep -v '301' )
+ # The response may end without final code 2xx/4xx/5xx somehow, e.g. network restrictions on www.bing.com causes redirecting to bing.com fails with connection refused.
+ # In this case it should not exclude the last.
+ last_http_code=$( echo "$http_codes" | tail -n 1 )
+ if ! [[ $last_http_code =~ ^(2|4|5)[0-9][0-9]$ ]]; then
+ broken_redirects=$( echo "$http_codes" | grep -v '301' )
+ fi
+
+ # All HTTP codes are 301 (Moved Permanently), the redirect link exists.
+ if [[ -z "$broken_redirects" ]]; then
+ aka_ms_download_link=$( echo "$response" | awk '$1 ~ /^Location/{print $2}' | tail -1 | tr -d '\r')
+
+ if [[ -z "$aka_ms_download_link" ]]; then
+ say_verbose "The aka.ms link '$aka_ms_link' is not valid: failed to get redirect location."
+ return 1
+ fi
+
+ say_verbose "The redirect location retrieved: '$aka_ms_download_link'."
+ return 0
+ else
+ say_verbose "The aka.ms link '$aka_ms_link' is not valid: received HTTP code: $(echo "$broken_redirects" | paste -sd "," -)."
+ return 1
+ fi
+}
+
+get_feeds_to_use()
+{
+ feeds=(
+ "https://builds.dotnet.microsoft.com/dotnet"
+ "https://ci.dot.net/public"
+ )
+
+ if [[ -n "$azure_feed" ]]; then
+ feeds=("$azure_feed")
+ fi
+
+ if [[ -n "$uncached_feed" ]]; then
+ feeds=("$uncached_feed")
+ fi
+}
+
+# THIS FUNCTION MAY EXIT (if the determined version is already installed).
+generate_download_links() {
+
+ download_links=()
+ specific_versions=()
+ effective_versions=()
+ link_types=()
+
+ # If generate_akams_links returns false, no fallback to old links. Just terminate.
+ # This function may also 'exit' (if the determined version is already installed).
+ generate_akams_links || return
+
+ # Check other feeds only if we haven't been able to find an aka.ms link.
+ if [[ "${#download_links[@]}" -lt 1 ]]; then
+ for feed in ${feeds[@]}
+ do
+ # generate_regular_links may also 'exit' (if the determined version is already installed).
+ generate_regular_links $feed || return
+ done
+ fi
+
+ if [[ "${#download_links[@]}" -eq 0 ]]; then
+ say_err "Failed to resolve the exact version number."
+ return 1
+ fi
+
+ say_verbose "Generated ${#download_links[@]} links."
+ for link_index in ${!download_links[@]}
+ do
+ say_verbose "Link $link_index: ${link_types[$link_index]}, ${effective_versions[$link_index]}, ${download_links[$link_index]}"
+ done
+}
+
+# THIS FUNCTION MAY EXIT (if the determined version is already installed).
+generate_akams_links() {
+ local valid_aka_ms_link=true;
+
+ normalized_version="$(to_lowercase "$version")"
+ if [[ "$normalized_version" != "latest" ]] && [ -n "$normalized_quality" ]; then
+ say_err "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."
+ return 1
+ fi
+
+ if [[ -n "$json_file" || "$normalized_version" != "latest" ]]; then
+ # aka.ms links are not needed when exact version is specified via command or json file
+ return
+ fi
+
+ get_download_link_from_aka_ms || valid_aka_ms_link=false
+
+ if [[ "$valid_aka_ms_link" == true ]]; then
+ say_verbose "Retrieved primary payload URL from aka.ms link: '$aka_ms_download_link'."
+ say_verbose "Downloading using legacy url will not be attempted."
+
+ download_link=$aka_ms_download_link
+
+ #get version from the path
+ IFS='/'
+ read -ra pathElems <<< "$download_link"
+ count=${#pathElems[@]}
+ specific_version="${pathElems[count-2]}"
+ unset IFS;
+ say_verbose "Version: '$specific_version'."
+
+ #Retrieve effective version
+ effective_version="$(get_specific_product_version "$azure_feed" "$specific_version" "$download_link")"
+
+ # Add link info to arrays
+ download_links+=($download_link)
+ specific_versions+=($specific_version)
+ effective_versions+=($effective_version)
+ link_types+=("aka.ms")
+
+ # Check if the SDK version is already installed.
+ if [[ "$dry_run" != true ]] && is_dotnet_package_installed "$install_root" "$asset_relative_path" "$effective_version"; then
+ say "$asset_name with version '$effective_version' is already installed."
+ exit 0
+ fi
+
+ return 0
+ fi
+
+ # if quality is specified - exit with error - there is no fallback approach
+ if [ ! -z "$normalized_quality" ]; then
+ say_err "Failed to locate the latest version in the channel '$normalized_channel' with '$normalized_quality' quality for '$normalized_product', os: '$normalized_os', architecture: '$normalized_architecture'."
+ say_err "Refer to: https://aka.ms/dotnet-os-lifecycle for information on .NET Core support."
+ return 1
+ fi
+ say_verbose "Falling back to latest.version file approach."
+}
+
+# THIS FUNCTION MAY EXIT (if the determined version is already installed)
+# args:
+# feed - $1
+generate_regular_links() {
+ local feed="$1"
+ local valid_legacy_download_link=true
+
+ specific_version=$(get_specific_version_from_version "$feed" "$channel" "$normalized_architecture" "$version" "$json_file") || specific_version='0'
+
+ if [[ "$specific_version" == '0' ]]; then
+ say_verbose "Failed to resolve the specific version number using feed '$feed'"
+ return
+ fi
+
+ effective_version="$(get_specific_product_version "$feed" "$specific_version")"
+ say_verbose "specific_version=$specific_version"
+
+ download_link="$(construct_download_link "$feed" "$channel" "$normalized_architecture" "$specific_version" "$normalized_os")"
+ say_verbose "Constructed primary named payload URL: $download_link"
+
+ # Add link info to arrays
+ download_links+=($download_link)
+ specific_versions+=($specific_version)
+ effective_versions+=($effective_version)
+ link_types+=("primary")
+
+ legacy_download_link="$(construct_legacy_download_link "$feed" "$channel" "$normalized_architecture" "$specific_version")" || valid_legacy_download_link=false
+
+ if [ "$valid_legacy_download_link" = true ]; then
+ say_verbose "Constructed legacy named payload URL: $legacy_download_link"
+
+ download_links+=($legacy_download_link)
+ specific_versions+=($specific_version)
+ effective_versions+=($effective_version)
+ link_types+=("legacy")
+ else
+ legacy_download_link=""
+ say_verbose "Could not construct a legacy_download_link; omitting..."
+ fi
+
+ # Check if the SDK version is already installed.
+ if [[ "$dry_run" != true ]] && is_dotnet_package_installed "$install_root" "$asset_relative_path" "$effective_version"; then
+ say "$asset_name with version '$effective_version' is already installed."
+ exit 0
+ fi
+}
+
+print_dry_run() {
+
+ say "Payload URLs:"
+
+ for link_index in "${!download_links[@]}"
+ do
+ say "URL #$link_index - ${link_types[$link_index]}: ${download_links[$link_index]}"
+ done
+
+ resolved_version=${specific_versions[0]}
+ repeatable_command="./$script_name --version "\""$resolved_version"\"" --install-dir "\""$install_root"\"" --architecture "\""$normalized_architecture"\"" --os "\""$normalized_os"\"""
+
+ if [ ! -z "$normalized_quality" ]; then
+ repeatable_command+=" --quality "\""$normalized_quality"\"""
+ fi
+
+ if [[ "$runtime" == "dotnet" ]]; then
+ repeatable_command+=" --runtime "\""dotnet"\"""
+ elif [[ "$runtime" == "aspnetcore" ]]; then
+ repeatable_command+=" --runtime "\""aspnetcore"\"""
+ fi
+
+ repeatable_command+="$non_dynamic_parameters"
+
+ if [ -n "$feed_credential" ]; then
+ repeatable_command+=" --feed-credential "\"""\"""
+ fi
+
+ say "Repeatable invocation: $repeatable_command"
+}
+
+calculate_vars() {
+ eval $invocation
+
+ script_name=$(basename "$0")
+ normalized_architecture="$(get_normalized_architecture_from_architecture "$architecture")"
+ say_verbose "Normalized architecture: '$normalized_architecture'."
+ normalized_os="$(get_normalized_os "$user_defined_os")"
+ say_verbose "Normalized OS: '$normalized_os'."
+ normalized_quality="$(get_normalized_quality "$quality")"
+ say_verbose "Normalized quality: '$normalized_quality'."
+ normalized_channel="$(get_normalized_channel "$channel")"
+ say_verbose "Normalized channel: '$normalized_channel'."
+ normalized_product="$(get_normalized_product "$runtime")"
+ say_verbose "Normalized product: '$normalized_product'."
+ install_root="$(resolve_installation_path "$install_dir")"
+ say_verbose "InstallRoot: '$install_root'."
+
+ normalized_architecture="$(get_normalized_architecture_for_specific_sdk_version "$version" "$normalized_channel" "$normalized_architecture")"
+
+ if [[ "$runtime" == "dotnet" ]]; then
+ asset_relative_path="shared/Microsoft.NETCore.App"
+ asset_name=".NET Core Runtime"
+ elif [[ "$runtime" == "aspnetcore" ]]; then
+ asset_relative_path="shared/Microsoft.AspNetCore.App"
+ asset_name="ASP.NET Core Runtime"
+ elif [ -z "$runtime" ]; then
+ asset_relative_path="sdk"
+ asset_name=".NET Core SDK"
+ fi
+
+ get_feeds_to_use
+}
+
+install_dotnet() {
+ eval $invocation
+ local download_failed=false
+ local download_completed=false
+ local remote_file_size=0
+
+ mkdir -p "$install_root"
+ zip_path="${zip_path:-$(mktemp "$temporary_file_template")}"
+ say_verbose "Archive path: $zip_path"
+
+ for link_index in "${!download_links[@]}"
+ do
+ download_link="${download_links[$link_index]}"
+ specific_version="${specific_versions[$link_index]}"
+ effective_version="${effective_versions[$link_index]}"
+ link_type="${link_types[$link_index]}"
+
+ say "Attempting to download using $link_type link $download_link"
+
+ # The download function will set variables $http_code and $download_error_msg in case of failure.
+ download_failed=false
+ download "$download_link" "$zip_path" 2>&1 || download_failed=true
+
+ if [ "$download_failed" = true ]; then
+ case ${http_code-} in
+ 404)
+ say "The resource at $link_type link '$download_link' is not available."
+ ;;
+ *)
+ say "Failed to download $link_type link '$download_link': ${http_code-} ${download_error_msg-}"
+ ;;
+ esac
+ rm -f "$zip_path" 2>&1 && say_verbose "Temporary archive file $zip_path was removed"
+ else
+ download_completed=true
+ break
+ fi
+ done
+
+ if [[ "$download_completed" == false ]]; then
+ say_err "Could not find \`$asset_name\` with version = $specific_version"
+ say_err "Refer to: https://aka.ms/dotnet-os-lifecycle for information on .NET Core support"
+ return 1
+ fi
+
+ remote_file_size="$(get_remote_file_size "$download_link")"
+
+ say "Extracting archive from $download_link"
+ extract_dotnet_package "$zip_path" "$install_root" "$remote_file_size" || return 1
+
+ # Check if the SDK version is installed; if not, fail the installation.
+ # if the version contains "RTM" or "servicing"; check if a 'release-type' SDK version is installed.
+ if [[ $specific_version == *"rtm"* || $specific_version == *"servicing"* ]]; then
+ IFS='-'
+ read -ra verArr <<< "$specific_version"
+ release_version="${verArr[0]}"
+ unset IFS;
+ say_verbose "Checking installation: version = $release_version"
+ if is_dotnet_package_installed "$install_root" "$asset_relative_path" "$release_version"; then
+ say "Installed version is $effective_version"
+ return 0
+ fi
+ fi
+
+ # Check if the standard SDK version is installed.
+ say_verbose "Checking installation: version = $effective_version"
+ if is_dotnet_package_installed "$install_root" "$asset_relative_path" "$effective_version"; then
+ say "Installed version is $effective_version"
+ return 0
+ fi
+
+ # Version verification failed. More likely something is wrong either with the downloaded content or with the verification algorithm.
+ say_err "Failed to verify the version of installed \`$asset_name\`.\nInstallation source: $download_link.\nInstallation location: $install_root.\nReport the bug at https://github.com/dotnet/install-scripts/issues."
+ say_err "\`$asset_name\` with version = $effective_version failed to install with an error."
+ return 1
+}
+
+args=("$@")
+
+local_version_file_relative_path="/.version"
+bin_folder_relative_path=""
+temporary_file_template="${TMPDIR:-/tmp}/dotnet.XXXXXXXXX"
+
+channel="LTS"
+version="Latest"
+json_file=""
+install_dir=""
+architecture=""
+dry_run=false
+no_path=false
+azure_feed=""
+uncached_feed=""
+feed_credential=""
+verbose=false
+runtime=""
+runtime_id=""
+quality=""
+internal=false
+override_non_versioned_files=true
+non_dynamic_parameters=""
+user_defined_os=""
+
+while [ $# -ne 0 ]
+do
+ name="$1"
+ case "$name" in
+ -c|--channel|-[Cc]hannel)
+ shift
+ channel="$1"
+ ;;
+ -v|--version|-[Vv]ersion)
+ shift
+ version="$1"
+ ;;
+ -q|--quality|-[Qq]uality)
+ shift
+ quality="$1"
+ ;;
+ --internal|-[Ii]nternal)
+ internal=true
+ non_dynamic_parameters+=" $name"
+ ;;
+ -i|--install-dir|-[Ii]nstall[Dd]ir)
+ shift
+ install_dir="$1"
+ ;;
+ --arch|--architecture|-[Aa]rch|-[Aa]rchitecture)
+ shift
+ architecture="$1"
+ ;;
+ --os|-[Oo][SS])
+ shift
+ user_defined_os="$1"
+ ;;
+ --shared-runtime|-[Ss]hared[Rr]untime)
+ say_warning "The --shared-runtime flag is obsolete and may be removed in a future version of this script. The recommended usage is to specify '--runtime dotnet'."
+ if [ -z "$runtime" ]; then
+ runtime="dotnet"
+ fi
+ ;;
+ --runtime|-[Rr]untime)
+ shift
+ runtime="$1"
+ if [[ "$runtime" != "dotnet" ]] && [[ "$runtime" != "aspnetcore" ]]; then
+ say_err "Unsupported value for --runtime: '$1'. Valid values are 'dotnet' and 'aspnetcore'."
+ if [[ "$runtime" == "windowsdesktop" ]]; then
+ say_err "WindowsDesktop archives are manufactured for Windows platforms only."
+ fi
+ exit 1
+ fi
+ ;;
+ --dry-run|-[Dd]ry[Rr]un)
+ dry_run=true
+ ;;
+ --no-path|-[Nn]o[Pp]ath)
+ no_path=true
+ non_dynamic_parameters+=" $name"
+ ;;
+ --verbose|-[Vv]erbose)
+ verbose=true
+ non_dynamic_parameters+=" $name"
+ ;;
+ --azure-feed|-[Aa]zure[Ff]eed)
+ shift
+ azure_feed="$1"
+ non_dynamic_parameters+=" $name "\""$1"\"""
+ ;;
+ --uncached-feed|-[Uu]ncached[Ff]eed)
+ shift
+ uncached_feed="$1"
+ non_dynamic_parameters+=" $name "\""$1"\"""
+ ;;
+ --feed-credential|-[Ff]eed[Cc]redential)
+ shift
+ feed_credential="$1"
+ #feed_credential should start with "?", for it to be added to the end of the link.
+ #adding "?" at the beginning of the feed_credential if needed.
+ [[ -z "$(echo $feed_credential)" ]] || [[ $feed_credential == \?* ]] || feed_credential="?$feed_credential"
+ ;;
+ --runtime-id|-[Rr]untime[Ii]d)
+ shift
+ runtime_id="$1"
+ non_dynamic_parameters+=" $name "\""$1"\"""
+ say_warning "Use of --runtime-id is obsolete and should be limited to the versions below 2.1. To override architecture, use --architecture option instead. To override OS, use --os option instead."
+ ;;
+ --jsonfile|-[Jj][Ss]on[Ff]ile)
+ shift
+ json_file="$1"
+ ;;
+ --skip-non-versioned-files|-[Ss]kip[Nn]on[Vv]ersioned[Ff]iles)
+ override_non_versioned_files=false
+ non_dynamic_parameters+=" $name"
+ ;;
+ --keep-zip|-[Kk]eep[Zz]ip)
+ keep_zip=true
+ non_dynamic_parameters+=" $name"
+ ;;
+ --zip-path|-[Zz]ip[Pp]ath)
+ shift
+ zip_path="$1"
+ ;;
+ -?|--?|-h|--help|-[Hh]elp)
+ script_name="dotnet-install.sh"
+ echo ".NET Tools Installer"
+ echo "Usage:"
+ echo " # Install a .NET SDK of a given Quality from a given Channel"
+ echo " $script_name [-c|--channel ] [-q|--quality ]"
+ echo " # Install a .NET SDK of a specific public version"
+ echo " $script_name [-v|--version ]"
+ echo " $script_name -h|-?|--help"
+ echo ""
+ echo "$script_name is a simple command line interface for obtaining dotnet cli."
+ echo " Note that the intended use of this script is for Continuous Integration (CI) scenarios, where:"
+ echo " - The SDK needs to be installed without user interaction and without admin rights."
+ echo " - The SDK installation doesn't need to persist across multiple CI runs."
+ echo " 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."
+ echo ""
+ echo "Options:"
+ echo " -c,--channel Download from the channel specified, Defaults to \`$channel\`."
+ echo " -Channel"
+ echo " Possible values:"
+ echo " - STS - the most recent Standard Term Support release"
+ echo " - LTS - the most recent Long Term Support release"
+ echo " - 2-part version in a format A.B - represents a specific release"
+ echo " examples: 2.0; 1.0"
+ echo " - 3-part version in a format A.B.Cxx - represents a specific SDK release"
+ echo " examples: 5.0.1xx, 5.0.2xx."
+ echo " Supported since 5.0 release"
+ echo " Warning: Value 'Current' is deprecated for the Channel parameter. Use 'STS' instead."
+ echo " Note: The version parameter overrides the channel parameter when any version other than 'latest' is used."
+ echo " -v,--version Use specific VERSION, Defaults to \`$version\`."
+ echo " -Version"
+ echo " Possible values:"
+ echo " - latest - the latest build on specific channel"
+ echo " - 3-part version in a format A.B.C - represents specific version of build"
+ echo " examples: 2.0.0-preview2-006120; 1.1.0"
+ echo " -q,--quality Download the latest build of specified quality in the channel."
+ echo " -Quality"
+ echo " The possible values are: daily, preview, GA."
+ echo " Works only in combination with channel. Not applicable for STS and LTS channels and will be ignored if those channels are used."
+ echo " For SDK use channel in A.B.Cxx format. Using quality for SDK together with channel in A.B format is not supported."
+ echo " Supported since 5.0 release."
+ echo " Note: The version parameter overrides the channel parameter when any version other than 'latest' is used, and therefore overrides the quality."
+ echo " --internal,-Internal Download internal builds. Requires providing credentials via --feed-credential parameter."
+ echo " --feed-credential Token to access Azure feed. Used as a query string to append to the Azure feed."
+ echo " -FeedCredential This parameter typically is not specified."
+ echo " -i,--install-dir Install under specified location (see Install Location below)"
+ echo " -InstallDir"
+ echo " --architecture Architecture of dotnet binaries to be installed, Defaults to \`$architecture\`."
+ echo " --arch,-Architecture,-Arch"
+ echo " Possible values: x64, arm, arm64, s390x, ppc64le and loongarch64"
+ echo " --os Specifies operating system to be used when selecting the installer."
+ echo " Overrides the OS determination approach used by the script. Supported values: osx, linux, linux-musl, freebsd, rhel.6."
+ echo " In case any other value is provided, the platform will be determined by the script based on machine configuration."
+ echo " Not supported for legacy links. Use --runtime-id to specify platform for legacy links."
+ echo " Refer to: https://aka.ms/dotnet-os-lifecycle for more information."
+ echo " --runtime Installs a shared runtime only, without the SDK."
+ echo " -Runtime"
+ echo " Possible values:"
+ echo " - dotnet - the Microsoft.NETCore.App shared runtime"
+ echo " - aspnetcore - the Microsoft.AspNetCore.App shared runtime"
+ echo " --dry-run,-DryRun Do not perform installation. Display download link."
+ echo " --no-path, -NoPath Do not set PATH for the current process."
+ echo " --verbose,-Verbose Display diagnostics information."
+ echo " --azure-feed,-AzureFeed For internal use only."
+ echo " Allows using a different storage to download SDK archives from."
+ echo " --uncached-feed,-UncachedFeed For internal use only."
+ echo " Allows using a different storage to download SDK archives from."
+ echo " --skip-non-versioned-files Skips non-versioned files if they already exist, such as the dotnet executable."
+ echo " -SkipNonVersionedFiles"
+ echo " --jsonfile Determines the SDK version from a user specified global.json file."
+ echo " Note: global.json must have a value for 'SDK:Version'"
+ echo " --keep-zip,-KeepZip If set, downloaded file is kept."
+ echo " --zip-path, -ZipPath If set, downloaded file is stored at the specified path."
+ echo " -?,--?,-h,--help,-Help Shows this help message"
+ echo ""
+ echo "Install Location:"
+ echo " Location is chosen in following order:"
+ echo " - --install-dir option"
+ echo " - Environmental variable DOTNET_INSTALL_DIR"
+ echo " - $HOME/.dotnet"
+ exit 0
+ ;;
+ *)
+ say_err "Unknown argument \`$name\`"
+ exit 1
+ ;;
+ esac
+
+ shift
+done
+
+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.\n"
+
+if [ "$internal" = true ] && [ -z "$(echo $feed_credential)" ]; then
+ message="Provide credentials via --feed-credential parameter."
+ if [ "$dry_run" = true ]; then
+ say_warning "$message"
+ else
+ say_err "$message"
+ exit 1
+ fi
+fi
+
+check_min_reqs
+calculate_vars
+# generate_regular_links call below will 'exit' if the determined version is already installed.
+generate_download_links
+
+if [[ "$dry_run" = true ]]; then
+ print_dry_run
+ exit 0
+fi
+
+install_dotnet
+
+bin_path="$(get_absolute_path "$(combine_paths "$install_root" "$bin_folder_relative_path")")"
+if [ "$no_path" = false ]; then
+ say "Adding to current process PATH: \`$bin_path\`. Note: This change will be visible only when sourcing script."
+ export PATH="$bin_path":"$PATH"
+else
+ say "Binaries of dotnet can be found in $bin_path"
+fi
+
+say "Note that the script does not resolve dependencies during installation."
+say "To check the list of dependencies, go to https://learn.microsoft.com/dotnet/core/install, select your operating system and check the \"Dependencies\" section."
+say "Installation finished successfully."
diff --git a/tools/variables/_define.ps1 b/tools/variables/_define.ps1
index d40e5cf..ba081f0 100644
--- a/tools/variables/_define.ps1
+++ b/tools/variables/_define.ps1
@@ -1,7 +1,7 @@
<#
.SYNOPSIS
This script translates the variables returned by the _all.ps1 script
- into commands that instruct Azure Pipelines to actually set those variables for other pipeline tasks to consume.
+ into commands that instruct Azure Pipelines or GitHub Actions to actually set those variables for other pipeline tasks to consume.
The build or release definition may have set these variables to override
what the build would do. So only set them if they have not already been set.
@@ -11,6 +11,10 @@
param (
)
+if ($env:GITHUB_ACTIONS) {
+ . "$PSScriptRoot\..\GitHubActions.ps1"
+}
+
(& "$PSScriptRoot\_all.ps1").GetEnumerator() |% {
# Always use ALL CAPS for env var names since Azure Pipelines converts variable names to all caps and on non-Windows OS, env vars are case sensitive.
$keyCaps = $_.Key.ToUpper()
@@ -24,7 +28,7 @@ param (
# and the second that works across jobs and stages but must be fully qualified when referenced.
Write-Host "##vso[task.setvariable variable=$keyCaps;isOutput=true]$($_.Value)"
} elseif ($env:GITHUB_ACTIONS) {
- Add-Content -LiteralPath $env:GITHUB_ENV -Value "$keyCaps=$($_.Value)"
+ Add-GitHubActionsEnvVariable -Name $keyCaps -Value ([string]$_.Value)
}
Set-Item -LiteralPath "env:$keyCaps" -Value $_.Value
}