diff --git a/.agents/skills/bundle-dependency-prs/SKILL.md b/.agents/skills/bundle-dependency-prs/SKILL.md
new file mode 100644
index 00000000..3fec6683
--- /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/.agents/skills/update-library-template/template-release-notes.md b/.agents/skills/update-library-template/template-release-notes.md
new file mode 100644
index 00000000..0ef41c2c
--- /dev/null
+++ b/.agents/skills/update-library-template/template-release-notes.md
@@ -0,0 +1,84 @@
+# Template release notes
+
+This file will describe significant changes in Library.Template as they are introduced, especially if they require special consideration when merging updates into existing repos.
+This file is referenced by update-library-template.prompt.md and should remain in place to facilitate future merges, whether done manually or by AI.
+
+## Solution rename
+
+Never leave a Library.slnx file in the repository.
+You might even see one there even though this particular merge didn't bring it in.
+This can be an artifact of having renamed Library.sln to Library.slnx in the template repo, but ultimately the receiving repo should have only one .sln or .slnx file, with a better name than `Library`.
+Delete any `Library.slnx` that you see.
+Migrate an `.sln` in the repo root to `.slnx` using this command:
+
+```ps1
+dotnet solution EXISTING.sln migrate
+```
+
+This will create an EXISTING.slnx file. `git add` that file, then `git rm` the old `.sln` file.
+Sometimes a repo will reference the sln filename in a script or doc somewhere.
+Search the repo for such references and update them to the slnx file.
+
+## Migrating from xUnit to TUnit
+
+The template now uses TUnit with Microsoft.Testing.Platform instead of the xUnit test runner.
+When merging this update into an existing repository, migrate all test projects and repo-specific test automation.
+Follow the [TUnit xUnit migration guide](https://tunit.dev/docs/migration/xunit), including its automated migration code fixes.
+
+* Replace xUnit runner/framework package references with `TUnit.Engine` and set `Exe` in each test project.
+ Remove obsolete runner dependencies such as `xunit.runner.visualstudio`, `Microsoft.NET.Test.Sdk`, and `coverlet.collector`, along with `xunit.runner.json`.
+ Reconcile central package versions with the template's `Directory.Packages.props`.
+* Existing xUnit assertions can remain: the template retains `xunit.v3.assert`, using `xunit.v3.assert.aot` for .NET 9 and later.
+ Keep `Xunit` imports for assertions, but use TUnit for test attributes and lifecycle APIs.
+* Convert `[Fact]` and `[Theory]` to `[Test]`, and `[InlineData]` to `[Arguments]`.
+ Migrate member/class data sources, traits, fixtures, collection behavior, setup/teardown, and test output according to the migration guide.
+ Review tests that depend on xUnit's execution ordering or parallelization rules; do not assume those rules carry over.
+* Merge the `global.json` Microsoft.Testing.Platform runner setting, test directory build files, traversal projects, and updated PowerShell/YAML automation.
+ Keep every test project in the repository's solution for managed test runs.
+ The traversals discover `.csproj` files under `src` and `test`; adjust discovery if your projects live elsewhere or use another project language.
+* Update custom test commands and filters.
+ VSTest `--filter` expressions do not work with this runner; pass TUnit filters after `--`, for example `-- --treenode-filter "/*/*/ClassName/MethodName"`.
+ Replace any xUnit-specific CI result or coverage handling with the template's Microsoft.Testing.Platform integration.
+* Validate managed tests and coverage, then run `dotnet publish tools\dirs.proj -c Release` and `.\tools\dotnet-test-cloud.ps1 -Configuration Release -IncludeNativeAOT`.
+ Test projects targeting .NET 8 or later are eligible for NativeAOT publication by default.
+ If a project cannot support NativeAOT, set `false` in its project file; it will still run as managed tests.
+
+## NativeAOT compatibility validation
+
+Shipping projects now set `IsAotCompatible` for target frameworks compatible with .NET 8.
+Apply the same conditional property to each shipping project that is intended to support NativeAOT, then resolve all trim and AOT analyzer warnings rather than suppressing them broadly.
+
+The new `test/AotCompatibilityTest` project catches NativeAOT compiler warnings that library analyzers may not report.
+Keep it in the solution, add every NativeAOT-compatible shipping assembly as a `TrimmerRootAssembly`, and retain its publishable entry in `test/dirs.proj`.
+CI validates this project through the existing traversal publish on Windows, Linux, and macOS; it is intentionally marked `IsTestProject=false` because publishing successfully is the test.
+
+## NativeAOT opt-out and AI merge guidance
+
+Not every repo wants to take on NativeAOT readiness or NativeAOT-published test validation.
+If a receiving repo does not intend to support NativeAOT, opt out explicitly instead of inheriting the template's defaults.
+
+For test projects that should remain managed-only:
+
+* Set `false` in the project file or in a repo-level `Directory.Build.props`.
+* Leave the managed test project in the solution so usual IL-based testing continues to work.
+* Keep `dotnet test` and the managed coverage flow unchanged; only the NativeAOT publish step is disabled.
+
+For shipping libraries that do not want NativeAOT compatibility analysis:
+
+* Remove `IsAotCompatible` from the project, or set it only for specific target frameworks that truly support NativeAOT.
+* Do not add new AOT compatibility warnings to the build if the library is not intended to be AOT-compatible.
+* If the repo does not target .NET 8 or later, the property should be omitted entirely rather than applied unconditionally.
+
+For the compatibility project itself:
+
+* Remove the `test/AotCompatibilityTest` project from the solution if NativeAOT validation is out of scope.
+* Remove its entry from `test/dirs.proj` so the traversal publish no longer publishes it.
+* Keep the project only when the repo intends to validate shipping assemblies under NativeAOT in CI.
+
+When merging this template into a downstream repo, the AI assistant should ask whether NativeAOT is a relevant target for that repo before applying these changes if `IsAotCompatible` is not already set by any of the projects.
+If the answer is no, it should apply the opt-out steps above instead of blindly keeping the NativeAOT defaults.
+This is especially important for repos that are not shipping a NativeAOT-ready library or that cannot support the extra publish-time validation in CI.
+If the repo owner is unsure about NativeAOT support, you might offer to test the repo for NativeAOT readiness by attempting a NativeAOT publish of the relevant projects and observing whether any compatibility warnings or errors are produced.
+
+In the event the user elects for NativeAOT targeting, make efforts to get tests to work under NativeAOT as well so the shipping code can confidently advertise its NativeAOT compatibility.
+If aspects of the library or tests make successful builds, publish or test runs problematic, discuss this with the repo owner before making significant changes or giving up on NativeAOT support.
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index a5037f9e..160aded9 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,42 +3,42 @@
"isRoot": true,
"tools": {
"powershell": {
- "version": "7.6.2",
+ "version": "7.6.6",
"commands": [
"pwsh"
],
"rollForward": false
},
"dotnet-coverage": {
- "version": "18.7.0",
+ "version": "18.11.2",
"commands": [
"dotnet-coverage"
],
"rollForward": false
},
"nbgv": {
- "version": "3.9.50",
+ "version": "3.10.94",
"commands": [
"nbgv"
],
"rollForward": false
},
"docfx": {
- "version": "2.78.5",
+ "version": "2.81.0",
"commands": [
"docfx"
],
"rollForward": false
},
"nerdbank.dotnetrepotools": {
- "version": "1.4.1",
+ "version": "1.5.42",
"commands": [
"repo"
],
"rollForward": false
},
"dotnet-symbol": {
- "version": "9.0.661903",
+ "version": "10.0.745401",
"commands": [
"dotnet-symbol"
],
diff --git a/.github/renovate.json b/.github/renovate.json
index 4cd2b0cb..b5d8ff9e 100644
--- a/.github/renovate.json
+++ b/.github/renovate.json
@@ -4,5 +4,33 @@
"github>microsoft/vs-renovate-presets:microbuild",
"github>microsoft/vs-renovate-presets:vs_main_dependencies"
],
- "packageRules": []
+ "labels": ["dependencies"],
+ "packageRules": [
+ {
+ "matchPackageNames": ["nbgv", "nerdbank.gitversioning"],
+ "groupName": "nbgv and nerdbank.gitversioning updates"
+ },
+ {
+ "matchPackageNames": ["xunit*"],
+ "groupName": "xunit"
+ },
+ {
+ "matchPackageNames": ["tunit*"],
+ "groupName": "tunit",
+ "schedule": ["on the first day of the month"]
+ },
+ {
+ "matchPackageNames": ["Microsoft.Testing.Extensions.*"],
+ "groupName": "Microsoft Testing Platform"
+ },
+ {
+ "matchDatasources": ["dotnet-version", "docker"],
+ "matchDepNames": ["dotnet-sdk", "mcr.microsoft.com/dotnet/sdk"],
+ "groupName": "Dockerfile and global.json updates"
+ },
+ {
+ "matchPackageNames": ["*"],
+ "allowedVersions": "!/-g[a-f0-9]+$/"
+ }
+ ]
}
diff --git a/.github/skills/update-library-template/template-release-notes.md b/.github/skills/update-library-template/template-release-notes.md
deleted file mode 100644
index 975da315..00000000
--- a/.github/skills/update-library-template/template-release-notes.md
+++ /dev/null
@@ -1,20 +0,0 @@
-# Template release notes
-
-This file will describe significant changes in Library.Template as they are introduced, especially if they require special consideration when merging updates into existing repos.
-This file is referenced by update-library-template.prompt.md and should remain in place to facilitate future merges, whether done manually or by AI.
-
-## Solution rename
-
-Never leave a Library.slnx file in the repository.
-You might even see one there even though this particular merge didn't bring it in.
-This can be an artifact of having renamed Library.sln to Library.slnx in the template repo, but ultimately the receiving repo should have only one .sln or .slnx file, with a better name than `Library`.
-Delete any `Library.slnx` that you see.
-Migrate an `.sln` in the repo root to `.slnx` using this command:
-
-```ps1
-dotnet solution EXISTING.sln migrate
-```
-
-This will create an EXISTING.slnx file. `git add` that file, then `git rm` the old `.sln` file.
-Sometimes a repo will reference the sln filename in a script or doc somewhere.
-Search the repo for such references and update them to the slnx file.
diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml
index 3c032fae..74a62566 100644
--- a/.github/workflows/copilot-setup-steps.yml
+++ b/.github/workflows/copilot-setup-steps.yml
@@ -16,7 +16,7 @@ on:
jobs:
# The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot.
copilot-setup-steps:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-26.04
# Set the permissions to the lowest permissions possible needed for your steps.
# Copilot will be given its own token for its operations.
permissions:
@@ -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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
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 78cd2edf..2361341f 100644
--- a/.github/workflows/libtemplate-update.yml
+++ b/.github/workflows/libtemplate-update.yml
@@ -12,12 +12,12 @@ on:
jobs:
merge:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-26.04
permissions:
contents: write
pull-requests: write
steps:
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
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 77%
rename from .github/copilot-instructions.md
rename to AGENTS.md
index 9e3933b6..5254321c 100644
--- a/.github/copilot-instructions.md
+++ b/AGENTS.md
@@ -13,11 +13,10 @@
## Testing
-**IMPORTANT**: This repository uses Microsoft.Testing.Platform (MTP v2) with xunit v3. Traditional `--filter` syntax does NOT work. Use the options below instead.
+**IMPORTANT**: This repository uses TUnit with Microsoft.Testing.Platform (MTP v2). Traditional `--filter` syntax does NOT work. Use the options below instead.
* There should generally be one test project (under the `test` directory) per shipping project (under the `src` directory). Test projects are named after the project being tested with a `.Tests` suffix.
-* Tests use xunit v3 with Microsoft.Testing.Platform (MTP v2). Traditional VSTest `--filter` syntax does NOT work.
-* Some tests are known to be unstable. When running tests, you should skip the unstable ones by using `-- --filter-not-trait "FailsInCloudTest=true"`.
+* Tests use TUnit with Microsoft.Testing.Platform (MTP v2), while retaining xUnit assertions. Traditional VSTest `--filter` syntax does NOT work.
### Running Tests
@@ -46,7 +45,7 @@ dotnet test --project test/Microsoft.VisualStudio.SolutionPersistence.Tests/Micr
dotnet test --project test/Microsoft.VisualStudio.SolutionPersistence.Tests/Microsoft.VisualStudio.SolutionPersistence.Tests.csproj --no-build -c Release -- --filter-method "*Pattern*"
```
-**Run tests with a specific trait** (equivalent to category filtering):
+**Run tests with a specific property**:
```bash
dotnet test --project test/Microsoft.VisualStudio.SolutionPersistence.Tests/Microsoft.VisualStudio.SolutionPersistence.Tests.csproj --no-build -c Release -- --filter-trait "TraitName=value"
```
@@ -67,13 +66,11 @@ cd test/Microsoft.VisualStudio.SolutionPersistence.Tests
dotnet run --no-build -c Release --framework net8.0 -- --list-tests
```
-**Key points about test filtering with MTP v2 / xunit v3**:
+**Key points about test filtering with TUnit / MTP v2**:
- Options after `--` are passed to the test runner, not to `dotnet test`
-- Use `--filter-method`, `--filter-class`, `--filter-namespace` for simple filtering
-- Use `--filter-trait` and `--filter-not-trait` for trait-based filtering (replaces `--filter "TestCategory=..."`)
+- Use `--treenode-filter` to select tests by assembly, namespace, class, method, or property
- Traditional VSTest `--filter` expressions do NOT work
-- Wildcards `*` are supported at the beginning and/or end of filter values
-- Multiple simple filters of the same type use OR logic, different types combine with AND
+- Wildcards `*` are supported in tree node segments
- See `--help` for query filter language for advanced scenarios
## Coding style
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ff095b47..b912a949 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -45,7 +45,30 @@ 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`.
+
+To build both managed and NativeAOT tests, run `dotnet publish tools/dirs.proj -c Release`.
+Then run `./tools/dotnet-test-cloud.ps1 -Configuration Release -IncludeNativeAOT`.
+The traversal projects discover projects under `src` and `test`, and publish each eligible test framework targeting .NET 8 or later.
+Keep test projects in the solution as well: managed test runs still use the solution, while NativeAOT runs use the traversal's evaluated executable paths.
+Test projects can opt out of NativeAOT publishing with `false`.
+One restore includes all test target frameworks, runtime identifiers, and NativeAOT compiler dependencies.
+Managed builds are RID-neutral by default; NativeAOT builds use the SDK's runtime-specific output directories.
+For a specified RID, managed execution and native publishing can share the same build outputs.
+Test builds keep dynamic code, startup hooks, and event tracing enabled for managed code coverage.
+The `ConfigureNativeAOTTestFeatures` target disables those features only in the native compiler's publish-time inputs, without rewriting the managed runtime configuration.
+It preserves all other runtime feature options, including invariant globalization, so native compilation and linking use consistent settings.
+For an existing RID-specific build, `dotnet publish test/Library.Tests/Library.Tests.csproj -f net8.0 -r -p:NativeAOT=true --no-build` publishes native tests from the managed build.
+Use the same configuration, framework, and RID for the preceding build and the publish.
+Test builds use invariant globalization and retain only English satellite resources; RID-specific builds are self-contained.
+Shipping libraries targeting .NET 8 or later opt into NativeAOT compatibility analysis with `IsAotCompatible`.
+The `test/AotCompatibilityTest` project complements those analyzers by rooting the shipping assembly and passing it through the NativeAOT compiler during every traversal publish.
+Add each shipping assembly that must be validated as a `TrimmerRootAssembly`, and keep this project publishable in `test/dirs.proj`.
+Root `Directory.Build.props` supplies project-reference defaults for both traversal and SDK projects that remove the `_IsPublishing` global property for managed dependencies, avoiding duplicate project instances that write to the same outputs during parallel publishing.
## Releases
@@ -54,17 +77,6 @@ Use `nbgv tag` to create a tag for a particular commit that you mean to release.
Push the tag.
-### GitHub Actions
-
-When your repo is hosted by GitHub and you are using GitHub Actions, you should create a GitHub Release using the standard GitHub UI.
-Having previously used `nbgv tag` and pushing the tag will help you identify the precise commit and name to use for this release.
-
-After publishing the release, the `.github/workflows/release.yml` workflow will be automatically triggered, which will:
-
-1. Find the most recent `.github/workflows/build.yml` GitHub workflow run of the tagged release.
-1. Upload the `deployables` artifact from that workflow run to your GitHub Release.
-1. If you have `NUGET_API_KEY` defined as a secret variable for your repo or org, any nuget packages in the `deployables` artifact will be pushed to nuget.org.
-
### Azure Pipelines
When your repo builds with Azure Pipelines, use the `azure-pipelines/release.yml` pipeline.
@@ -102,3 +114,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.Build.props b/Directory.Build.props
index e0d04b6f..e31f75bc 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -49,6 +49,18 @@
16.9
+
+ win
+ linux
+ osx
+
+ x64
+ arm64
+
+ $(RidOsPrefix)-$(RidOsArchitecture)
+ $(DefaultRuntimeIdentifier)
+
+
@@ -58,6 +70,11 @@
+
+
+ _IsPublishing
+ _IsPublishing
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
index ecdfcc48..c7f26f0b 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -4,24 +4,29 @@
true
true
- 2.2.3
+ 2.4.1
- 2.0.226
+ 2.0.257
-
-
+
+
-
+
+
+
+
+
+
@@ -29,11 +34,11 @@
-
+
-
+
diff --git a/azure-pipelines/archive-sourcecode.yml b/azure-pipelines/archive-sourcecode.yml
index 86aaad3a..94f5ed3d 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/build.yml b/azure-pipelines/build.yml
index 8262e4ef..0e0fe1be 100644
--- a/azure-pipelines/build.yml
+++ b/azure-pipelines/build.yml
@@ -102,7 +102,7 @@ parameters:
- name: linuxPool
type: object
default:
- vmImage: ubuntu-24.04
+ vmImage: ubuntu-26.04
- name: macOSPool
type: object
default:
diff --git a/azure-pipelines/dotnet.yml b/azure-pipelines/dotnet.yml
index b67b4a62..28111637 100644
--- a/azure-pipelines/dotnet.yml
+++ b/azure-pipelines/dotnet.yml
@@ -8,13 +8,21 @@ parameters:
steps:
-- script: dotnet build -t:build,pack --no-restore -c $(BuildConfiguration) -warnAsError -warnNotAsError:NU1901,NU1902,NU1903,NU1904,LOCTASK002 /bl:"$(Build.ArtifactStagingDirectory)/build_logs/build.binlog"
- displayName: ๐ dotnet build
- ${{ if parameters.BuildRequiresAccessToken }}:
- env:
- SYSTEM_ACCESSTOKEN: $(System.AccessToken)
+- ${{ if parameters.RunTests }}:
+ - script: dotnet build tools/dirs.proj -t:build,pack,publish --no-restore -c $(BuildConfiguration) -warnAsError -warnNotAsError:NU1901,NU1902,NU1903,NU1904,LOCTASK002 /bl:"$(Build.ArtifactStagingDirectory)/build_logs/build.binlog"
+ displayName: ๐ dotnet build
+ ${{ if parameters.BuildRequiresAccessToken }}:
+ env:
+ SYSTEM_ACCESSTOKEN: $(System.AccessToken)
+
+- ${{ if not(parameters.RunTests) }}:
+ - script: dotnet build tools/dirs.proj -t:build,pack --no-restore -c $(BuildConfiguration) -warnAsError -warnNotAsError:NU1901,NU1902,NU1903,NU1904,LOCTASK002 /bl:"$(Build.ArtifactStagingDirectory)/build_logs/build.binlog"
+ displayName: ๐ dotnet build
+ ${{ if parameters.BuildRequiresAccessToken }}:
+ env:
+ SYSTEM_ACCESSTOKEN: $(System.AccessToken)
-- powershell: tools/dotnet-test-cloud.ps1 -Configuration $(BuildConfiguration) -Agent $(Agent.JobName) -PublishResults
+- powershell: tools/dotnet-test-cloud.ps1 -Configuration $(BuildConfiguration) -Agent $(Agent.JobName) -PublishResults -IncludeNativeAOT
displayName: ๐งช dotnet test
condition: and(succeeded(), ${{ parameters.RunTests }})
diff --git a/azure-pipelines/libtemplate-update.yml b/azure-pipelines/libtemplate-update.yml
index 388338d4..ef72abfd 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 39427c6f..2aa08425 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 9f934f92..9abd3770 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 662c07eb..243e8909 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 313c9183..6be04dcd 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 2cbdb035..5eeea955 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: VSEng-MicroBuildVSStable
+ 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 35aa1406..2a8e2d80 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: VSEng-MicroBuildVSStable
+ 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:
@@ -112,7 +129,7 @@ extends:
This PR is for **validation purposes only** for !$(System.PullRequest.PullRequestId). **Do not complete**.
CustomScriptExecutionCommand: $(Pipeline.Workspace)\VSInsertion-Windows\vs-insertion-script.ps1; src\VSSDK\NuGet\AllowUnstablePackages.ps1
InsertionBuildPolicies: Request Perf DDRITs
- InsertionReviewers: $(Build.RequestedFor)
+ InsertionReviewers: $(Build.RequestedFor),VS Core - Solution Experience
DraftPR: false # set to true and update InsertionBuildPolicy when we can specify all the validations we want to run (https://dev.azure.com/devdiv/DevDiv/_workitems/edit/2224288)
AutoCompletePR: false
ShallowClone: true
diff --git a/global.json b/global.json
index b3a3862a..556ca493 100644
--- a/global.json
+++ b/global.json
@@ -1,6 +1,6 @@
{
"sdk": {
- "version": "10.0.300",
+ "version": "10.0.401",
"rollForward": "patch",
"allowPrerelease": false
},
@@ -8,6 +8,7 @@
"runner": "Microsoft.Testing.Platform"
},
"msbuild-sdks": {
- "Microsoft.Build.NoTargets": "3.7.56"
+ "Microsoft.Build.NoTargets": "3.7.56",
+ "Microsoft.Build.Traversal": "4.1.82"
}
}
diff --git a/init.ps1 b/init.ps1
index 8bb78ad8..775bd821 100755
--- a/init.ps1
+++ b/init.ps1
@@ -113,10 +113,11 @@ try {
if (!$NoRestore -and $PSCmdlet.ShouldProcess("NuGet packages", "Restore")) {
Write-Host "Restoring NuGet packages" -ForegroundColor $HeaderColor
- dotnet restore @RestoreArguments
+ dotnet restore "$PSScriptRoot/tools/dirs.proj" @RestoreArguments
if ($lastexitcode -ne 0) {
throw "Failure while restoring packages."
}
+
}
if (!$NoToolRestore -and $PSCmdlet.ShouldProcess("dotnet tool", "restore")) {
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index 9ba7818d..b01ac125 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -9,4 +9,5 @@
+
diff --git a/src/dirs.proj b/src/dirs.proj
new file mode 100644
index 00000000..b6fa81f9
--- /dev/null
+++ b/src/dirs.proj
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/test/AOT.props b/test/AOT.props
new file mode 100644
index 00000000..623ff24d
--- /dev/null
+++ b/test/AOT.props
@@ -0,0 +1,40 @@
+
+
+ true
+ $(AvailableRuntimeIdentifiers)
+
+
+
+ true
+ true
+ true
+
+
+ true
+ en
+
+
+ true
+
+
+
+ %(ProjectReference.GlobalPropertiesToRemove);RuntimeIdentifier;NativeAOT
+
+
+
+
+
+ false
+ false
+ false
+
+
+
+
+ false
+
+
+
+
diff --git a/test/AotCompatibilityTest/AotCompatibilityTest.csproj b/test/AotCompatibilityTest/AotCompatibilityTest.csproj
new file mode 100644
index 00000000..f8a37360
--- /dev/null
+++ b/test/AotCompatibilityTest/AotCompatibilityTest.csproj
@@ -0,0 +1,19 @@
+
+
+ net10.0
+ Exe
+ false
+ $(DefaultRuntimeIdentifier)
+ true
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/AotCompatibilityTest/Program.cs b/test/AotCompatibilityTest/Program.cs
new file mode 100644
index 00000000..460bd335
--- /dev/null
+++ b/test/AotCompatibilityTest/Program.cs
@@ -0,0 +1,4 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+Console.WriteLine("This project validates NativeAOT compatibility when it is published; the executable is not run.");
diff --git a/test/Directory.Build.props b/test/Directory.Build.props
index 466be54e..564eede7 100644
--- a/test/Directory.Build.props
+++ b/test/Directory.Build.props
@@ -3,9 +3,16 @@
+ true
+ true
false
true
true
+ false
+
+
+
+ $(DefaultRuntimeIdentifier)
diff --git a/test/Directory.Build.targets b/test/Directory.Build.targets
index 3758bb8c..95b0658b 100644
--- a/test/Directory.Build.targets
+++ b/test/Directory.Build.targets
@@ -1,14 +1,47 @@
+
+
+
+ $(NoWarn);IL2104;IL3053
+
+
-
-
-
-
+
+
+
+ <_TestFramework Include="$(TargetFrameworks)" />
+
+
+
+
+
+
+
+
+
+
+
+ <_NativeExecutableExtension Condition="$([MSBuild]::IsOsPlatform('Windows'))">.exe
+
+
+ <_NativeAOTTestExecutable Include="$(MSBuildProjectFullPath)">
+ $(MSBuildProjectName)
+ $(TargetFramework)
+ $([MSBuild]::NormalizePath('$(MSBuildProjectDirectory)', '$(PublishDir)', '$(AssemblyName)$(_NativeExecutableExtension)'))
+
+
+
diff --git a/test/Microsoft.VisualStudio.SolutionPersistence.Tests/Microsoft.VisualStudio.SolutionPersistence.Tests.csproj b/test/Microsoft.VisualStudio.SolutionPersistence.Tests/Microsoft.VisualStudio.SolutionPersistence.Tests.csproj
index de8660de..9c30d24d 100644
--- a/test/Microsoft.VisualStudio.SolutionPersistence.Tests/Microsoft.VisualStudio.SolutionPersistence.Tests.csproj
+++ b/test/Microsoft.VisualStudio.SolutionPersistence.Tests/Microsoft.VisualStudio.SolutionPersistence.Tests.csproj
@@ -13,8 +13,7 @@
-
-
+
diff --git a/test/Microsoft.VisualStudio.SolutionPersistence.Tests/xunit.runner.json b/test/Microsoft.VisualStudio.SolutionPersistence.Tests/xunit.runner.json
deleted file mode 100644
index 8465a454..00000000
--- a/test/Microsoft.VisualStudio.SolutionPersistence.Tests/xunit.runner.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
- "shadowCopy": false
-}
diff --git a/test/dirs.proj b/test/dirs.proj
new file mode 100644
index 00000000..6f6def8c
--- /dev/null
+++ b/test/dirs.proj
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_NativeAOTTest Update="@(_NativeAOTTest)" UndefineProperties="" />
+
+
+
+
+
+
+
diff --git a/tools/Get-CodeCovTool.ps1 b/tools/Get-CodeCovTool.ps1
index 734ee607..893a3534 100644
--- a/tools/Get-CodeCovTool.ps1
+++ b/tools/Get-CodeCovTool.ps1
@@ -10,15 +10,15 @@ Param(
)
if ($IsMacOS) {
- $codeCovUrl = "https://uploader.codecov.io/latest/macos/codecov"
+ $codeCovUrl = "https://cli.codecov.io/latest/macos/codecov"
$toolName = 'codecov'
}
elseif ($IsLinux) {
- $codeCovUrl = "https://uploader.codecov.io/latest/linux/codecov"
+ $codeCovUrl = "https://cli.codecov.io/latest/linux/codecov"
$toolName = 'codecov'
}
else {
- $codeCovUrl = "https://uploader.codecov.io/latest/windows/codecov.exe"
+ $codeCovUrl = "https://cli.codecov.io/latest/windows/codecov.exe"
$toolName = 'codecov.exe'
}
@@ -41,7 +41,7 @@ Function Get-FileFromWeb([Uri]$Uri, $OutDir) {
}
$toolsPath = & "$PSScriptRoot\Get-TempToolsPath.ps1"
-$binaryToolsPath = Join-Path $toolsPath codecov
+$binaryToolsPath = Join-Path $toolsPath codecov-cli
$testingPath = Join-Path $binaryToolsPath unverified
$finalToolPath = Join-Path $binaryToolsPath $toolName
@@ -52,13 +52,20 @@ if (!(Test-Path $finalToolPath)) {
$tool = Get-FileFromWeb $codeCovUrl $testingPath
$sha = Get-FileFromWeb "$codeCovUrl$shaSuffix" $testingPath
$sig = Get-FileFromWeb "$codeCovUrl$sigSuffix" $testingPath
- $key = Get-FileFromWeb https://keybase.io/codecovsecurity/pgp_keys.asc $testingPath
+ $key = Get-FileFromWeb https://keybase.io/codecovsecops/pgp_keys.asc $testingPath
if ((Get-Command gpg -ErrorAction SilentlyContinue)) {
Write-Host "Importing codecov key" -ForegroundColor Yellow
gpg --import $key
+ if ($LASTEXITCODE -ne 0) {
+ throw "Failed to import the Codecov signature verification key."
+ }
+
Write-Host "Verifying signature on codecov hash" -ForegroundColor Yellow
gpg --verify $sig $sha
+ if ($LASTEXITCODE -ne 0) {
+ throw "Codecov CLI signature verification failed."
+ }
} else {
if ($AllowSkipVerify) {
Write-Warning "gpg not found. Unable to verify hash signature."
diff --git a/tools/Get-NativeAOTTestProjects.ps1 b/tools/Get-NativeAOTTestProjects.ps1
new file mode 100644
index 00000000..27a39ec3
--- /dev/null
+++ b/tools/Get-NativeAOTTestProjects.ps1
@@ -0,0 +1,25 @@
+#!/usr/bin/env pwsh
+
+<#
+.SYNOPSIS
+ Gets NativeAOT test executable paths evaluated by the test traversal project.
+.PARAMETER Configuration
+ The build configuration used to evaluate output paths.
+#>
+[CmdletBinding()]
+Param(
+ [string]$Configuration='Debug'
+)
+
+$result = & dotnet msbuild "$PSScriptRoot/../test/dirs.proj" `
+ -nologo `
+ -getTargetResult:GetNativeAOTTests `
+ "-property:Configuration=$Configuration"
+if ($LASTEXITCODE -ne 0) {
+ throw 'Failed to discover NativeAOT tests through MSBuild.'
+}
+
+$tests = ($result | Out-String | ConvertFrom-Json).TargetResults.GetNativeAOTTests.Items
+foreach ($test in $tests) {
+ $test
+}
diff --git a/tools/GitHubActions.ps1 b/tools/GitHubActions.ps1
new file mode 100644
index 00000000..c732a0f9
--- /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 d9d35bb1..f4fcdaae 100644
--- a/tools/Install-DotNetSdk.ps1
+++ b/tools/Install-DotNetSdk.ps1
@@ -99,9 +99,9 @@ if (!$IncludeAspNetCore) {
$aspnetRuntimeVersions = @()
}
-Function Get-FileFromWeb([Uri]$Uri, $OutDir) {
+Function Get-FileFromWeb([Uri]$Uri, $OutDir, [switch]$Force) {
$OutFile = Join-Path $OutDir $Uri.Segments[-1]
- if (!(Test-Path $OutFile)) {
+ if ($Force -or !(Test-Path $OutFile)) {
Write-Verbose "Downloading $Uri..."
if (!(Test-Path $OutDir)) { New-Item -ItemType Directory -Path $OutDir | Out-Null }
try {
@@ -134,38 +134,49 @@ Function Get-InstallerExe(
$majorMinor = "$($TypedVersion.Major).$($TypedVersion.Minor)"
$ReleasesFile = Join-Path $DotNetInstallScriptRoot "$majorMinor\releases.json"
- if (!(Test-Path $ReleasesFile)) {
- Get-FileFromWeb -Uri "https://builds.dotnet.microsoft.com/dotnet/release-metadata/$majorMinor/releases.json" -OutDir (Split-Path $ReleasesFile) | Out-Null
+ $ReleasesFileWasCached = Test-Path $ReleasesFile
+ $ReleasesUri = "https://builds.dotnet.microsoft.com/dotnet/release-metadata/$majorMinor/releases.json"
+ if (!$ReleasesFileWasCached) {
+ Get-FileFromWeb -Uri $ReleasesUri -OutDir (Split-Path $ReleasesFile) | Out-Null
}
- $releases = Get-Content $ReleasesFile | ConvertFrom-Json
$url = $null
- foreach ($release in $releases.releases) {
- $filesElement = $null
- if ($release.$sku.version -eq $Version) {
- $filesElement = $release.$sku.files
- }
- if (!$filesElement -and ($sku -eq 'sdk') -and $release.sdks) {
- foreach ($sdk in $release.sdks) {
- if ($sdk.version -eq $Version) {
- $filesElement = $sdk.files
- break
+ for ($attempt = 0; $attempt -lt 2; $attempt++) {
+ $releases = Get-Content $ReleasesFile | ConvertFrom-Json
+ foreach ($release in $releases.releases) {
+ $filesElement = $null
+ if ($release.$sku.version -eq $Version) {
+ $filesElement = $release.$sku.files
+ }
+ if (!$filesElement -and ($sku -eq 'sdk') -and $release.sdks) {
+ foreach ($sdk in $release.sdks) {
+ if ($sdk.version -eq $Version) {
+ $filesElement = $sdk.files
+ break
+ }
}
}
- }
- if ($filesElement) {
- foreach ($file in $filesElement) {
- if ($file.rid -eq "win-$Architecture") {
- $url = $file.url
+ if ($filesElement) {
+ foreach ($file in $filesElement) {
+ if ($file.rid -eq "win-$Architecture") {
+ $url = $file.url
+ Break
+ }
+ }
+
+ if ($url) {
Break
}
}
+ }
- if ($url) {
- Break
- }
+ if ($url -or !$ReleasesFileWasCached -or $attempt -gt 0) {
+ break
}
+
+ Write-Verbose "Release not found in cached metadata. Refreshing $ReleasesUri..."
+ Get-FileFromWeb -Uri $ReleasesUri -OutDir (Split-Path $ReleasesFile) -Force | Out-Null
}
if ($url) {
@@ -187,6 +198,66 @@ Function Install-DotNet($Version, $Architecture, [ValidateSet('Sdk','Runtime','W
}
}
+Function Test-DotNetRuntimeInstalled(
+ $DotNetRoot,
+ [ValidateSet('Microsoft.NETCore.App','Microsoft.WindowsDesktop.App','Microsoft.AspNetCore.App')]
+ [string]$Runtime,
+ [Version]$Version
+) {
+ if (!$DotNetRoot) {
+ return $false
+ }
+
+ $runtimeRoot = Join-Path $DotNetRoot "shared\$Runtime"
+ if (!(Test-Path -LiteralPath $runtimeRoot)) {
+ return $false
+ }
+
+ foreach ($runtimeDirectory in Get-ChildItem -LiteralPath $runtimeRoot -Directory) {
+ $installedVersion = $null
+ if ([Version]::TryParse($runtimeDirectory.Name, [ref]$installedVersion) -and
+ $Version.Major -eq $installedVersion.Major -and
+ $Version.Minor -eq $installedVersion.Minor -and
+ (($Version.Build -eq -1) -or ($Version.Build -eq $installedVersion.Build)) -and
+ (($Version.Revision -eq -1) -or ($Version.Revision -eq $installedVersion.Revision))) {
+ return $true
+ }
+ }
+
+ return $false
+}
+
+Function Test-DotNetSdkInstalled($DotNetRoot, $Version, $Channel) {
+ if (!$DotNetRoot) {
+ return $false
+ }
+
+ $sdkRoot = Join-Path $DotNetRoot 'sdk'
+ if (!(Test-Path -LiteralPath $sdkRoot)) {
+ return $false
+ }
+
+ if ($Version) {
+ return Test-Path -LiteralPath (Join-Path $sdkRoot $Version) -PathType Container
+ }
+
+ $channelVersion = $null
+ if (![Version]::TryParse($Channel, [ref]$channelVersion)) {
+ return $false
+ }
+
+ foreach ($sdkDirectory in Get-ChildItem -LiteralPath $sdkRoot -Directory) {
+ $installedVersion = $null
+ if ([Version]::TryParse($sdkDirectory.Name, [ref]$installedVersion) -and
+ $channelVersion.Major -eq $installedVersion.Major -and
+ $channelVersion.Minor -eq $installedVersion.Minor) {
+ return $true
+ }
+ }
+
+ return $false
+}
+
$switches = @()
$envVars = @{
# For locally installed dotnet, skip first time experience which takes a long time
@@ -197,53 +268,67 @@ if ($InstallLocality -eq 'machine') {
if ($IsMacOS -or $IsLinux) {
$DotNetInstallDir = '/usr/share/dotnet'
} else {
+ $DotNetInstallDir = Join-Path $(if ($env:ProgramW6432) { $env:ProgramW6432 } else { $env:ProgramFiles }) 'dotnet'
+ $DotNetX86InstallDir = Join-Path ${env:ProgramFiles(x86)} 'dotnet'
$restartRequired = $false
$sdks |% {
if ($_.Version) { $version = $_.Version } else { $version = $_.Channel }
- if ($PSCmdlet.ShouldProcess(".NET SDK $version ($arch)", "Install")) {
+ if (!(Test-DotNetSdkInstalled -DotNetRoot $DotNetInstallDir -Version $_.Version -Channel $_.Channel) -and
+ $PSCmdlet.ShouldProcess(".NET SDK $version ($arch)", "Install")) {
Install-DotNet -Version $version -Architecture $arch
$restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010)
+ }
- if ($IncludeX86) {
- Install-DotNet -Version $version -Architecture x86
- $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010)
- }
+ if ($IncludeX86 -and
+ !(Test-DotNetSdkInstalled -DotNetRoot $DotNetX86InstallDir -Version $_.Version -Channel $_.Channel) -and
+ $PSCmdlet.ShouldProcess(".NET SDK $version (x86)", "Install")) {
+ Install-DotNet -Version $version -Architecture x86
+ $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010)
}
}
- $runtimeVersions | Sort-Object | Get-Unique |% {
- if ($PSCmdlet.ShouldProcess(".NET runtime $_", "Install")) {
+ $runtimeVersions | Sort-Object -Unique |% {
+ if (!(Test-DotNetRuntimeInstalled -DotNetRoot $DotNetInstallDir -Runtime Microsoft.NETCore.App -Version $_) -and
+ $PSCmdlet.ShouldProcess(".NET runtime $_ ($arch)", "Install")) {
Install-DotNet -Version $_ -sku Runtime -Architecture $arch
$restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010)
+ }
- if ($IncludeX86) {
- Install-DotNet -Version $_ -sku Runtime -Architecture x86
- $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010)
- }
+ if ($IncludeX86 -and
+ !(Test-DotNetRuntimeInstalled -DotNetRoot $DotNetX86InstallDir -Runtime Microsoft.NETCore.App -Version $_) -and
+ $PSCmdlet.ShouldProcess(".NET runtime $_ (x86)", "Install")) {
+ Install-DotNet -Version $_ -sku Runtime -Architecture x86
+ $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010)
}
}
- $windowsDesktopRuntimeVersions | Sort-Object | Get-Unique |% {
- if ($PSCmdlet.ShouldProcess(".NET Windows Desktop $_", "Install")) {
+ $windowsDesktopRuntimeVersions | Sort-Object -Unique |% {
+ if (!(Test-DotNetRuntimeInstalled -DotNetRoot $DotNetInstallDir -Runtime Microsoft.WindowsDesktop.App -Version $_) -and
+ $PSCmdlet.ShouldProcess(".NET Windows Desktop $_ ($arch)", "Install")) {
Install-DotNet -Version $_ -sku WindowsDesktop -Architecture $arch
$restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010)
+ }
- if ($IncludeX86) {
- Install-DotNet -Version $_ -sku WindowsDesktop -Architecture x86
- $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010)
- }
+ if ($IncludeX86 -and
+ !(Test-DotNetRuntimeInstalled -DotNetRoot $DotNetX86InstallDir -Runtime Microsoft.WindowsDesktop.App -Version $_) -and
+ $PSCmdlet.ShouldProcess(".NET Windows Desktop $_ (x86)", "Install")) {
+ Install-DotNet -Version $_ -sku WindowsDesktop -Architecture x86
+ $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010)
}
}
- $aspnetRuntimeVersions | Sort-Object | Get-Unique |% {
- if ($PSCmdlet.ShouldProcess("ASP.NET Core $_", "Install")) {
+ $aspnetRuntimeVersions | Sort-Object -Unique |% {
+ if (!(Test-DotNetRuntimeInstalled -DotNetRoot $DotNetInstallDir -Runtime Microsoft.AspNetCore.App -Version $_) -and
+ $PSCmdlet.ShouldProcess("ASP.NET Core $_ ($arch)", "Install")) {
Install-DotNet -Version $_ -sku AspNetCore -Architecture $arch
$restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010)
+ }
- if ($IncludeX86) {
- Install-DotNet -Version $_ -sku AspNetCore -Architecture x86
- $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010)
- }
+ if ($IncludeX86 -and
+ !(Test-DotNetRuntimeInstalled -DotNetRoot $DotNetX86InstallDir -Runtime Microsoft.AspNetCore.App -Version $_) -and
+ $PSCmdlet.ShouldProcess("ASP.NET Core $_ (x86)", "Install")) {
+ Install-DotNet -Version $_ -sku AspNetCore -Architecture x86
+ $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010)
}
}
if ($restartRequired) {
@@ -284,18 +369,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.
@@ -309,19 +391,21 @@ $global:LASTEXITCODE = 0
$sdks |% {
if ($_.Version) { $parameters = '-Version', $_.Version } else { $parameters = '-Channel', $_.Channel }
- if ($PSCmdlet.ShouldProcess(".NET SDK $_ ($arch)", "Install")) {
- $anythingInstalled = $true
- Invoke-Expression -Command "$DotNetInstallScriptPathExpression $parameters -Architecture $arch -InstallDir $DotNetInstallDir $switches"
+ if (!(Test-DotNetSdkInstalled -DotNetRoot $DotNetInstallDir -Version $_.Version -Channel $_.Channel)) {
+ if ($PSCmdlet.ShouldProcess(".NET SDK $_ ($arch)", "Install")) {
+ $anythingInstalled = $true
+ Invoke-Expression -Command "$DotNetInstallScriptPathExpression $parameters -Architecture $arch -InstallDir $DotNetInstallDir $switches"
- if ($LASTEXITCODE -ne 0) {
- Write-Error ".NET SDK installation failure: $LASTEXITCODE"
- exit $LASTEXITCODE
+ if ($LASTEXITCODE -ne 0) {
+ Write-Error ".NET SDK installation failure: $LASTEXITCODE"
+ exit $LASTEXITCODE
+ }
+ } else {
+ Invoke-Expression -Command "$DotNetInstallScriptPathExpression $parameters -Architecture $arch -InstallDir $DotNetInstallDir $switches -DryRun"
}
- } else {
- Invoke-Expression -Command "$DotNetInstallScriptPathExpression $parameters -Architecture $arch -InstallDir $DotNetInstallDir $switches -DryRun"
}
- if ($IncludeX86) {
+ if ($IncludeX86 -and !(Test-DotNetSdkInstalled -DotNetRoot $DotNetX86InstallDir -Version $_.Version -Channel $_.Channel)) {
if ($PSCmdlet.ShouldProcess(".NET x86 SDK $_", "Install")) {
$anythingInstalled = $true
Invoke-Expression -Command "$DotNetInstallScriptPathExpression $parameters -Architecture x86 -InstallDir $DotNetX86InstallDir $switches"
@@ -339,19 +423,21 @@ $sdks |% {
$dotnetRuntimeSwitches = $switches + '-Runtime','dotnet'
$runtimeVersions | Sort-Object -Unique |% {
- if ($PSCmdlet.ShouldProcess(".NET $Arch runtime $_", "Install")) {
- $anythingInstalled = $true
- Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $dotnetRuntimeSwitches"
+ if (!(Test-DotNetRuntimeInstalled -DotNetRoot $DotNetInstallDir -Runtime Microsoft.NETCore.App -Version $_)) {
+ if ($PSCmdlet.ShouldProcess(".NET $Arch runtime $_", "Install")) {
+ $anythingInstalled = $true
+ Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $dotnetRuntimeSwitches"
- if ($LASTEXITCODE -ne 0) {
- Write-Error ".NET SDK installation failure: $LASTEXITCODE"
- exit $LASTEXITCODE
+ if ($LASTEXITCODE -ne 0) {
+ Write-Error ".NET SDK installation failure: $LASTEXITCODE"
+ exit $LASTEXITCODE
+ }
+ } else {
+ Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $dotnetRuntimeSwitches -DryRun"
}
- } else {
- Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $dotnetRuntimeSwitches -DryRun"
}
- if ($IncludeX86) {
+ if ($IncludeX86 -and !(Test-DotNetRuntimeInstalled -DotNetRoot $DotNetX86InstallDir -Runtime Microsoft.NETCore.App -Version $_)) {
if ($PSCmdlet.ShouldProcess(".NET x86 runtime $_", "Install")) {
$anythingInstalled = $true
Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture x86 -InstallDir $DotNetX86InstallDir $dotnetRuntimeSwitches"
@@ -369,19 +455,21 @@ $runtimeVersions | Sort-Object -Unique |% {
$windowsDesktopRuntimeSwitches = $switches + '-Runtime','windowsdesktop'
$windowsDesktopRuntimeVersions | Sort-Object -Unique |% {
- if ($PSCmdlet.ShouldProcess(".NET WindowsDesktop $arch runtime $_", "Install")) {
- $anythingInstalled = $true
- Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $windowsDesktopRuntimeSwitches"
+ if (!(Test-DotNetRuntimeInstalled -DotNetRoot $DotNetInstallDir -Runtime Microsoft.WindowsDesktop.App -Version $_)) {
+ if ($PSCmdlet.ShouldProcess(".NET WindowsDesktop $arch runtime $_", "Install")) {
+ $anythingInstalled = $true
+ Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $windowsDesktopRuntimeSwitches"
- if ($LASTEXITCODE -ne 0) {
- Write-Error ".NET SDK installation failure: $LASTEXITCODE"
- exit $LASTEXITCODE
+ if ($LASTEXITCODE -ne 0) {
+ Write-Error ".NET SDK installation failure: $LASTEXITCODE"
+ exit $LASTEXITCODE
+ }
+ } else {
+ Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $windowsDesktopRuntimeSwitches -DryRun"
}
- } else {
- Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $windowsDesktopRuntimeSwitches -DryRun"
}
- if ($IncludeX86) {
+ if ($IncludeX86 -and !(Test-DotNetRuntimeInstalled -DotNetRoot $DotNetX86InstallDir -Runtime Microsoft.WindowsDesktop.App -Version $_)) {
if ($PSCmdlet.ShouldProcess(".NET WindowsDesktop x86 runtime $_", "Install")) {
$anythingInstalled = $true
Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture x86 -InstallDir $DotNetX86InstallDir $windowsDesktopRuntimeSwitches"
@@ -399,19 +487,21 @@ $windowsDesktopRuntimeVersions | Sort-Object -Unique |% {
$aspnetRuntimeSwitches = $switches + '-Runtime','aspnetcore'
$aspnetRuntimeVersions | Sort-Object -Unique |% {
- if ($PSCmdlet.ShouldProcess(".NET ASP.NET Core $arch runtime $_", "Install")) {
- $anythingInstalled = $true
- Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $aspnetRuntimeSwitches"
+ if (!(Test-DotNetRuntimeInstalled -DotNetRoot $DotNetInstallDir -Runtime Microsoft.AspNetCore.App -Version $_)) {
+ if ($PSCmdlet.ShouldProcess(".NET ASP.NET Core $arch runtime $_", "Install")) {
+ $anythingInstalled = $true
+ Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $aspnetRuntimeSwitches"
- if ($LASTEXITCODE -ne 0) {
- Write-Error ".NET SDK installation failure: $LASTEXITCODE"
- exit $LASTEXITCODE
+ if ($LASTEXITCODE -ne 0) {
+ Write-Error ".NET SDK installation failure: $LASTEXITCODE"
+ exit $LASTEXITCODE
+ }
+ } else {
+ Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $aspnetRuntimeSwitches -DryRun"
}
- } else {
- Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture $arch -InstallDir $DotNetInstallDir $aspnetRuntimeSwitches -DryRun"
}
- if ($IncludeX86) {
+ if ($IncludeX86 -and !(Test-DotNetRuntimeInstalled -DotNetRoot $DotNetX86InstallDir -Runtime Microsoft.AspNetCore.App -Version $_)) {
if ($PSCmdlet.ShouldProcess(".NET ASP.NET Core x86 runtime $_", "Install")) {
$anythingInstalled = $true
Invoke-Expression -Command "$DotNetInstallScriptPathExpression -Channel $_ -Architecture x86 -InstallDir $DotNetX86InstallDir $aspnetRuntimeSwitches"
diff --git a/tools/Set-EnvVars.ps1 b/tools/Set-EnvVars.ps1
index 2bd3ca4d..c85d6941 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 00000000..bc0cba65
--- /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/artifacts/testResults.ps1 b/tools/artifacts/testResults.ps1
index a841967e..d96c8847 100644
--- a/tools/artifacts/testResults.ps1
+++ b/tools/artifacts/testResults.ps1
@@ -4,14 +4,25 @@ Param(
$result = @{}
+# Hang and crash dumps are copied into the TRX attachment directory when a TRX report is requested.
+# Prefer that copy to avoid uploading the same dump twice, but keep the original when no attachment
+# copy exists (e.g. GitHub Actions runs, which don't request a TRX report) so crashes stay diagnosable.
+function Select-TestResultFile($files) {
+ $attachedDumpNames = @($files | Where-Object { $_.Extension -eq '.dmp' -and $_.FullName -match '[/\\]In[/\\]' } | ForEach-Object { $_.Name })
+ $files | Where-Object { $_.Extension -ne '.dmp' -or $_.FullName -match '[/\\]In[/\\]' -or $attachedDumpNames -notcontains $_.Name }
+}
+
$RepoRoot = Resolve-Path "$PSScriptRoot\..\.."
$testRoot = Join-Path $RepoRoot test
-$result[$testRoot] = (Get-ChildItem "$testRoot\TestResults" -Recurse -Directory | Get-ChildItem -Recurse -File)
+$legacyTestResults = Join-Path $testRoot TestResults
+if (Test-Path $legacyTestResults) {
+ $result[$testRoot] = Select-TestResultFile @(Get-ChildItem $legacyTestResults -Recurse -Directory | Get-ChildItem -Recurse -File)
+}
$artifactStaging = & "$PSScriptRoot/../Get-ArtifactsStagingDirectory.ps1"
$testlogsPath = Join-Path $artifactStaging "test_logs"
if (Test-Path $testlogsPath) {
- $result[$testlogsPath] = Get-ChildItem $testlogsPath -Recurse;
+ $result[$testlogsPath] = Select-TestResultFile @(Get-ChildItem $testlogsPath -Recurse)
}
$result
diff --git a/tools/dirs.proj b/tools/dirs.proj
new file mode 100644
index 00000000..7b266e21
--- /dev/null
+++ b/tools/dirs.proj
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/tools/dotnet-install.ps1 b/tools/dotnet-install.ps1
new file mode 100644
index 00000000..e62264b1
--- /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 00000000..6180745e
--- /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/dotnet-test-cloud.ps1 b/tools/dotnet-test-cloud.ps1
index 5d204819..1a000768 100644
--- a/tools/dotnet-test-cloud.ps1
+++ b/tools/dotnet-test-cloud.ps1
@@ -5,6 +5,8 @@
Runs tests as they are run in cloud test runs.
.PARAMETER Configuration
The configuration within which to run tests
+.PARAMETER IncludeNativeAOT
+ Runs the NativeAOT-compiled tests and fails if the expected image is missing.
.PARAMETER Agent
The name of the agent. This is used in preparing test run titles.
.PARAMETER PublishResults
@@ -13,14 +15,18 @@
A switch to run the tests in an x86 process.
.PARAMETER dotnet32
The path to a 32-bit dotnet executable to use.
+.PARAMETER NoCoverage
+ A switch to skip code coverage collection.
#>
[CmdletBinding()]
Param(
[string]$Configuration='Debug',
+ [switch]$IncludeNativeAOT,
[string]$Agent='Local',
[switch]$PublishResults,
[switch]$x86,
- [string]$dotnet32
+ [string]$dotnet32,
+ [switch]$NoCoverage
)
$RepoRoot = (Resolve-Path "$PSScriptRoot/..").Path
@@ -40,13 +46,16 @@ if ($x86) {
Write-Host "Running tests using `"$dotnet`"" -ForegroundColor DarkGray
} else {
Write-Error "Unable to find 32-bit dotnet.exe"
- return 1
+ exit 1
}
}
}
$testBinLog = Join-Path $ArtifactStagingFolder (Join-Path build_logs test.binlog)
$testLogs = Join-Path $ArtifactStagingFolder test_logs
+if (Test-Path -LiteralPath $testLogs) {
+ Remove-Item -LiteralPath $testLogs -Recurse -Force
+}
$globalJson = Get-Content $PSScriptRoot/../global.json | ConvertFrom-Json
$isMTP = $globalJson.test.runner -eq 'Microsoft.Testing.Platform'
@@ -58,12 +67,14 @@ if ($isMTP) {
$dumpSwitches = @(
,'--hangdump'
- ,'--hangdump-timeout','120s'
+ ,'--hangdump-timeout','5m'
,'--crashdump'
+ ,'--crashdump-type','Heap'
+ # The native crash report accompanies the dump and is often the only way to identify the
+ # faulting thread and instruction when a test host dies of an access violation on Linux.
+ ,'--crash-report-if-supported'
)
$mtpArgs = @(
- ,'--coverage'
- ,'--coverage-output-format','cobertura'
,'--diagnostic'
,'--diagnostic-output-directory',$testLogs
,'--diagnostic-verbosity','Information'
@@ -71,31 +82,78 @@ if ($isMTP) {
,'--report-trx'
)
- & $dotnet test --solution $RepoRoot `
+ if (-not $NoCoverage) {
+ $mtpArgs += @(
+ ,'--coverage'
+ ,'--coverage-output-format','cobertura'
+ ,'--coverage-settings',"$PSScriptRoot/test.runsettings"
+ )
+ }
+
+ $solutionFiles = @(Get-ChildItem -LiteralPath $RepoRoot -File | Where-Object { $_.Extension -in '.sln', '.slnx' })
+ if ($solutionFiles.Count -ne 1) {
+ throw "Expected exactly one solution file in $RepoRoot, but found $($solutionFiles.Count)."
+ }
+
+ $solutionPath = $solutionFiles[0].FullName
+ & $dotnet test $solutionPath `
--no-build `
-c $Configuration `
-bl:"$testBinLog" `
- --filter-not-trait 'TestCategory=FailsInCloudTest' `
- --coverage-settings "$PSScriptRoot/test.runsettings" `
+ -- `
@mtpArgs `
@dumpSwitches `
@extraArgs
if ($LASTEXITCODE -ne 0) { $failedTests += 1 }
+ if ($IncludeNativeAOT) {
+ $nativeAotTests = @(& "$PSScriptRoot/Get-NativeAOTTestProjects.ps1" -Configuration $Configuration)
+ foreach ($nativeAotTest in $nativeAotTests) {
+ $testExecutable = $nativeAotTest.ExecutablePath
+ if (-not (Test-Path -LiteralPath $testExecutable -PathType Leaf)) {
+ Write-Error "Expected NativeAOT TUnit test executable '$testExecutable' was not found."
+ $failedTests += 1
+ continue
+ }
+
+ $nativeAotArgs = @(
+ ,'--diagnostic'
+ ,'--diagnostic-output-directory',$testLogs
+ ,'--diagnostic-verbosity','Information'
+ ,'--results-directory',$testLogs
+ ,'--report-trx'
+ ,'--report-trx-filename',"$($nativeAotTest.ProjectName)_$($nativeAotTest.TargetFramework)_NativeAOT_{arch}.trx"
+ )
+ if ($IsWindows) {
+ $nativeAotArgs += $dumpSwitches
+ }
+ Write-Host "Running NativeAOT TUnit tests from '$testExecutable'." -ForegroundColor Cyan
+ & $testExecutable @nativeAotArgs @extraArgs
+ if ($LASTEXITCODE -ne 0) { $failedTests += 1 }
+ }
+ }
+
$trxFiles = Get-ChildItem -Recurse -Path $testLogs\*.trx
} else {
$testDiagLog = Join-Path $ArtifactStagingFolder (Join-Path test_logs diag.log)
+ $coverageArgs = @()
+ if (-not $NoCoverage) {
+ $coverageArgs = @(
+ ,'--collect','Code Coverage;Format=cobertura'
+ ,'--settings',"$PSScriptRoot/test.runsettings"
+ )
+ }
+
& $dotnet test $RepoRoot `
--no-build `
-c $Configuration `
--filter "TestCategory!=FailsInCloudTest" `
- --collect "Code Coverage;Format=cobertura" `
- --settings "$PSScriptRoot/test.runsettings" `
--blame-hang-timeout 60s `
--blame-crash `
-bl:"$testBinLog" `
--diag "$testDiagLog;TraceLevel=info" `
--logger trx `
+ @coverageArgs `
@extraArgs
if ($LASTEXITCODE -ne 0) { $failedTests += 1 }
diff --git a/tools/publish-CodeCov.ps1 b/tools/publish-CodeCov.ps1
index 1d736511..d9bc81db 100644
--- a/tools/publish-CodeCov.ps1
+++ b/tools/publish-CodeCov.ps1
@@ -21,10 +21,35 @@ Param (
)
$RepoRoot = (Resolve-Path "$PSScriptRoot/..").Path
+$codeCovTool = & "$PSScriptRoot/Get-CodeCovTool.ps1"
+$coverageFiles = @(Get-ChildItem -Recurse -LiteralPath $PathToCodeCoverage -Filter "*.cobertura.xml")
+if ($coverageFiles.Count -eq 0) {
+ return
+}
-Get-ChildItem -Recurse -LiteralPath $PathToCodeCoverage -Filter "*.cobertura.xml" | % {
- $relativeFilePath = Resolve-Path -relative $_.FullName
-
+$arguments = @(
+ "upload-process",
+ "--disable-search",
+ "--fail-on-error",
+ "-t", $CodeCovToken,
+ "--network-root-folder", $RepoRoot
+)
+foreach ($coverageFile in $coverageFiles) {
+ $relativeFilePath = Resolve-Path -Relative $coverageFile.FullName
Write-Host "Uploading: $relativeFilePath" -ForegroundColor Yellow
- & (& "$PSScriptRoot/Get-CodeCovTool.ps1") -t $CodeCovToken -f $relativeFilePath -R $RepoRoot -F $Flags -n $Name
+ $arguments += @("-f", $relativeFilePath)
+}
+
+if ($Flags) {
+ $arguments += @("-F", $Flags)
+}
+
+if ($Name) {
+ $arguments += @("-n", $Name)
+}
+
+& $codeCovTool @arguments
+if ($LASTEXITCODE -ne 0) {
+ Write-Error "Codecov CLI failed with exit code $LASTEXITCODE."
+ exit $LASTEXITCODE
}
diff --git a/tools/variables/_define.ps1 b/tools/variables/_define.ps1
index d40e5cf1..ba081f0f 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
}