diff --git a/Makefile b/Makefile
index 85c28361c68..99427070154 100644
--- a/Makefile
+++ b/Makefile
@@ -121,11 +121,15 @@ list-nunit-tests:
include build-tools/scripts/runtime-helpers.mk
.PHONY: prepare
-prepare:
+prepare: install-dotnet
$(call SYSTEM_DOTNET_BINLOG,prepare-run,run) $(PREPARE_MSBUILD_FLAGS) --project "$(PREPARE_PROJECT)" --framework $(PREPARE_NET_FX) -- $(_PREPARE_ARGS)
$(call SYSTEM_DOTNET_BINLOG,prepare-bootstrap) Xamarin.Android.BootstrapTasks.sln
$(call DOTNET_BINLOG,prepare-java.interop) $(SOLUTION) -t:PrepareJavaInterop
+.PHONY: install-dotnet
+install-dotnet:
+ bash ./eng/install-dotnet.sh
+
.PHONY: prepare-help
prepare-help:
$(call SYSTEM_DOTNET_BINLOG,prepare-help,run) --project "$(PREPARE_PROJECT)" --framework $(PREPARE_NET_FX) -- -h
diff --git a/build-tools/scripts/PrepareWindows.targets b/build-tools/scripts/PrepareWindows.targets
index bcd6144a4f3..0875e4bf558 100644
--- a/build-tools/scripts/PrepareWindows.targets
+++ b/build-tools/scripts/PrepareWindows.targets
@@ -9,7 +9,13 @@
<_XAPrepareStandardArgs Condition=" '$(XA_FORCE_COMPONENT_REFRESH)' == 'true' ">$(_XAPrepareStandardArgs) -refresh
-
+
+
+
+
- /// Path to a local .NET SDK archive to use instead of downloading.
- ///
- public string? LocalDotNetSdkArchive { get; set; }
-
///
/// Determines whether or not we are running on a hosted azure pipelines agent.
/// These agents have certain limitations, the most pressing being the amount of available storage.
diff --git a/build-tools/xaprepare/xaprepare/ConfigAndData/Configurables.Unix.cs b/build-tools/xaprepare/xaprepare/ConfigAndData/Configurables.Unix.cs
index 01b91fbcf68..b1654a89b42 100644
--- a/build-tools/xaprepare/xaprepare/ConfigAndData/Configurables.Unix.cs
+++ b/build-tools/xaprepare/xaprepare/ConfigAndData/Configurables.Unix.cs
@@ -9,14 +9,5 @@ partial class Defaults
{
public const string DefaultCompiler = "cc";
}
-
- partial class Urls
- {
- // This is the "public" url that we really should be using, but it keeps failing with:
- // AuthenticationException: The remote certificate is invalid because of errors in the certificate chain: RevocationStatusUnknown
- // For now we'll grab it directly from GitHub
- // public static readonly Uri DotNetInstallScript = new Uri ("https://builds.dotnet.microsoft.com/dotnet/scripts/v1/dotnet-install.sh");
- public static readonly Uri DotNetInstallScript = new Uri ("https://builds.dotnet.microsoft.com/dotnet/scripts/v1/dotnet-install.sh");
- }
}
}
diff --git a/build-tools/xaprepare/xaprepare/ConfigAndData/Configurables.Windows.cs b/build-tools/xaprepare/xaprepare/ConfigAndData/Configurables.Windows.cs
index 7c7a025398f..d59cfe637d6 100644
--- a/build-tools/xaprepare/xaprepare/ConfigAndData/Configurables.Windows.cs
+++ b/build-tools/xaprepare/xaprepare/ConfigAndData/Configurables.Windows.cs
@@ -13,10 +13,5 @@ partial class Defaults
partial class Paths
{
}
-
- partial class Urls
- {
- public static readonly Uri DotNetInstallScript = new Uri ("https://builds.dotnet.microsoft.com/dotnet/scripts/v1/dotnet-install.ps1");
- }
}
}
diff --git a/build-tools/xaprepare/xaprepare/Main.cs b/build-tools/xaprepare/xaprepare/Main.cs
index d9157457413..4a65478c8be 100644
--- a/build-tools/xaprepare/xaprepare/Main.cs
+++ b/build-tools/xaprepare/xaprepare/Main.cs
@@ -27,7 +27,6 @@ sealed class ParsedOptions
public string? Configuration { get; set; }
public bool AutoProvision { get; set; }
public bool AutoProvisionUsesSudo { get; set; }
- public string? LocalDotNetSdkArchive { get; set; }
}
public static int Main (string[] args)
@@ -98,7 +97,6 @@ static async Task Run (string[] args)
"",
{"auto-provision=", $"Automatically install software required by .NET for Android", v => parsedOptions.AutoProvision = ParseBoolean (v)},
{"auto-provision-uses-sudo=", $"Allow use of sudo(1) when provisioning", v => parsedOptions.AutoProvisionUsesSudo = ParseBoolean (v)},
- {"dotnet-sdk-archive=", "Path to a local .NET SDK archive (zip or tar.gz) to use instead of downloading from the internet.", v => parsedOptions.LocalDotNetSdkArchive = v?.Trim () },
"",
{"h|help", "Show this help message", v => parsedOptions.ShowHelp = true },
};
@@ -128,7 +126,6 @@ static async Task Run (string[] args)
Context.Instance.DebugFileExtension = parsedOptions.DebugFileExtension;
Context.Instance.AutoProvision = parsedOptions.AutoProvision;
Context.Instance.AutoProvisionUsesSudo = parsedOptions.AutoProvisionUsesSudo;
- Context.Instance.LocalDotNetSdkArchive = parsedOptions.LocalDotNetSdkArchive;
if (!String.IsNullOrEmpty (parsedOptions.Configuration))
Context.Instance.Configuration = parsedOptions.Configuration!;
diff --git a/build-tools/xaprepare/xaprepare/Scenarios/Scenario_AndroidTestDependencies.cs b/build-tools/xaprepare/xaprepare/Scenarios/Scenario_AndroidTestDependencies.cs
index ca0c311f2d2..0c9ec4b62c0 100644
--- a/build-tools/xaprepare/xaprepare/Scenarios/Scenario_AndroidTestDependencies.cs
+++ b/build-tools/xaprepare/xaprepare/Scenarios/Scenario_AndroidTestDependencies.cs
@@ -15,7 +15,7 @@ protected Scenario_AndroidTestDependencies (string name, string description)
protected override void AddSteps (Context context)
{
- Steps.Add (new Step_InstallDotNetPreview ());
+ Steps.Add (new Step_PrepareDotNetWorkloads ());
// disable installation of missing programs...
context.SetCondition (KnownConditions.AllowProgramInstallation, false);
diff --git a/build-tools/xaprepare/xaprepare/Scenarios/Scenario_Standard.cs b/build-tools/xaprepare/xaprepare/Scenarios/Scenario_Standard.cs
index 862c752e07a..dbc8015809b 100644
--- a/build-tools/xaprepare/xaprepare/Scenarios/Scenario_Standard.cs
+++ b/build-tools/xaprepare/xaprepare/Scenarios/Scenario_Standard.cs
@@ -18,7 +18,7 @@ protected override void AddSteps (Context context)
if (context == null)
throw new ArgumentNullException (nameof (context));
- Steps.Add (new Step_InstallDotNetPreview ());
+ Steps.Add (new Step_PrepareDotNetWorkloads ());
Steps.Add (new Step_GenerateFiles (atBuildStart: true));
Steps.Add (new Step_GenerateCGManifest ());
}
diff --git a/build-tools/xaprepare/xaprepare/Steps/Step_InstallDotNetPreview.cs b/build-tools/xaprepare/xaprepare/Steps/Step_InstallDotNetPreview.cs
deleted file mode 100644
index 7d53a9b5330..00000000000
--- a/build-tools/xaprepare/xaprepare/Steps/Step_InstallDotNetPreview.cs
+++ /dev/null
@@ -1,220 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Threading.Tasks;
-
-namespace Xamarin.Android.Prepare
-{
- class Step_InstallDotNetPreview : StepWithDownloadProgress, IBuildInventoryItem
- {
- public string BuildToolName => "dotnet-preview";
- public string BuildToolVersion => Context.Instance.Properties.GetRequiredValue (KnownProperties.MicrosoftDotnetSdkInternalPackageVersion);
-
- public Step_InstallDotNetPreview ()
- : base ("Install required .NET Preview SDK locally")
- { }
-
- protected override async Task Execute (Context context)
- {
- var dotnetPath = Configurables.Paths.DotNetPreviewPath;
- dotnetPath = dotnetPath.TrimEnd (new char [] { Path.DirectorySeparatorChar });
-
- // Check if a local SDK archive was specified
- if (!String.IsNullOrEmpty (context.LocalDotNetSdkArchive)) {
- if (!await InstallDotNetFromLocalArchiveAsync (context, dotnetPath, context.LocalDotNetSdkArchive!)) {
- Log.ErrorLine ($"Installation of dotnet SDK from local archive '{context.LocalDotNetSdkArchive}' failed.");
- return false;
- }
- } else if (!await InstallDotNetAsync (context, dotnetPath, BuildToolVersion, useCachedInstallScript: true) &&
- !await InstallDotNetAsync (context, dotnetPath, BuildToolVersion, useCachedInstallScript: false)) {
- Log.ErrorLine ($"Installation of dotnet SDK '{BuildToolVersion}' failed.");
- return false;
- }
-
- AddToInventory ();
-
- // Delete all relevant NuGet package install directories, as we could possibly be using a new runtime commit with a previously installed version (6.0.0)
- var runtimeDirs = Directory.GetDirectories (Configurables.Paths.XAPackagesDir, "microsoft.netcore.app.runtime.mono.android*");
- var packageDirsToRemove = new List (runtimeDirs);
- packageDirsToRemove.Add (Configurables.Paths.MicrosoftNETWorkloadMonoPackageDir);
- foreach (var packageDir in packageDirsToRemove) {
- if (Directory.Exists (packageDir)) {
- Utilities.DeleteDirectory (packageDir);
- }
- }
-
- // Install runtime packs associated with the SDK previously installed.
- var packageDownloadProj = Path.Combine (BuildPaths.XamarinAndroidSourceRoot, "build-tools", "xaprepare", "xaprepare", "package-download.proj");
- var logPathBase = Path.Combine (Configurables.Paths.BuildBinDir, $"msbuild-{context.BuildTimeStamp}-download-runtime-packs");
-
- const int maxAttempts = 3;
- const int initialBackoffDelayMilliseconds = 2000;
- bool restoreSucceeded = false;
- for (int attempt = 1; attempt <= maxAttempts; attempt++) {
- var logPath = $"{logPathBase}-attempt{attempt}.binlog";
- var runner = new ProcessRunner (Configurables.Paths.DotNetPreviewTool, "restore",
- packageDownloadProj,
- "--configfile", Path.Combine (BuildPaths.XamarinAndroidSourceRoot, "NuGet.config"),
- $"-bl:{logPath}",
- "--verbosity", "normal"
- ) {
- EchoStandardOutput = true,
- EchoStandardError = true,
- };
- if (runner.Run ()) {
- restoreSucceeded = true;
- break;
- }
- if (attempt < maxAttempts) {
- Log.WarningLine ($"Failed to restore runtime packs (attempt {attempt}/{maxAttempts}), retrying...");
- var delayMilliseconds = initialBackoffDelayMilliseconds * (1 << (attempt - 1));
- await Task.Delay (delayMilliseconds);
- }
- }
- if (!restoreSucceeded) {
- Log.ErrorLine ($"Failed to restore runtime packs using '{packageDownloadProj}' after {maxAttempts} attempts.");
- return false;
- }
-
- var sdk_manifests = Path.Combine (dotnetPath, "sdk-manifests");
-
- // Copy the WorkloadManifest.* files from the latest Microsoft.NET.Workload.* listed in package-download.proj
- var dotnets = new [] { "net6", "net7", "net8", "net9", "net10", "current" };
- foreach (var dotnet in dotnets) {
- var destination = Path.Combine (sdk_manifests,
- context.Properties.GetRequiredValue (KnownProperties.DotNetMonoManifestVersionBand),
- $"microsoft.net.workload.mono.toolchain.{dotnet}",
- context.Properties.GetRequiredValue (KnownProperties.MicrosoftNETWorkloadMonoToolChainPackageVersion));
- Utilities.DeleteDirectory (destination, recurse: true);
- foreach (var file in Directory.GetFiles (string.Format (Configurables.Paths.MicrosoftNETWorkloadMonoToolChainDir, dotnet), "*")) {
- Utilities.CopyFileToDir (file, destination);
- }
- destination = Path.Combine (sdk_manifests,
- context.Properties.GetRequiredValue (KnownProperties.DotNetEmscriptenManifestVersionBand),
- $"microsoft.net.workload.emscripten.{dotnet}",
- context.Properties.GetRequiredValue (KnownProperties.MicrosoftNETWorkloadEmscriptenPackageVersion));
- Utilities.DeleteDirectory (destination, recurse: true);
- foreach (var file in Directory.GetFiles (string.Format (Configurables.Paths.MicrosoftNETWorkloadEmscriptenDir, dotnet), "*")) {
- Utilities.CopyFileToDir (file, destination);
- }
- }
-
- return true;
- }
-
- async Task DownloadDotNetInstallScript (Context context, string dotnetScriptPath, Uri dotnetScriptUrl, bool useCachedInstallScript)
- {
- string tempDotnetScriptPath = dotnetScriptPath + "-tmp";
- Utilities.DeleteFile (tempDotnetScriptPath);
-
- Log.StatusLine ("Downloading dotnet-install script...");
-
- if (useCachedInstallScript && File.Exists (dotnetScriptPath)) {
- Log.StatusLine ($"Using cached installation script found in '{dotnetScriptPath}'");
- return true;
- }
- Utilities.DeleteFile (dotnetScriptPath);
-
- Log.StatusLine ($" {context.Characters.Link} {dotnetScriptUrl}", ConsoleColor.White);
- await Utilities.Download (dotnetScriptUrl, tempDotnetScriptPath, DownloadStatus.Empty);
-
- if (File.Exists (tempDotnetScriptPath)) {
- Utilities.CopyFile (tempDotnetScriptPath, dotnetScriptPath);
- Utilities.DeleteFile (tempDotnetScriptPath);
- return true;
- }
-
- if (File.Exists (dotnetScriptPath)) {
- Log.WarningLine ($"Download of dotnet-install from '{dotnetScriptUrl}' failed");
- Log.StatusLine ($"Using cached installation script found in '{dotnetScriptPath}'");
- return true;
- } else {
- Log.ErrorLine ($"Download of dotnet-install from '{dotnetScriptUrl}' failed");
- return false;
- }
- }
-
- string[] GetInstallationScriptArgs (string version, string dotnetPath, string dotnetScriptPath, bool runtimeOnly)
- {
- List args;
- if (Context.IsWindows) {
- args = new List {
- "-NoProfile", "-ExecutionPolicy", "unrestricted", "-file", dotnetScriptPath,
- "-Version", version, "-InstallDir", dotnetPath, "-Verbose"
- };
- if (runtimeOnly) {
- args.AddRange (new string [] { "-Runtime", "dotnet" });
- }
- return args.ToArray ();
- }
-
- args = new List {
- dotnetScriptPath, "--version", version, "--install-dir", dotnetPath, "--verbose"
- };
-
- if (runtimeOnly) {
- args.AddRange (new string [] { "--runtime", "dotnet" });
- }
- return args.ToArray ();
- }
-
- async Task InstallDotNetFromLocalArchiveAsync (Context context, string dotnetPath, string archivePath)
- {
- if (!File.Exists (archivePath)) {
- Log.ErrorLine ($"Local .NET SDK archive not found: '{archivePath}'");
- return false;
- }
-
- Log.StatusLine ($"Installing .NET SDK from local archive: {archivePath}");
-
- // Always delete the bin/$(Configuration)/dotnet/ directory
- Utilities.DeleteDirectory (dotnetPath);
-
- return await Utilities.Unpack (archivePath, dotnetPath);
- }
-
- // Standardize on dotnet/arcade's usage of the dotnet-install scripts:
- // invoke dotnet-install.{sh,ps1} directly with --version/--install-dir
- // rather than harvesting URLs and downloading/extracting the archive
- // ourselves. This matches Arcade's eng/common/tools.sh and our CI
- // (build-tools/automation/yaml-templates/use-dot-net.yaml), lets the
- // script perform its built-in SHA-512 verification of the archive, and
- // relies on the script's own "already installed" check for incremental
- // re-runs (no need to re-download when the requested version is present).
- async Task InstallDotNetAsync (Context context, string dotnetPath, string version, bool useCachedInstallScript, bool runtimeOnly = false)
- {
- string cacheDir = context.Properties.GetRequiredValue (KnownProperties.AndroidToolchainCacheDirectory);
-
- Uri dotnetScriptUrl = Configurables.Urls.DotNetInstallScript;
- string scriptFileName = Path.GetFileName (dotnetScriptUrl.LocalPath);
- string cachedDotnetScriptPath = Path.Combine (cacheDir, scriptFileName);
- if (!await DownloadDotNetInstallScript (context, cachedDotnetScriptPath, dotnetScriptUrl, useCachedInstallScript)) {
- return false;
- }
-
- Directory.CreateDirectory (dotnetPath);
- string dotnetScriptPath = Path.Combine (dotnetPath, scriptFileName);
- Utilities.CopyFile (cachedDotnetScriptPath, dotnetScriptPath);
-
- var type = runtimeOnly ? "runtime" : "SDK";
- Log.StatusLine ($"Installing dotnet {type} '{version}'...");
-
- string scriptCommand = Context.IsWindows ? "powershell.exe" : "bash";
- string[] scriptArgs = GetInstallationScriptArgs (version, dotnetPath, dotnetScriptPath, runtimeOnly);
- return Utilities.RunCommand (scriptCommand, scriptArgs);
- }
-
- bool TestDotNetSdk (string dotnetTool)
- {
- return Utilities.RunCommand (dotnetTool, new string [] { "--version" });
- }
-
- public void AddToInventory ()
- {
- if (!string.IsNullOrEmpty (BuildToolName) && !string.IsNullOrEmpty (BuildToolVersion) && !Context.Instance.BuildToolsInventory.ContainsKey (BuildToolName)) {
- Context.Instance.BuildToolsInventory.Add (BuildToolName, BuildToolVersion);
- }
- }
-
- }
-}
diff --git a/build-tools/xaprepare/xaprepare/Steps/Step_PrepareDotNetWorkloads.cs b/build-tools/xaprepare/xaprepare/Steps/Step_PrepareDotNetWorkloads.cs
new file mode 100644
index 00000000000..cf26a254ac8
--- /dev/null
+++ b/build-tools/xaprepare/xaprepare/Steps/Step_PrepareDotNetWorkloads.cs
@@ -0,0 +1,120 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace Xamarin.Android.Prepare
+{
+ // Prepares the Android-specific workloads against the .NET SDK that
+ // `eng/install-dotnet.{sh,ps1}` (Arcade's `eng/common/tools.{sh,ps1}`)
+ // has already provisioned at `bin/$(Configuration)/dotnet/`:
+ // * Cleans stale runtime/workload NuGet directories.
+ // * Restores `package-download.proj` to pull down the Mono Android
+ // runtime packs and the Mono/Emscripten workload manifest packages.
+ // * Copies the workload manifests into the local SDK's `sdk-manifests/`
+ // so the SDK can resolve the workloads against the locally installed
+ // runtime packs.
+ //
+ // The SDK itself is installed by Arcade's `eng/common/tools.{sh,ps1}`
+ // via `eng/install-dotnet.{sh,ps1}` invoked from the `Prepare` MSBuild
+ // target (Windows) and `make prepare` (Unix). This step assumes the SDK
+ // already exists at `Configurables.Paths.DotNetPreviewPath`.
+ class Step_PrepareDotNetWorkloads : StepWithDownloadProgress, IBuildInventoryItem
+ {
+ public string BuildToolName => "dotnet-preview";
+ public string BuildToolVersion => Context.Instance.Properties.GetRequiredValue (KnownProperties.MicrosoftDotnetSdkInternalPackageVersion);
+
+ public Step_PrepareDotNetWorkloads ()
+ : base ("Prepare .NET workloads against the locally installed SDK")
+ { }
+
+ protected override async Task Execute (Context context)
+ {
+ var dotnetPath = Configurables.Paths.DotNetPreviewPath;
+ dotnetPath = dotnetPath.TrimEnd (new char [] { Path.DirectorySeparatorChar });
+
+ var dotnetTool = Configurables.Paths.DotNetPreviewTool;
+ if (!Directory.Exists (dotnetPath)) {
+ Log.ErrorLine ($"Expected .NET SDK at '{dotnetPath}' but the directory does not exist. Run `eng/install-dotnet.{{sh,ps1}}` (or `make prepare`) first.");
+ return false;
+ }
+
+ AddToInventory ();
+
+ // Delete all relevant NuGet package install directories, as we could possibly be using a new runtime commit with a previously installed version (6.0.0)
+ var runtimeDirs = Directory.GetDirectories (Configurables.Paths.XAPackagesDir, "microsoft.netcore.app.runtime.mono.android*");
+ var packageDirsToRemove = new List (runtimeDirs);
+ packageDirsToRemove.Add (Configurables.Paths.MicrosoftNETWorkloadMonoPackageDir);
+ foreach (var packageDir in packageDirsToRemove) {
+ if (Directory.Exists (packageDir)) {
+ Utilities.DeleteDirectory (packageDir);
+ }
+ }
+
+ // Install runtime packs associated with the SDK previously installed.
+ var packageDownloadProj = Path.Combine (BuildPaths.XamarinAndroidSourceRoot, "build-tools", "xaprepare", "xaprepare", "package-download.proj");
+ var logPathBase = Path.Combine (Configurables.Paths.BuildBinDir, $"msbuild-{context.BuildTimeStamp}-download-runtime-packs");
+
+ const int maxAttempts = 3;
+ const int initialBackoffDelayMilliseconds = 2000;
+ bool restoreSucceeded = false;
+ for (int attempt = 1; attempt <= maxAttempts; attempt++) {
+ var logPath = $"{logPathBase}-attempt{attempt}.binlog";
+ var runner = new ProcessRunner (dotnetTool, "restore",
+ packageDownloadProj,
+ "--configfile", Path.Combine (BuildPaths.XamarinAndroidSourceRoot, "NuGet.config"),
+ $"-bl:{logPath}",
+ "--verbosity", "normal"
+ ) {
+ EchoStandardOutput = true,
+ EchoStandardError = true,
+ };
+ if (runner.Run ()) {
+ restoreSucceeded = true;
+ break;
+ }
+ if (attempt < maxAttempts) {
+ Log.WarningLine ($"Failed to restore runtime packs (attempt {attempt}/{maxAttempts}), retrying...");
+ var delayMilliseconds = initialBackoffDelayMilliseconds * (1 << (attempt - 1));
+ await Task.Delay (delayMilliseconds);
+ }
+ }
+ if (!restoreSucceeded) {
+ Log.ErrorLine ($"Failed to restore runtime packs using '{packageDownloadProj}' after {maxAttempts} attempts.");
+ return false;
+ }
+
+ var sdk_manifests = Path.Combine (dotnetPath, "sdk-manifests");
+
+ // Copy the WorkloadManifest.* files from the latest Microsoft.NET.Workload.* listed in package-download.proj
+ var dotnets = new [] { "net6", "net7", "net8", "net9", "net10", "current" };
+ foreach (var dotnet in dotnets) {
+ var destination = Path.Combine (sdk_manifests,
+ context.Properties.GetRequiredValue (KnownProperties.DotNetMonoManifestVersionBand),
+ $"microsoft.net.workload.mono.toolchain.{dotnet}",
+ context.Properties.GetRequiredValue (KnownProperties.MicrosoftNETWorkloadMonoToolChainPackageVersion));
+ Utilities.DeleteDirectory (destination, recurse: true);
+ foreach (var file in Directory.GetFiles (string.Format (Configurables.Paths.MicrosoftNETWorkloadMonoToolChainDir, dotnet), "*")) {
+ Utilities.CopyFileToDir (file, destination);
+ }
+ destination = Path.Combine (sdk_manifests,
+ context.Properties.GetRequiredValue (KnownProperties.DotNetEmscriptenManifestVersionBand),
+ $"microsoft.net.workload.emscripten.{dotnet}",
+ context.Properties.GetRequiredValue (KnownProperties.MicrosoftNETWorkloadEmscriptenPackageVersion));
+ Utilities.DeleteDirectory (destination, recurse: true);
+ foreach (var file in Directory.GetFiles (string.Format (Configurables.Paths.MicrosoftNETWorkloadEmscriptenDir, dotnet), "*")) {
+ Utilities.CopyFileToDir (file, destination);
+ }
+ }
+
+ return true;
+ }
+
+ public void AddToInventory ()
+ {
+ if (!string.IsNullOrEmpty (BuildToolName) && !string.IsNullOrEmpty (BuildToolVersion) && !Context.Instance.BuildToolsInventory.ContainsKey (BuildToolName)) {
+ Context.Instance.BuildToolsInventory.Add (BuildToolName, BuildToolVersion);
+ }
+ }
+ }
+}
diff --git a/eng/install-dotnet.ps1 b/eng/install-dotnet.ps1
new file mode 100644
index 00000000000..6eaa2a4e44d
--- /dev/null
+++ b/eng/install-dotnet.ps1
@@ -0,0 +1,43 @@
+<#
+.SYNOPSIS
+ Provisions the .NET SDK into bin\$Configuration\dotnet\.
+
+.DESCRIPTION
+ The SDK version is read from eng\Versions.props (single source of truth
+ kept up to date by darc when Microsoft.NET.Sdk flows from dotnet/dotnet),
+ so global.json does not need a 'tools.dotnet' pin.
+#>
+[CmdletBinding(PositionalBinding=$false)]
+param(
+ [string][Alias('c')] $configuration = $(if ($env:CONFIGURATION) { $env:CONFIGURATION } else { 'Debug' })
+)
+
+$ErrorActionPreference = 'Stop'
+
+$scriptroot = Split-Path -Parent $MyInvocation.MyCommand.Path
+$repoRoot = (Resolve-Path (Join-Path $scriptroot '..')).Path
+
+$versionsProps = Join-Path $repoRoot 'eng\Versions.props'
+[xml] $versionsXml = Get-Content -LiteralPath $versionsProps
+$sdkVersion = $versionsXml.SelectSingleNode('//MicrosoftNETSdkPackageVersion').InnerText
+if ([string]::IsNullOrWhiteSpace($sdkVersion)) {
+ Write-Error "Could not read from $versionsProps"
+ exit 1
+}
+
+$installDir = Join-Path $repoRoot "bin\$configuration\dotnet"
+New-Item -ItemType Directory -Force -Path $installDir | Out-Null
+
+# Download Microsoft's official dotnet-install.ps1 (cached under $installDir
+# to avoid hitting the CDN on idempotent re-runs).
+$installScript = Join-Path $installDir 'dotnet-install.ps1'
+if (-not (Test-Path $installScript)) {
+ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+ Invoke-WebRequest -Uri 'https://builds.dotnet.microsoft.com/dotnet/scripts/v1/dotnet-install.ps1' -OutFile $installScript -UseBasicParsing
+}
+
+Write-Host "Installing .NET SDK $sdkVersion into $installDir"
+& $installScript -Version $sdkVersion -InstallDir $installDir -NoPath
+if ($LASTEXITCODE -ne 0) {
+ exit $LASTEXITCODE
+}
diff --git a/eng/install-dotnet.sh b/eng/install-dotnet.sh
new file mode 100644
index 00000000000..da3fe042a62
--- /dev/null
+++ b/eng/install-dotnet.sh
@@ -0,0 +1,40 @@
+#!/usr/bin/env bash
+#
+# Provisions the .NET SDK into bin/$Configuration/dotnet/.
+#
+# The SDK version is read from eng/Versions.props (single source of truth
+# kept up to date by darc when Microsoft.NET.Sdk flows from dotnet/dotnet),
+# so global.json does not need a 'tools.dotnet' pin.
+#
+# Inputs (env vars):
+# CONFIGURATION - Debug (default) or Release; controls install path.
+#
+
+set -euo pipefail
+
+scriptroot="$( cd -P "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+repo_root="$( cd -P "$scriptroot/.." && pwd )"
+
+configuration="${CONFIGURATION:-Debug}"
+
+versions_props="$repo_root/eng/Versions.props"
+sdk_version="$(sed -n 's|.*\([^<]*\).*|\1|p' "$versions_props" | head -n 1)"
+if [[ -z "$sdk_version" ]]; then
+ echo "error: could not read from $versions_props" >&2
+ exit 1
+fi
+
+install_dir="$repo_root/bin/$configuration/dotnet"
+mkdir -p "$install_dir"
+
+# Download Microsoft's official dotnet-install.sh (cached under
+# $install_dir to avoid hitting the CDN on idempotent re-runs). Invoke
+# via `bash` so the executable bit isn't needed (Windows clones often
+# strip it).
+install_script="$install_dir/dotnet-install.sh"
+if [[ ! -f "$install_script" ]]; then
+ curl -fsSL "https://builds.dotnet.microsoft.com/dotnet/scripts/v1/dotnet-install.sh" -o "$install_script"
+fi
+
+echo "Installing .NET SDK $sdk_version into $install_dir"
+bash "$install_script" --version "$sdk_version" --install-dir "$install_dir" --no-path