Skip to content
Draft
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
Copy link
Member

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.

Copy link
Contributor Author

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() and WriteUserPath() methods to WindowsPathHelper for registry-based user PATH access. Created generic AddPathEntry() helper that:

  • Takes both unexpanded and expanded PATH
  • Uses expanded PATH for detection
  • Modifies unexpanded PATH to preserve environment variables

Updated SetUserInstallRoot() to use these new methods on Windows, with a fallback to Environment API for non-Windows platforms.

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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, we need to get the unexpanded path and modify that (which is what we do for the admin path in WindowsPathHelper).

Reuse existing helper methods and refactor them as necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit b182662. Created generic RemovePathEntry() helper method that:

  • Takes both unexpanded and expanded PATH
  • Finds indices to remove using the expanded PATH
  • Removes those indices from the unexpanded PATH to preserve environment variables

Updated SetAdminInstallRoot() to use ReadUserPath(), RemovePathEntry(), and WriteUserPath() for registry-based operations that preserve unexpanded environment variables.

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;
}
}
4 changes: 4 additions & 0 deletions src/Installer/dotnetup/Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
using Microsoft.DotNet.Tools.Bootstrapper.Commands.Sdk;
using Microsoft.DotNet.Tools.Bootstrapper.Commands.Sdk.Install;
using Microsoft.DotNet.Tools.Bootstrapper.Commands.Sdk.Update;
using Microsoft.DotNet.Tools.Bootstrapper.Commands.ElevatedAdminPath;
using Microsoft.DotNet.Tools.Bootstrapper.Commands.SetInstallRoot;

namespace Microsoft.DotNet.Tools.Bootstrapper
{
Expand Down Expand Up @@ -38,6 +40,8 @@ private static RootCommand ConfigureCommandLine(RootCommand rootCommand)
rootCommand.Subcommands.Add(SdkCommandParser.GetCommand());
rootCommand.Subcommands.Add(SdkInstallCommandParser.GetRootInstallCommand());
rootCommand.Subcommands.Add(SdkUpdateCommandParser.GetRootUpdateCommand());
rootCommand.Subcommands.Add(ElevatedAdminPathCommandParser.GetCommand());
rootCommand.Subcommands.Add(SetInstallRootCommandParser.GetCommand());

return rootCommand;
}
Expand Down
Loading
Loading