From bd28700ca011b3cb33fd5e509f31444f571e7349 Mon Sep 17 00:00:00 2001 From: Daniel Plaisted Date: Thu, 21 Aug 2025 16:46:21 -0400 Subject: [PATCH 1/4] Add more installer methods --- .../Commands/Sdk/Install/SdkInstallCommand.cs | 123 ++++++++++-------- src/Installer/dnup/DotnetInstaller.cs | 5 + src/Installer/dnup/IDotnetInstaller.cs | 9 ++ 3 files changed, 80 insertions(+), 57 deletions(-) diff --git a/src/Installer/dnup/Commands/Sdk/Install/SdkInstallCommand.cs b/src/Installer/dnup/Commands/Sdk/Install/SdkInstallCommand.cs index 680f10c3a70a..f81ceee91d42 100644 --- a/src/Installer/dnup/Commands/Sdk/Install/SdkInstallCommand.cs +++ b/src/Installer/dnup/Commands/Sdk/Install/SdkInstallCommand.cs @@ -213,73 +213,22 @@ public override int Execute() SpectreAnsiConsole.MarkupInterpolated($"Installing .NET SDK [blue]{resolvedChannelVersion}[/] to [blue]{resolvedInstallPath}[/]..."); - string downloadLink = "https://builds.dotnet.microsoft.com/dotnet/Sdk/9.0.303/dotnet-sdk-9.0.303-win-x64.exe"; - // Download the file to a temp path with progress - using (var httpClient = new System.Net.Http.HttpClient()) - { - SpectreAnsiConsole.Progress() - .Start(ctx => - { - var task = ctx.AddTask($"Downloading .NET SDK {resolvedChannelVersion}"); - - List additionalDownloads = additionalVersionsToInstall.Select(version => - { - var additionalTask = ctx.AddTask($"Downloading .NET SDK {version}"); - return (Action)(() => - { - Download(downloadLink, httpClient, additionalTask); - }); - }).ToList(); - Download(downloadLink, httpClient, task); + SpectreAnsiConsole.Progress() + .Start(ctx => + { + _dotnetInstaller.InstallSdks(resolvedInstallPath, ctx, new[] { resolvedChannelVersion }.Concat(additionalVersionsToInstall)); + }); - foreach (var additionalDownload in additionalDownloads) - { - additionalDownload(); - } - }); - } SpectreAnsiConsole.WriteLine($"Complete!"); return 0; } - void Download(string url, HttpClient httpClient, ProgressTask task) - { - //string tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetFileName(url)); - //using (var response = httpClient.GetAsync(url, System.Net.Http.HttpCompletionOption.ResponseHeadersRead).GetAwaiter().GetResult()) - //{ - // response.EnsureSuccessStatusCode(); - // var contentLength = response.Content.Headers.ContentLength ?? 0; - // using (var stream = response.Content.ReadAsStream()) - // using (var fileStream = File.Create(tempFilePath)) - // { - // var buffer = new byte[81920]; - // long totalRead = 0; - // int read; - // while ((read = stream.Read(buffer, 0, buffer.Length)) > 0) - // { - // fileStream.Write(buffer, 0, read); - // totalRead += read; - // if (contentLength > 0) - // { - // task.Value = (double)totalRead / contentLength * 100; - // } - // } - // task.Value = 100; - // } - //} - - for (int i = 0; i < 100; i++) - { - task.Increment(1); - Thread.Sleep(20); // Simulate some work - } - task.Value = 100; - } + string? ResolveChannelFromGlobalJson(string globalJsonPath) { @@ -330,6 +279,66 @@ public SdkInstallType GetConfiguredInstallType(out string? currentInstallPath) } return latestAdminVersion; } + + public void InstallSdks(string dotnetRoot, ProgressContext progressContext, IEnumerable sdkVersions) + { + //var task = progressContext.AddTask($"Downloading .NET SDK {resolvedChannelVersion}"); + using (var httpClient = new System.Net.Http.HttpClient()) + { + List downloads = sdkVersions.Select(version => + { + string downloadLink = "https://builds.dotnet.microsoft.com/dotnet/Sdk/9.0.303/dotnet-sdk-9.0.303-win-x64.exe"; + var task = progressContext.AddTask($"Downloading .NET SDK {version}"); + return (Action)(() => + { + Download(downloadLink, httpClient, task); + }); + }).ToList(); + + + foreach (var download in downloads) + { + download(); + } + } + } + + void Download(string url, HttpClient httpClient, ProgressTask task) + { + //string tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetFileName(url)); + //using (var response = httpClient.GetAsync(url, System.Net.Http.HttpCompletionOption.ResponseHeadersRead).GetAwaiter().GetResult()) + //{ + // response.EnsureSuccessStatusCode(); + // var contentLength = response.Content.Headers.ContentLength ?? 0; + // using (var stream = response.Content.ReadAsStream()) + // using (var fileStream = File.Create(tempFilePath)) + // { + // var buffer = new byte[81920]; + // long totalRead = 0; + // int read; + // while ((read = stream.Read(buffer, 0, buffer.Length)) > 0) + // { + // fileStream.Write(buffer, 0, read); + // totalRead += read; + // if (contentLength > 0) + // { + // task.Value = (double)totalRead / contentLength * 100; + // } + // } + // task.Value = 100; + // } + //} + + for (int i = 0; i < 100; i++) + { + task.Increment(1); + Thread.Sleep(20); // Simulate some work + } + task.Value = 100; + } + + public void UpdateGlobalJson(string globalJsonPath, string? sdkVersion = null, bool? allowPrerelease = null, string? rollForward = null) => throw new NotImplementedException(); + public void ConfigureInstallType(SdkInstallType installType, string? dotnetRoot = null) => throw new NotImplementedException(); } class EnvironmentVariableMockReleaseInfoProvider : IReleaseInfoProvider diff --git a/src/Installer/dnup/DotnetInstaller.cs b/src/Installer/dnup/DotnetInstaller.cs index 34b104dba6cc..120412b1e8ab 100644 --- a/src/Installer/dnup/DotnetInstaller.cs +++ b/src/Installer/dnup/DotnetInstaller.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Text.Json; using Microsoft.DotNet.Cli.Utils; +using Spectre.Console; namespace Microsoft.DotNet.Tools.Bootstrapper; @@ -99,4 +100,8 @@ public GlobalJsonInfo GetGlobalJsonInfo(string initialDirectory) // TODO: Implement this return null; } + + public void InstallSdks(string dotnetRoot, ProgressContext progressContext, IEnumerable sdkVersions) => throw new NotImplementedException(); + public void UpdateGlobalJson(string globalJsonPath, string? sdkVersion = null, bool? allowPrerelease = null, string? rollForward = null) => throw new NotImplementedException(); + public void ConfigureInstallType(SdkInstallType installType, string? dotnetRoot = null) => throw new NotImplementedException(); } diff --git a/src/Installer/dnup/IDotnetInstaller.cs b/src/Installer/dnup/IDotnetInstaller.cs index 443c9a12666c..47affcc51390 100644 --- a/src/Installer/dnup/IDotnetInstaller.cs +++ b/src/Installer/dnup/IDotnetInstaller.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Text; +using Spectre.Console; namespace Microsoft.DotNet.Tools.Bootstrapper; @@ -16,6 +17,14 @@ public interface IDotnetInstaller SdkInstallType GetConfiguredInstallType(out string? currentInstallPath); string? GetLatestInstalledAdminVersion(); + + void InstallSdks(string dotnetRoot, ProgressContext progressContext, IEnumerable sdkVersions); + + void UpdateGlobalJson(string globalJsonPath, string? sdkVersion = null, bool? allowPrerelease = null, string? rollForward = null); + + void ConfigureInstallType(SdkInstallType installType, string? dotnetRoot = null); + + } public enum SdkInstallType From 8e2bdf9b8f963724f28623e8a425a1041bf3368f Mon Sep 17 00:00:00 2001 From: Daniel Plaisted Date: Fri, 22 Aug 2025 08:50:10 -0400 Subject: [PATCH 2/4] Add calls to new methods --- .../Commands/Sdk/Install/SdkInstallCommand.cs | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/Installer/dnup/Commands/Sdk/Install/SdkInstallCommand.cs b/src/Installer/dnup/Commands/Sdk/Install/SdkInstallCommand.cs index f81ceee91d42..590eab9f3f48 100644 --- a/src/Installer/dnup/Commands/Sdk/Install/SdkInstallCommand.cs +++ b/src/Installer/dnup/Commands/Sdk/Install/SdkInstallCommand.cs @@ -210,10 +210,11 @@ public override int Execute() } + // TODO: Implement transaction / rollback? + // TODO: Use Mutex to avoid concurrent installs? - SpectreAnsiConsole.MarkupInterpolated($"Installing .NET SDK [blue]{resolvedChannelVersion}[/] to [blue]{resolvedInstallPath}[/]..."); - // Download the file to a temp path with progress + SpectreAnsiConsole.MarkupInterpolated($"Installing .NET SDK [blue]{resolvedChannelVersion}[/] to [blue]{resolvedInstallPath}[/]..."); SpectreAnsiConsole.Progress() .Start(ctx => @@ -221,6 +222,16 @@ public override int Execute() _dotnetInstaller.InstallSdks(resolvedInstallPath, ctx, new[] { resolvedChannelVersion }.Concat(additionalVersionsToInstall)); }); + if (resolvedSetDefaultInstall == true) + { + _dotnetInstaller.ConfigureInstallType(SdkInstallType.User, resolvedInstallPath); + } + + if (resolvedUpdateGlobalJson == true) + { + _dotnetInstaller.UpdateGlobalJson(globalJsonInfo!.GlobalJsonPath!, resolvedChannelVersion, globalJsonInfo.AllowPrerelease, globalJsonInfo.RollForward); + } + SpectreAnsiConsole.WriteLine($"Complete!"); @@ -337,8 +348,14 @@ void Download(string url, HttpClient httpClient, ProgressTask task) task.Value = 100; } - public void UpdateGlobalJson(string globalJsonPath, string? sdkVersion = null, bool? allowPrerelease = null, string? rollForward = null) => throw new NotImplementedException(); - public void ConfigureInstallType(SdkInstallType installType, string? dotnetRoot = null) => throw new NotImplementedException(); + public void UpdateGlobalJson(string globalJsonPath, string? sdkVersion = null, bool? allowPrerelease = null, string? rollForward = null) + { + SpectreAnsiConsole.WriteLine($"Updating {globalJsonPath} to SDK version {sdkVersion} (AllowPrerelease={allowPrerelease}, RollForward={rollForward})"); + } + public void ConfigureInstallType(SdkInstallType installType, string? dotnetRoot = null) + { + SpectreAnsiConsole.WriteLine($"Configuring install type to {installType} (dotnetRoot={dotnetRoot})"); + } } class EnvironmentVariableMockReleaseInfoProvider : IReleaseInfoProvider From 19f42c85c366e15e9133a4f55347d1468d9b0810 Mon Sep 17 00:00:00 2001 From: Daniel Plaisted Date: Fri, 22 Aug 2025 09:15:53 -0400 Subject: [PATCH 3/4] Initial ConfigureInstallType implementation Copilot prompt: Implement the #method:'Microsoft.DotNet.Tools.Bootstrapper.DotnetInstaller.ConfigureInstallType':4479-4606 method. If the install type is user, remove any other folder with dotnet in it from the PATH and add the dotnetRoot to the PATH. Also set the DOTNET_ROOT environment variable to dotnetRoot. If the install type is Admin, unset DOTNET_ROOT, and add dotnetRoot to the path (removing any other dotnet folder from the path). If the install type is None, unset DOTNET_ROOT and remove any dotnet folder from the path. For any other install type, throw an ArgumentException. --- src/Installer/dnup/DotnetInstaller.cs | 39 ++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/Installer/dnup/DotnetInstaller.cs b/src/Installer/dnup/DotnetInstaller.cs index 120412b1e8ab..2ce205a030ea 100644 --- a/src/Installer/dnup/DotnetInstaller.cs +++ b/src/Installer/dnup/DotnetInstaller.cs @@ -103,5 +103,42 @@ public GlobalJsonInfo GetGlobalJsonInfo(string initialDirectory) public void InstallSdks(string dotnetRoot, ProgressContext progressContext, IEnumerable sdkVersions) => throw new NotImplementedException(); public void UpdateGlobalJson(string globalJsonPath, string? sdkVersion = null, bool? allowPrerelease = null, string? rollForward = null) => throw new NotImplementedException(); - public void ConfigureInstallType(SdkInstallType installType, string? dotnetRoot = null) => throw new NotImplementedException(); + + public void ConfigureInstallType(SdkInstallType installType, string? dotnetRoot = null) + { + // Get current PATH + var path = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.User) ?? string.Empty; + var pathEntries = path.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries).ToList(); + // Remove all entries containing "dotnet" (case-insensitive) + pathEntries = pathEntries.Where(p => !p.Contains("dotnet", StringComparison.OrdinalIgnoreCase)).ToList(); + + switch (installType) + { + case SdkInstallType.User: + if (string.IsNullOrEmpty(dotnetRoot)) + throw new ArgumentNullException(nameof(dotnetRoot)); + // Add dotnetRoot to PATH + pathEntries.Insert(0, dotnetRoot); + // Set DOTNET_ROOT + Environment.SetEnvironmentVariable("DOTNET_ROOT", dotnetRoot, EnvironmentVariableTarget.User); + break; + case SdkInstallType.Admin: + if (string.IsNullOrEmpty(dotnetRoot)) + throw new ArgumentNullException(nameof(dotnetRoot)); + // Add dotnetRoot to PATH + pathEntries.Insert(0, dotnetRoot); + // Unset DOTNET_ROOT + Environment.SetEnvironmentVariable("DOTNET_ROOT", null, EnvironmentVariableTarget.User); + break; + case SdkInstallType.None: + // Unset DOTNET_ROOT + Environment.SetEnvironmentVariable("DOTNET_ROOT", null, EnvironmentVariableTarget.User); + break; + default: + throw new ArgumentException($"Unknown install type: {installType}", nameof(installType)); + } + // Update PATH + var newPath = string.Join(Path.PathSeparator, pathEntries); + Environment.SetEnvironmentVariable("PATH", newPath, EnvironmentVariableTarget.User); + } } From a559c31c9c677d5b8498cee12aeaec4a5937e44f Mon Sep 17 00:00:00 2001 From: Daniel Plaisted Date: Fri, 22 Aug 2025 09:19:22 -0400 Subject: [PATCH 4/4] Fix check for dotnet folder in PATH Copilot prompt: In #method:'Microsoft.DotNet.Tools.Bootstrapper.DotnetInstaller.ConfigureInstallType':4481-6459 what I meant by removing a folder from the path if it has dotnet in it was that you should check the contents of each folder and if it is a dotnet installation folder then it should be removed from the path. A simple way to check if it's a dotnet installation folder is if it has a dotnet executable in it. --- src/Installer/dnup/DotnetInstaller.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Installer/dnup/DotnetInstaller.cs b/src/Installer/dnup/DotnetInstaller.cs index 2ce205a030ea..5c4a7ccce83f 100644 --- a/src/Installer/dnup/DotnetInstaller.cs +++ b/src/Installer/dnup/DotnetInstaller.cs @@ -109,8 +109,9 @@ public void ConfigureInstallType(SdkInstallType installType, string? dotnetRoot // Get current PATH var path = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.User) ?? string.Empty; var pathEntries = path.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries).ToList(); - // Remove all entries containing "dotnet" (case-insensitive) - pathEntries = pathEntries.Where(p => !p.Contains("dotnet", StringComparison.OrdinalIgnoreCase)).ToList(); + string exeName = OperatingSystem.IsWindows() ? "dotnet.exe" : "dotnet"; + // Remove only actual dotnet installation folders from PATH + pathEntries = pathEntries.Where(p => !File.Exists(Path.Combine(p, exeName))).ToList(); switch (installType) {