-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Add Windows admin PATH management commands to dotnetup #52181
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: release/dnup
Are you sure you want to change the base?
Changes from 13 commits
a86586f
542c866
d8fed6a
374d46e
6174dec
3d163b5
5585351
1d91179
bbd3d5c
f4e9278
8aa862f
bd56b89
532dcd0
b182662
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.CommandLine; | ||
| using System.Runtime.InteropServices; | ||
| using System.Runtime.Versioning; | ||
|
|
||
| namespace Microsoft.DotNet.Tools.Bootstrapper.Commands.ElevatedAdminPath; | ||
|
|
||
| internal class ElevatedAdminPathCommand : CommandBase | ||
| { | ||
| private readonly string _operation; | ||
|
|
||
| public ElevatedAdminPathCommand(ParseResult result) : base(result) | ||
| { | ||
| _operation = result.GetValue(ElevatedAdminPathCommandParser.OperationArgument)!; | ||
| } | ||
|
|
||
| public override int Execute() | ||
| { | ||
| // This command only works on Windows | ||
| if (!OperatingSystem.IsWindows()) | ||
| { | ||
| Console.Error.WriteLine("Error: The elevatedadminpath command is only supported on Windows."); | ||
| return 1; | ||
| } | ||
|
|
||
| // Check if running with elevated privileges | ||
| if (!Environment.IsPrivilegedProcess) | ||
| { | ||
| Console.Error.WriteLine("Error: This operation requires administrator privileges. Please run from an elevated command prompt."); | ||
| return 1; | ||
| } | ||
|
|
||
| return _operation.ToLowerInvariant() switch | ||
| { | ||
| "removedotnet" => RemoveDotnet(), | ||
| "adddotnet" => AddDotnet(), | ||
| _ => throw new InvalidOperationException($"Unknown operation: {_operation}") | ||
| }; | ||
| } | ||
|
|
||
| [SupportedOSPlatform("windows")] | ||
| private int RemoveDotnet() | ||
| { | ||
| using var pathHelper = new WindowsPathHelper(); | ||
| return pathHelper.RemoveDotnetFromAdminPath(); | ||
| } | ||
|
|
||
| [SupportedOSPlatform("windows")] | ||
| private int AddDotnet() | ||
| { | ||
| using var pathHelper = new WindowsPathHelper(); | ||
| return pathHelper.AddDotnetToAdminPath(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.CommandLine; | ||
|
|
||
| namespace Microsoft.DotNet.Tools.Bootstrapper.Commands.ElevatedAdminPath; | ||
|
|
||
| internal static class ElevatedAdminPathCommandParser | ||
| { | ||
| public static readonly Argument<string> OperationArgument = new("operation") | ||
| { | ||
| HelpName = "OPERATION", | ||
| Description = "The operation to perform: 'removedotnet' or 'adddotnet'", | ||
| Arity = ArgumentArity.ExactlyOne, | ||
| }; | ||
|
|
||
| private static readonly Command ElevatedAdminPathCommand = ConstructCommand(); | ||
|
|
||
| public static Command GetCommand() | ||
| { | ||
| return ElevatedAdminPathCommand; | ||
| } | ||
|
|
||
| private static Command ConstructCommand() | ||
| { | ||
| Command command = new("elevatedadminpath", "Modifies the machine-wide admin PATH (requires elevated privileges)"); | ||
| command.Hidden = true; | ||
|
|
||
| command.Arguments.Add(OperationArgument); | ||
|
|
||
| command.SetAction(parseResult => new ElevatedAdminPathCommand(parseResult).Execute()); | ||
|
|
||
| return command; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,219 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.CommandLine; | ||
| using System.Runtime.InteropServices; | ||
| using System.Runtime.Versioning; | ||
|
|
||
| namespace Microsoft.DotNet.Tools.Bootstrapper.Commands.SetInstallRoot; | ||
|
|
||
| internal class SetInstallRootCommand : CommandBase | ||
| { | ||
| private readonly string _installType; | ||
| private readonly IDotnetInstallManager _dotnetInstaller; | ||
|
|
||
| public SetInstallRootCommand(ParseResult result, IDotnetInstallManager? dotnetInstaller = null) : base(result) | ||
| { | ||
| _installType = result.GetValue(SetInstallRootCommandParser.InstallTypeArgument)!; | ||
| _dotnetInstaller = dotnetInstaller ?? new DotnetInstallManager(); | ||
| } | ||
|
|
||
| public override int Execute() | ||
| { | ||
| return _installType.ToLowerInvariant() switch | ||
| { | ||
| SetInstallRootCommandParser.UserInstallType => SetUserInstallRoot(), | ||
| SetInstallRootCommandParser.AdminInstallType => SetAdminInstallRoot(), | ||
| _ => throw new InvalidOperationException($"Unknown install type: {_installType}") | ||
| }; | ||
| } | ||
|
|
||
| [SupportedOSPlatform("windows")] | ||
| private bool HandleWindowsAdminPath() | ||
| { | ||
| // Check if admin PATH needs to be changed | ||
| if (WindowsPathHelper.AdminPathContainsProgramFilesDotnet(out var foundDotnetPaths)) | ||
| { | ||
| if (foundDotnetPaths.Count == 1) | ||
| { | ||
| Console.WriteLine($"Removing {foundDotnetPaths[0]} from system PATH."); | ||
| } | ||
| else | ||
| { | ||
| Console.WriteLine("Removing the following dotnet paths from system PATH:"); | ||
| foreach (var path in foundDotnetPaths) | ||
| { | ||
| Console.WriteLine($" - {path}"); | ||
| } | ||
| } | ||
|
|
||
| if (Environment.IsPrivilegedProcess) | ||
| { | ||
| // We're already elevated, modify the admin PATH directly | ||
| using var pathHelper = new WindowsPathHelper(); | ||
| pathHelper.RemoveDotnetFromAdminPath(); | ||
| } | ||
| else | ||
| { | ||
| // Not elevated, shell out to elevated process | ||
| Console.WriteLine("Launching elevated process to modify system PATH..."); | ||
|
|
||
| bool succeeded = WindowsPathHelper.StartElevatedProcess("elevatedadminpath removedotnet"); | ||
| if (!succeeded) | ||
| { | ||
| Console.Error.WriteLine("Warning: Elevation was cancelled. Admin PATH was not modified."); | ||
| return false; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| private int SetUserInstallRoot() | ||
| { | ||
| // Get the default user dotnet installation path | ||
| string userDotnetPath = _dotnetInstaller.GetDefaultDotnetInstallPath(); | ||
|
|
||
| Console.WriteLine($"Setting up user install root at: {userDotnetPath}"); | ||
|
|
||
|
|
||
| // Add the user dotnet path to the user PATH | ||
| try | ||
| { | ||
| // On Windows, check if we need to modify the admin PATH | ||
| if (OperatingSystem.IsWindows()) | ||
| { | ||
| if (!HandleWindowsAdminPath()) | ||
| { | ||
| // UAC prompt was cancelled | ||
| return 1; | ||
| } | ||
| } | ||
|
|
||
| Console.WriteLine($"Adding {userDotnetPath} to user PATH..."); | ||
|
|
||
| var userPath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.User) ?? string.Empty; | ||
| var pathEntries = userPath.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries).ToList(); | ||
|
|
||
| // Check if the user dotnet path is already in the user PATH | ||
| bool alreadyExists = pathEntries.Any(entry => | ||
| Path.TrimEndingDirectorySeparator(entry).Equals( | ||
| Path.TrimEndingDirectorySeparator(userDotnetPath), | ||
| StringComparison.OrdinalIgnoreCase)); | ||
|
|
||
| if (!alreadyExists) | ||
| { | ||
| // Add to the beginning of PATH | ||
| pathEntries.Insert(0, userDotnetPath); | ||
| var newUserPath = string.Join(Path.PathSeparator, pathEntries); | ||
| Environment.SetEnvironmentVariable("PATH", newUserPath, EnvironmentVariableTarget.User); | ||
| Console.WriteLine($"Successfully added {userDotnetPath} to user PATH."); | ||
| } | ||
| else | ||
| { | ||
| Console.WriteLine($"User dotnet path is already in user PATH."); | ||
| } | ||
|
|
||
| // Set DOTNET_ROOT for user | ||
| Environment.SetEnvironmentVariable("DOTNET_ROOT", userDotnetPath, EnvironmentVariableTarget.User); | ||
| Console.WriteLine($"Set DOTNET_ROOT to {userDotnetPath}"); | ||
|
|
||
| Console.WriteLine("User install root configured successfully."); | ||
| Console.WriteLine("Note: You may need to restart your terminal or application for the changes to take effect."); | ||
|
|
||
| return 0; | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| Console.Error.WriteLine($"Error: Failed to configure user install root: {ex.ToString()}"); | ||
| return 1; | ||
| } | ||
| } | ||
|
|
||
| private int SetAdminInstallRoot() | ||
| { | ||
| Console.WriteLine("Setting up admin install root..."); | ||
|
|
||
| // On Windows, add Program Files dotnet path back to admin PATH and remove user settings | ||
| if (OperatingSystem.IsWindows()) | ||
| { | ||
| try | ||
| { | ||
| // Add Program Files dotnet path back to admin PATH | ||
| if (Environment.IsPrivilegedProcess) | ||
| { | ||
| // We're already elevated, modify the admin PATH directly | ||
| Console.WriteLine("Running with elevated privileges. Modifying admin PATH..."); | ||
| using var pathHelper = new WindowsPathHelper(); | ||
| pathHelper.AddDotnetToAdminPath(); | ||
| } | ||
| else | ||
| { | ||
| // Not elevated, shell out to elevated process | ||
| Console.WriteLine("Launching elevated process to modify admin PATH..."); | ||
| try | ||
| { | ||
| bool succeeded = WindowsPathHelper.StartElevatedProcess("elevatedadminpath adddotnet"); | ||
| if (!succeeded) | ||
| { | ||
| Console.Error.WriteLine("Warning: Elevation was cancelled. Admin PATH was not modified."); | ||
| return 1; | ||
| } | ||
| } | ||
| catch (InvalidOperationException ex) | ||
| { | ||
| Console.Error.WriteLine($"Error: Failed to modify admin PATH: {ex.Message}"); | ||
| Console.Error.WriteLine("You may need to manually add the Program Files dotnet path to the system PATH."); | ||
| return 1; | ||
| } | ||
| } | ||
|
|
||
| // Get the user dotnet installation path | ||
| string userDotnetPath = _dotnetInstaller.GetDefaultDotnetInstallPath(); | ||
|
|
||
| // Remove user dotnet path from user PATH | ||
| Console.WriteLine($"Removing {userDotnetPath} from user PATH..."); | ||
| var userPath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.User) ?? string.Empty; | ||
|
||
| var pathEntries = userPath.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries).ToList(); | ||
|
|
||
| // Remove entries that match the user dotnet path | ||
| int removedCount = pathEntries.RemoveAll(entry => | ||
| Path.TrimEndingDirectorySeparator(entry).Equals( | ||
| Path.TrimEndingDirectorySeparator(userDotnetPath), | ||
| StringComparison.OrdinalIgnoreCase)); | ||
|
|
||
| if (removedCount > 0) | ||
| { | ||
| var newUserPath = string.Join(Path.PathSeparator, pathEntries); | ||
| Environment.SetEnvironmentVariable("PATH", newUserPath, EnvironmentVariableTarget.User); | ||
| Console.WriteLine($"Successfully removed {userDotnetPath} from user PATH."); | ||
| } | ||
| else | ||
| { | ||
| Console.WriteLine($"User dotnet path was not found in user PATH."); | ||
| } | ||
|
|
||
| // Unset user DOTNET_ROOT environment variable | ||
| Console.WriteLine("Unsetting user DOTNET_ROOT environment variable..."); | ||
| Environment.SetEnvironmentVariable("DOTNET_ROOT", null, EnvironmentVariableTarget.User); | ||
| Console.WriteLine("Successfully unset DOTNET_ROOT."); | ||
|
|
||
| Console.WriteLine("Admin install root configured successfully."); | ||
| Console.WriteLine("Note: You may need to restart your terminal or application for the changes to take effect."); | ||
|
|
||
| return 0; | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| Console.Error.WriteLine($"Error: Failed to configure admin install root: {ex.Message}"); | ||
| return 1; | ||
| } | ||
| } | ||
| else | ||
| { | ||
| Console.Error.WriteLine("Error: Admin install root is only supported on Windows."); | ||
| return 1; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.CommandLine; | ||
|
|
||
| namespace Microsoft.DotNet.Tools.Bootstrapper.Commands.SetInstallRoot; | ||
|
|
||
| internal static class SetInstallRootCommandParser | ||
| { | ||
| public const string UserInstallType = "user"; | ||
| public const string AdminInstallType = "admin"; | ||
|
|
||
| public static readonly Argument<string> InstallTypeArgument = CreateInstallTypeArgument(); | ||
|
|
||
| private static Argument<string> CreateInstallTypeArgument() | ||
| { | ||
| var argument = new Argument<string>("installtype") | ||
| { | ||
| HelpName = "INSTALL_TYPE", | ||
| Description = "The type of installation root to set: 'user' or 'admin'", | ||
| Arity = ArgumentArity.ExactlyOne, | ||
| }; | ||
| argument.AcceptOnlyFromAmong(UserInstallType, AdminInstallType); | ||
| return argument; | ||
| } | ||
|
|
||
| private static readonly Command SetInstallRootCommand = ConstructCommand(); | ||
|
|
||
| public static Command GetCommand() | ||
| { | ||
| return SetInstallRootCommand; | ||
| } | ||
|
|
||
| private static Command ConstructCommand() | ||
| { | ||
| Command command = new("setinstallroot", "Sets the dotnet installation root"); | ||
|
|
||
| command.Arguments.Add(InstallTypeArgument); | ||
|
|
||
| command.SetAction(parseResult => new SetInstallRootCommand(parseResult).Execute()); | ||
|
|
||
| return command; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here we need to read the unexpanded user PATH value from the registry and modify that. Use helper methods from WindowsPathHelper as possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in commit b182662. Added
ReadUserPath()andWriteUserPath()methods to WindowsPathHelper for registry-based user PATH access. Created genericAddPathEntry()helper that:Updated
SetUserInstallRoot()to use these new methods on Windows, with a fallback to Environment API for non-Windows platforms.