-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Dotnetup: Add logic to configure PATH and other default install settings when installing SDK #52453
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?
Conversation
…oot configuration Co-authored-by: dsplaisted <[email protected]>
Co-authored-by: dsplaisted <[email protected]>
|
@copilot Thanks. Now remove IsOnPath and IsSetAsDotnetRoot from DotnetInstallRootConfiguration. Instead, add an IsFullyConfigured property. Update DotnetInstallManager.GetConfiguredInstallType to be implemented in terms of InstallRootManager. It can call the InstallRootManager to get the changes necessary to switch to User or Admin mode. If either mode needs no changes it can return that type as fully configured. Otherwise, if the PATH resolves to dotnet in one of the expected paths, it can return the corresponding type with IsFullyConfigured set to false. Then update the logic in SdkInstallCommand so that it doesn't initially directly call InstallRootManager to determine what prompts to show. If it does need to make changes, then it can use InstallRootManager at the end. |
…sFullyConfigured property Co-authored-by: dsplaisted <[email protected]>
Co-authored-by: dsplaisted <[email protected]>
Done. Changes implemented in commits f7db809 and 9a331ed:
|
dsplaisted
left a comment
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.
See comments.
Also, update InstallRootManager and the AddPathEntry logic so that it handles the case where the specified pathToAdd is on the path, but there is another path before it that also has a dotnet in it. In that case, move the pathToAdd to the front of the path.
| // If not fully configured, we leave resolvedSetDefaultInstall as null (no prompt) | ||
| // since the install path is already on PATH. The user can run "dotnetup defaultinstall" | ||
| // if they want to fully configure it. |
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.
Update this so that if the user install is not fully configured, it should display the things which need to be configured and prompt whether they should be fixed.
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.
Done in commit dba18fc. When user install is not fully configured, the command now displays what needs to be configured (remove admin paths, add to user PATH, set DOTNET_ROOT) and prompts whether to apply these changes.
| if (resolvedSetDefaultInstall == true) | ||
| { | ||
| _dotnetInstaller.ConfigureInstallType(InstallType.User, resolvedInstallPath); | ||
| if (OperatingSystem.IsWindows()) |
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.
Refactor this so that it calls _dotnetInstaller.ConfigureInstallType on all operating systems, but the implementation on Windows uses InstallRootManager.
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.
Done in commit dba18fc. Refactored ConfigureInstallType to work on all platforms. On Windows, it now uses InstallRootManager internally. SdkInstallCommand now calls _dotnetInstaller.ConfigureInstallType(InstallType.User, resolvedInstallPath) for all platforms.
| string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); | ||
| string programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); | ||
| bool isAdminInstall = installDir.StartsWith(Path.Combine(programFiles, "dotnet"), StringComparison.OrdinalIgnoreCase) || | ||
| installDir.StartsWith(Path.Combine(programFilesX86, "dotnet"), StringComparison.OrdinalIgnoreCase); |
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.
We can skip this logic here. If we don't need any changes to switch to admin mode, then we can return that the admin mode is fully configured.
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.
Done in commit dba18fc. Removed the unnecessary logic - if GetAdminInstallRootChanges() reports no changes needed, we return Admin type as fully configured regardless of path location.
…nstallType on all platforms, improve AddPathEntry logic Co-authored-by: dsplaisted <[email protected]>
Co-authored-by: dsplaisted <[email protected]>
Done in commits dba18fc and ea5d2bb. Updated
|
| } | ||
|
|
||
| bool? resolvedSetDefaultInstall = _setDefaultInstall; | ||
| UserInstallRootChanges? userInstallRootChanges = null; |
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.
Don't declare this here. Only declare it later when it is first used.
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.
Done in commit ef7728b. Moved the declaration to line 158 where it's first used.
| /// <param name="expandedPath">The expanded PATH string to use for detection.</param> | ||
| /// <param name="pathToAdd">The path to add.</param> | ||
| /// <returns>The modified unexpanded PATH string.</returns> | ||
| public static string AddPathEntry(string unexpandedPath, string expandedPath, string pathToAdd) |
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.
Change this method so it also takes a commandName that is supposed to be resolved from the pathToAdd.
The first thing the method should do is to check whether commandName already resolves to an executable in the specified pathToAdd. It should do this using APIs from Microsoft.DotNet.Cli.Utils.EnvironmentProvider. That class will need to be updated so that there is a version of GetCommandPath where you can pass in the search paths (which in this case should come from expandedPath). The same logic that SearchPaths uses to split and clean up the PATH should also be applied.
If the resolved command is already in the pathToAdd, then just return unexpandedPath as is.
Otherwise, check to see if pathToAdd is already in the expandedPath. If it is, then move it to the front and return that. If it isn't, then add it to the front.
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.
Done in commits ef7728b and a1389bb. Updated AddPathEntry to:
- Take
commandNameparameter - Added
GetCommandPath(string commandName, IEnumerable<string> searchPaths, ...)overload toIEnvironmentProviderandEnvironmentProvider - Use
EnvironmentProviderto check if command already resolves topathToAdd- if yes, return unchanged - If not, move existing path to front or add new path to front
The logic properly processes search paths using the same PATH splitting/cleaning logic as SearchPaths.
…on and move variable declaration closer to usage Co-authored-by: dsplaisted <[email protected]>
…ntry Co-authored-by: dsplaisted <[email protected]>
dsplaisted
left a comment
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.
In EnvironmentProvider, factor out the following logic from SearchPaths into a SplitPaths helper method:
.Split(s_pathSeparator)
.Select(p => p.Trim(s_quote))
.Where(p => !string.IsNullOrWhiteSpace(p))
.Select(p => ExpandTildeSlash(p))
Then update WindowsPathHelper.SplitPath call this helper method.
| return commandPath; | ||
| } | ||
|
|
||
| public string? GetCommandPath(string commandName, IEnumerable<string> searchPaths, params string[] extensions) |
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.
Never mind, you can just delete this overload. I didn't realize that the class already had a way to specify custom search paths.
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.
Done in commit bac3323. Removed the overload since the constructor already supports custom search paths.
…undant GetCommandPath overload Co-authored-by: dsplaisted <[email protected]>
Done in commit bac3323. Created |
|
|
||
| string? GetCommandPathFromRootPath(string rootPath, string commandName, IEnumerable<string> extensions); | ||
|
|
||
| IEnumerable<string> SplitPaths(string pathString); |
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.
Don't add this method to the interface. We don't need to go through the interface in WindowsPathHelper where we use it.
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.
Done in commit 941fe48. Removed SplitPaths from the interface since it's only used directly in WindowsPathHelper.
| if (resolvedCommandPath != null) | ||
| { | ||
| var resolvedDir = Path.GetDirectoryName(resolvedCommandPath); | ||
| var normalizedPathToAdd = Path.TrimEndingDirectorySeparator(pathToAdd); |
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.
Move this line before the if statement, and then you can get rid of the normalizedPathToAddFinal variable and just use normalizedPathToAdd.
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.
Done in commit 941fe48. Moved normalizedPathToAdd before the if statement and removed the redundant normalizedPathToAddFinal variable.
| // Check if pathToAdd is already in the expanded PATH | ||
| var normalizedPathToAddFinal = Path.TrimEndingDirectorySeparator(pathToAdd); | ||
| int existingIndex = -1; | ||
| for (int i = 0; i < expandedEntries.Count; i++) |
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.
Rather than using a loop here, use the FindIndex method on List class:
int existingIndex = expandedEntries.FindIndex(expandedEntry => Path.TrimEndingDirectorySeparator(expandedEntry).Equals(normalizedPathToAddFinal, StringComparison.OrdinalIgnoreCase));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.
Done in commit 941fe48. Replaced the manual loop with FindIndex method.
| string installDir = Path.GetDirectoryName(foundDotnet)!; | ||
| var installRoot = new DotnetInstallRoot(installDir, InstallerUtilities.GetDefaultInstallArchitecture()); |
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.
Change this:
| string installDir = Path.GetDirectoryName(foundDotnet)!; | |
| var installRoot = new DotnetInstallRoot(installDir, InstallerUtilities.GetDefaultInstallArchitecture()); | |
| var currentInstallRoot = new DotnetInstallRoot(Path.GetDirectoryName(foundDotnet)!, InstallerUtilities.GetDefaultInstallArchitecture()); |
Then use currentInstallRoot.Path later on instead of installDir.
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.
Done in commit 941fe48. Created currentInstallRoot at the beginning and used currentInstallRoot.Path throughout the method instead of installDir.
…y, use currentInstallRoot consistently Co-authored-by: dsplaisted <[email protected]>
| var unexpandedEntries = SplitPath(unexpandedPath); | ||
|
|
||
| // Check if the command already resolves to the pathToAdd using EnvironmentProvider | ||
| var envProvider = new Microsoft.DotNet.Cli.Utils.EnvironmentProvider(searchPathsOverride: expandedEntries); |
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.
I've seen the use of environment provider in a few places, but IMO it doesn't provide utility if we're new-ing it up every time. The point of the provider is to allow it to be injected (especially during tests) so that we can simulate different configurations without needing to actually modify process-global state. It feels like we're missing method parameters to some of the members, or they need to be instance members of types that take the provider as a constructor parameter or something.
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 I'm not trying to use it as a provider, I just want to use some of the logic that's in it without rewriting / duplicating it.
| { | ||
| // No need to prompt here, the default install is already set up. | ||
| // If the current install is fully configured and matches the resolved path, skip the prompt | ||
| if (currentDotnetInstallRoot.IsFullyConfigured) |
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.
this logic itself makes sense to me, but overall the flow of this execute method seems very long. this is very subjective but since installation in general is a step-by-step process I would expect to see each logic step of that process extracted out to a named method or something so that we can more easily a) verify the steps we expect are happening and b) change a specific step in isolation/narrow the blast radius.
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.
Yes, I'd like to try to refactor this sometime. But it's tricky because I think we want to ask all of the questions first before we take any actions, and there are a lot of dependencies between the questions we ask and the current state.
| // Use InstallRootManager to determine if the install is fully configured | ||
| if (OperatingSystem.IsWindows()) | ||
| { | ||
| var installRootManager = new InstallRootManager(this); |
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.
similar question here - we do we need to new up this during the invocation of this method. if it's a dependency should/can it be provided in the constructor and/or as a method parameter to allow simulating different inputs?
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.
There's a lot of overlap in the functionality between DotnetInstallManager and InstallRootManager, I'm not sure if they even need to be separate classes. I'm thinking of working on support for non-Windows OS's next. I suspect I will refactor this some as part of that, InstallRootManager might change to be a Windows-specific implementation of an interface, with other implementations for other OS's or shells.
| string programFilesPath = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); | ||
| string programFilesX86Path = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); | ||
| bool isAdminPath = currentInstallRoot.Path.StartsWith(Path.Combine(programFilesPath, "dotnet"), StringComparison.OrdinalIgnoreCase) || | ||
| currentInstallRoot.Path.StartsWith(Path.Combine(programFilesX86Path, "dotnet"), StringComparison.OrdinalIgnoreCase); | ||
|
|
||
| return new(installRoot, isAdminInstall ? InstallType.Admin : InstallType.User, IsOnPath: true, isSetAsDotnetRoot); | ||
| return new(currentInstallRoot, isAdminPath ? InstallType.Admin : InstallType.User, IsFullyConfigured: false); | ||
| } | ||
| else | ||
| { | ||
| // For non-Windows platforms, determine based on path location | ||
| // TODO: This should be improved to not be windows-specific https://github.com/dotnet/sdk/issues/51601 | ||
| string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); | ||
| string programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); | ||
| bool isAdminInstall = currentInstallRoot.Path.StartsWith(Path.Combine(programFiles, "dotnet"), StringComparison.OrdinalIgnoreCase) || | ||
| currentInstallRoot.Path.StartsWith(Path.Combine(programFilesX86, "dotnet"), StringComparison.OrdinalIgnoreCase); |
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.
This logic doesn't sit right with me - we have a documented way to detect global installations for all of our primary operating systems, and we should be using it here.
| msg => Console.WriteLine(msg), | ||
| msg => Console.Error.WriteLine(msg)); |
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.
should this be writing to the ANSI console so we can use markup in our error messages (and so we can inject an ANSI console consistently for testing)? same questions for lines 171-172 below.
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.
@copilot Update this and the instance below to use the Spectre APIs.
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.
Done in commit eaab195. Updated to use AnsiConsole.WriteLine and AnsiConsole.MarkupLine with red markup for error messages.
Co-authored-by: dsplaisted <[email protected]>
Summary of Changes
All review feedback has been addressed. Latest change:
Updated error message output to use Spectre APIs: Changed
Console.WriteLineandConsole.Error.WriteLinecalls inConfigureInstallTypeto useAnsiConsole.WriteLineandAnsiConsole.MarkupLinewith red markup for error messages. This provides:All changes build successfully and pass tests.
Original prompt
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.