diff --git a/documentation/general/dotnetup/designs/dotnetup-env.md b/documentation/general/dotnetup/designs/dotnetup-env.md index d64e09808083..e06093507af9 100644 --- a/documentation/general/dotnetup/designs/dotnetup-env.md +++ b/documentation/general/dotnetup/designs/dotnetup-env.md @@ -154,9 +154,11 @@ DotnetAccessMode.Shell = old ShellProfile // shell profile file onl DotnetAccessMode.Everywhere = old FullPathReplacement // env-var PATH + shell profile ``` -The JSON serialization writes the new lowercase names (`none` / `shell` / `everywhere`). -On read, the converter also accepts the legacy spellings (`full`, `fullpathreplacement`, -etc.) so configs written by earlier internal builds keep their chosen mode. +The JSON serialization writes the new lowercase names (`none` / `shell` / `everywhere`) +via the built-in string-enum converter (camel-case, integer values rejected). Legacy +spellings from earlier internal builds (`full` / `fullpathreplacement`, and the old +`pathPreference` property) are **not** accepted: an unrecognized value is treated as a +corrupt config, which re-defaults on the next write rather than silently mapping to a mode. ## `dotnetup` on PATH: an orthogonal setting diff --git a/src/Installer/dotnetup.Library/Commands/Dotnet/DotnetCommand.cs b/src/Installer/dotnetup.Library/Commands/Dotnet/DotnetCommand.cs index e1cf25e7c053..b96e874a085e 100644 --- a/src/Installer/dotnetup.Library/Commands/Dotnet/DotnetCommand.cs +++ b/src/Installer/dotnetup.Library/Commands/Dotnet/DotnetCommand.cs @@ -59,13 +59,13 @@ protected override void ExecuteCore() } /// - /// Resolves the dotnet installation path using the same logic as other dotnetup commands: - /// configured install type (user install) falls back to the default install path. + /// Resolves the dotnet installation path: when the PATH-resolved install is a + /// dotnetup-managed hive, use it; otherwise fall back to the default install path. /// private string ResolveDotnetPath() { var configuredRoot = _dotnetEnvironment.GetCurrentPathConfiguration(); - if (configuredRoot is not null && configuredRoot.InstallType == InstallType.User) + if (configuredRoot is { IsDotnetupHive: true }) { return configuredRoot.Path; } diff --git a/src/Installer/dotnetup.Library/Commands/Init/InitWorkflows.cs b/src/Installer/dotnetup.Library/Commands/Init/InitWorkflows.cs index 67bc5ee3b2f4..0f32ea6d2504 100644 --- a/src/Installer/dotnetup.Library/Commands/Init/InitWorkflows.cs +++ b/src/Installer/dotnetup.Library/Commands/Init/InitWorkflows.cs @@ -446,9 +446,10 @@ internal static List FormatMigrationDisplayItems(List - /// Determines whether the given path is an admin/system-managed .NET install location. - /// These locations are managed by system package managers or OS installers and should not - /// be used by dotnetup for user-level installations. + /// Determines whether the given path is in a well-known system/admin .NET install location + /// (for example Program Files\dotnet on Windows). These locations are usually managed + /// by system package managers or OS installers and should not be used by dotnetup. + /// + /// This is a location heuristic only, it doesn't verify that the install there is actually managed + /// by an OS installer or package manager. /// public static bool IsAdminInstallPath(string path) { diff --git a/src/Installer/dotnetup.Library/Commands/Shared/InstallWorkflow.cs b/src/Installer/dotnetup.Library/Commands/Shared/InstallWorkflow.cs index 16bb3e4b510c..9fd35ac745b6 100644 --- a/src/Installer/dotnetup.Library/Commands/Shared/InstallWorkflow.cs +++ b/src/Installer/dotnetup.Library/Commands/Shared/InstallWorkflow.cs @@ -338,7 +338,7 @@ private void RecordInstallTelemetry( _command.SetCommandTag(TelemetryTagNames.InstallRequestedVersion, VersionSanitizer.Sanitize(requestedVersionOrChannel)); _command.SetCommandTag(TelemetryTagNames.InstallPathExplicit, (explicitInstallPath is not null).ToString()); _command.SetCommandTag(TelemetryTagNames.InstallHasGlobalJson, (globalJson?.GlobalJsonPath is not null).ToString()); - _command.SetCommandTag(TelemetryTagNames.InstallExistingInstallType, currentInstallRoot?.InstallType.ToString() ?? "none"); + _command.SetCommandTag(TelemetryTagNames.InstallExistingInstallType, currentInstallRoot is null ? "none" : InstallPathClassifier.ClassifyInstallPath(currentInstallRoot.Path)); _command.SetCommandTag(TelemetryTagNames.InstallPathType, InstallPathClassifier.ClassifyInstallPath(pathResolution.ResolvedInstallPath, pathResolution.PathSource)); _command.SetCommandTag(TelemetryTagNames.InstallPathSource, pathResolution.PathSource.ToString().ToLowerInvariant()); _command.SetCommandTag(TelemetryTagNames.InstallResolvedVersion, resolved.ResolvedVersion.ToString()); diff --git a/src/Installer/dotnetup.Library/Commands/Shared/UninstallWorkflow.cs b/src/Installer/dotnetup.Library/Commands/Shared/UninstallWorkflow.cs index e8172ff952f2..f505700a28b5 100644 --- a/src/Installer/dotnetup.Library/Commands/Shared/UninstallWorkflow.cs +++ b/src/Installer/dotnetup.Library/Commands/Shared/UninstallWorkflow.cs @@ -155,7 +155,9 @@ private static void CheckAndReportStillPresent( /// /// Resolves the install path for uninstall using the same logic as the install command: - /// only use the configured path if it's a user install, otherwise fall back to default. + /// only use the configured path if it is a dotnetup-managed hive, otherwise fall back to + /// default. This prevents uninstall from targeting a dotnet that dotnetup does not own (e.g. + /// a system install or a hand-extracted dotnet that happens to win on PATH). /// internal static string ResolveInstallPath(string? explicitInstallPath, IDotnetEnvironmentManager dotnetEnvironment) { @@ -165,7 +167,7 @@ internal static string ResolveInstallPath(string? explicitInstallPath, IDotnetEn } var configuredInstall = dotnetEnvironment.GetCurrentPathConfiguration(); - if (configuredInstall is { InstallType: InstallType.User }) + if (configuredInstall is { IsDotnetupHive: true }) { return configuredInstall.Path; } diff --git a/src/Installer/dotnetup.Library/DotnetEnvironmentManager.cs b/src/Installer/dotnetup.Library/DotnetEnvironmentManager.cs index 7df86a37fa97..22b251b52bff 100644 --- a/src/Installer/dotnetup.Library/DotnetEnvironmentManager.cs +++ b/src/Installer/dotnetup.Library/DotnetEnvironmentManager.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Dotnet.Installation.Internal; -using Microsoft.DotNet.Tools.Bootstrapper.Commands.Shared; using Microsoft.DotNet.Tools.Bootstrapper.Shell; using Microsoft.DotNet.Tools.Bootstrapper.Telemetry; using Spectre.Console; @@ -43,11 +42,22 @@ public DotnetEnvironmentManager() ResolveCurrentInstallRootPath(foundDotnet), InstallerUtilities.GetDefaultInstallArchitecture()); - // Classify the resolved dotnet by location. InstallPathClassifier.IsAdminInstallPath is the - // canonical "is this a system/admin-managed install?" check (Program Files on Windows; the - // standard /usr and /opt locations on Unix) used across dotnetup, so both platforms share it. - bool isAdminInstall = InstallPathClassifier.IsAdminInstallPath(currentInstallRoot.Path); - return new(currentInstallRoot, isAdminInstall ? InstallType.System : InstallType.User); + // Report whether the resolved dotnet is a dotnetup-managed hive — an install dotnetup owns + // and may run or uninstall from — rather than classifying it as "system vs user". A dotnet + // that merely lives in a user-writable location (e.g. a hand-extracted C:\dotnet on PATH) is + // NOT dotnetup's and must not be treated as such. Today the only supported hive is the + // default install path; configurable hives are tracked in + // https://github.com/dotnet/sdk/issues/55346, at which point this check would also consult + // the persisted root. + // + // Resolve the default path's symlinks too so the comparison is symmetric: currentInstallRoot.Path + // is already realpath-resolved above, and the default path's base (LocalApplicationData / + // XDG_DATA_HOME, which is not required to be a real directory) may itself be a symlink. Without + // this, a symlinked data directory would make dotnetup fail to recognize its own hive. + string defaultInstallPath = GetDefaultDotnetInstallPath(); + string resolvedDefaultInstallPath = ExecutablePathResolver.ResolveRealPath(defaultInstallPath) ?? defaultInstallPath; + bool isDotnetupHive = DotnetupUtilities.PathsEqual(currentInstallRoot.Path, resolvedDefaultInstallPath); + return new(currentInstallRoot, isDotnetupHive); } public string GetDefaultDotnetInstallPath() diff --git a/src/Installer/dotnetup.Library/DotnetupPaths.cs b/src/Installer/dotnetup.Library/DotnetupPaths.cs index 6024f3cadff6..1f18a75e8de9 100644 --- a/src/Installer/dotnetup.Library/DotnetupPaths.cs +++ b/src/Installer/dotnetup.Library/DotnetupPaths.cs @@ -121,10 +121,19 @@ public static string ManifestPath /// Gets the default dotnet install path managed by dotnetup. /// This is the user-local dotnet root (e.g. %LOCALAPPDATA%\dotnet on Windows). /// + /// + /// Can be overridden via DOTNET_TESTHOOK_DEFAULT_DOTNET_PATH environment variable. + /// public static string DefaultDotnetInstallPath { get { + var overridePath = Environment.GetEnvironmentVariable("DOTNET_TESTHOOK_DEFAULT_DOTNET_PATH"); + if (!string.IsNullOrEmpty(overridePath)) + { + return overridePath; + } + var baseDir = GetBaseDirectory(); if (string.IsNullOrEmpty(baseDir)) { diff --git a/src/Installer/dotnetup.Library/ExecutablePathResolver.cs b/src/Installer/dotnetup.Library/ExecutablePathResolver.cs index 14a8bf1886fa..faf3d9838858 100644 --- a/src/Installer/dotnetup.Library/ExecutablePathResolver.cs +++ b/src/Installer/dotnetup.Library/ExecutablePathResolver.cs @@ -22,13 +22,28 @@ internal static class ExecutablePathResolver /// public static string? ResolveRealDirectory(string? executablePath) { - if (string.IsNullOrEmpty(executablePath)) + string? resolvedPath = ResolveRealPath(executablePath); + return resolvedPath is null ? null : Path.GetDirectoryName(resolvedPath); + } + + /// + /// Returns itself with symlinks resolved (unlike + /// , which resolves the containing directory of an + /// executable). Use this to canonicalize a directory that may be reached through a symlinked + /// parent, such as a dotnet install root under a symlinked LocalApplicationData / + /// XDG_DATA_HOME. Returns null when the path is null or empty. + /// is a no-op on Windows, and on Unix returns + /// null for a path that does not yet exist; in both cases the normalized full path is + /// returned so callers still get a usable value. + /// + public static string? ResolveRealPath(string? path) + { + if (string.IsNullOrEmpty(path)) { return null; } - string fullPath = Path.GetFullPath(executablePath); - string resolvedPath = FileInterop.ResolveRealPath(fullPath) ?? fullPath; - return Path.GetDirectoryName(resolvedPath); + string fullPath = Path.GetFullPath(path); + return FileInterop.ResolveRealPath(fullPath) ?? fullPath; } } diff --git a/src/Installer/dotnetup.Library/IDotnetEnvironmentManager.cs b/src/Installer/dotnetup.Library/IDotnetEnvironmentManager.cs index 051daa11575e..bac21685a2f6 100644 --- a/src/Installer/dotnetup.Library/IDotnetEnvironmentManager.cs +++ b/src/Installer/dotnetup.Library/IDotnetEnvironmentManager.cs @@ -16,6 +16,11 @@ internal interface IDotnetEnvironmentManager { string GetDefaultDotnetInstallPath(); + /// + /// Resolves the dotnet that currently wins on PATH and reports whether it is a + /// dotnetup-managed hive (i.e. an install dotnetup owns and may run or uninstall from). + /// Returns null when no dotnet is found on PATH. + /// DotnetInstallRootConfiguration? GetCurrentPathConfiguration(); string? GetLatestInstalledSystemVersion(); @@ -75,7 +80,7 @@ public string? SdkPath public record DotnetInstallRootConfiguration( DotnetInstallRoot InstallRoot, - InstallType InstallType) + bool IsDotnetupHive) { public string Path => InstallRoot.Path; } diff --git a/src/Installer/dotnetup.Library/Strings.resx b/src/Installer/dotnetup.Library/Strings.resx index 03c4dc3a7deb..ff89c905add8 100644 --- a/src/Installer/dotnetup.Library/Strings.resx +++ b/src/Installer/dotnetup.Library/Strings.resx @@ -163,7 +163,7 @@ Arguments to forward to dotnet. - Error: dotnet executable not found at '{0}'. + dotnet executable not found at '{0}'. Run 'dotnetup install' to install a .NET SDK first. @@ -283,7 +283,7 @@ Or open a new terminal. Open a new terminal for the change to take effect. - Open a new terminal for the change to take effect in other terminals and applications. + Other open terminals and applications will pick up this change after they're restarted. Could not detect the current shell, which is required to update the dotnetup profile entry. Re-run with --shell <{0}> to specify it explicitly. diff --git a/src/Installer/dotnetup.Library/xlf/Strings.cs.xlf b/src/Installer/dotnetup.Library/xlf/Strings.cs.xlf index 79e8e09b1273..f08d1f8a69eb 100644 --- a/src/Installer/dotnetup.Library/xlf/Strings.cs.xlf +++ b/src/Installer/dotnetup.Library/xlf/Strings.cs.xlf @@ -28,8 +28,8 @@ - Error: dotnet executable not found at '{0}'. - Error: dotnet executable not found at '{0}'. + dotnet executable not found at '{0}'. + dotnet executable not found at '{0}'. @@ -107,8 +107,8 @@ Or open a new terminal. {Locked="shell"} - Open a new terminal for the change to take effect in other terminals and applications. - Open a new terminal for the change to take effect in other terminals and applications. + Other open terminals and applications will pick up this change after they're restarted. + Other open terminals and applications will pick up this change after they're restarted. diff --git a/src/Installer/dotnetup.Library/xlf/Strings.de.xlf b/src/Installer/dotnetup.Library/xlf/Strings.de.xlf index 13b9d117311c..5b1496df85b8 100644 --- a/src/Installer/dotnetup.Library/xlf/Strings.de.xlf +++ b/src/Installer/dotnetup.Library/xlf/Strings.de.xlf @@ -28,8 +28,8 @@ - Error: dotnet executable not found at '{0}'. - Error: dotnet executable not found at '{0}'. + dotnet executable not found at '{0}'. + dotnet executable not found at '{0}'. @@ -107,8 +107,8 @@ Or open a new terminal. {Locked="shell"} - Open a new terminal for the change to take effect in other terminals and applications. - Open a new terminal for the change to take effect in other terminals and applications. + Other open terminals and applications will pick up this change after they're restarted. + Other open terminals and applications will pick up this change after they're restarted. diff --git a/src/Installer/dotnetup.Library/xlf/Strings.es.xlf b/src/Installer/dotnetup.Library/xlf/Strings.es.xlf index d1a7bbb6ca1c..77aa652946de 100644 --- a/src/Installer/dotnetup.Library/xlf/Strings.es.xlf +++ b/src/Installer/dotnetup.Library/xlf/Strings.es.xlf @@ -28,8 +28,8 @@ - Error: dotnet executable not found at '{0}'. - Error: dotnet executable not found at '{0}'. + dotnet executable not found at '{0}'. + dotnet executable not found at '{0}'. @@ -107,8 +107,8 @@ Or open a new terminal. {Locked="shell"} - Open a new terminal for the change to take effect in other terminals and applications. - Open a new terminal for the change to take effect in other terminals and applications. + Other open terminals and applications will pick up this change after they're restarted. + Other open terminals and applications will pick up this change after they're restarted. diff --git a/src/Installer/dotnetup.Library/xlf/Strings.fr.xlf b/src/Installer/dotnetup.Library/xlf/Strings.fr.xlf index 0977f5a1c853..b94cbd115ce2 100644 --- a/src/Installer/dotnetup.Library/xlf/Strings.fr.xlf +++ b/src/Installer/dotnetup.Library/xlf/Strings.fr.xlf @@ -28,8 +28,8 @@ - Error: dotnet executable not found at '{0}'. - Error: dotnet executable not found at '{0}'. + dotnet executable not found at '{0}'. + dotnet executable not found at '{0}'. @@ -107,8 +107,8 @@ Or open a new terminal. {Locked="shell"} - Open a new terminal for the change to take effect in other terminals and applications. - Open a new terminal for the change to take effect in other terminals and applications. + Other open terminals and applications will pick up this change after they're restarted. + Other open terminals and applications will pick up this change after they're restarted. diff --git a/src/Installer/dotnetup.Library/xlf/Strings.it.xlf b/src/Installer/dotnetup.Library/xlf/Strings.it.xlf index db404f6a3749..6cdc3c7b7f6a 100644 --- a/src/Installer/dotnetup.Library/xlf/Strings.it.xlf +++ b/src/Installer/dotnetup.Library/xlf/Strings.it.xlf @@ -28,8 +28,8 @@ - Error: dotnet executable not found at '{0}'. - Error: dotnet executable not found at '{0}'. + dotnet executable not found at '{0}'. + dotnet executable not found at '{0}'. @@ -107,8 +107,8 @@ Or open a new terminal. {Locked="shell"} - Open a new terminal for the change to take effect in other terminals and applications. - Open a new terminal for the change to take effect in other terminals and applications. + Other open terminals and applications will pick up this change after they're restarted. + Other open terminals and applications will pick up this change after they're restarted. diff --git a/src/Installer/dotnetup.Library/xlf/Strings.ja.xlf b/src/Installer/dotnetup.Library/xlf/Strings.ja.xlf index 1ca8cec377e8..ca5e8215d4f6 100644 --- a/src/Installer/dotnetup.Library/xlf/Strings.ja.xlf +++ b/src/Installer/dotnetup.Library/xlf/Strings.ja.xlf @@ -28,8 +28,8 @@ - Error: dotnet executable not found at '{0}'. - Error: dotnet executable not found at '{0}'. + dotnet executable not found at '{0}'. + dotnet executable not found at '{0}'. @@ -107,8 +107,8 @@ Or open a new terminal. {Locked="shell"} - Open a new terminal for the change to take effect in other terminals and applications. - Open a new terminal for the change to take effect in other terminals and applications. + Other open terminals and applications will pick up this change after they're restarted. + Other open terminals and applications will pick up this change after they're restarted. diff --git a/src/Installer/dotnetup.Library/xlf/Strings.ko.xlf b/src/Installer/dotnetup.Library/xlf/Strings.ko.xlf index a52c66b4bf08..bab525c62c19 100644 --- a/src/Installer/dotnetup.Library/xlf/Strings.ko.xlf +++ b/src/Installer/dotnetup.Library/xlf/Strings.ko.xlf @@ -28,8 +28,8 @@ - Error: dotnet executable not found at '{0}'. - Error: dotnet executable not found at '{0}'. + dotnet executable not found at '{0}'. + dotnet executable not found at '{0}'. @@ -107,8 +107,8 @@ Or open a new terminal. {Locked="shell"} - Open a new terminal for the change to take effect in other terminals and applications. - Open a new terminal for the change to take effect in other terminals and applications. + Other open terminals and applications will pick up this change after they're restarted. + Other open terminals and applications will pick up this change after they're restarted. diff --git a/src/Installer/dotnetup.Library/xlf/Strings.pl.xlf b/src/Installer/dotnetup.Library/xlf/Strings.pl.xlf index 21079e58d905..40b58ef7dcff 100644 --- a/src/Installer/dotnetup.Library/xlf/Strings.pl.xlf +++ b/src/Installer/dotnetup.Library/xlf/Strings.pl.xlf @@ -28,8 +28,8 @@ - Error: dotnet executable not found at '{0}'. - Error: dotnet executable not found at '{0}'. + dotnet executable not found at '{0}'. + dotnet executable not found at '{0}'. @@ -107,8 +107,8 @@ Or open a new terminal. {Locked="shell"} - Open a new terminal for the change to take effect in other terminals and applications. - Open a new terminal for the change to take effect in other terminals and applications. + Other open terminals and applications will pick up this change after they're restarted. + Other open terminals and applications will pick up this change after they're restarted. diff --git a/src/Installer/dotnetup.Library/xlf/Strings.pt-BR.xlf b/src/Installer/dotnetup.Library/xlf/Strings.pt-BR.xlf index 82a89f2d4134..e2afcfb9976f 100644 --- a/src/Installer/dotnetup.Library/xlf/Strings.pt-BR.xlf +++ b/src/Installer/dotnetup.Library/xlf/Strings.pt-BR.xlf @@ -28,8 +28,8 @@ - Error: dotnet executable not found at '{0}'. - Error: dotnet executable not found at '{0}'. + dotnet executable not found at '{0}'. + dotnet executable not found at '{0}'. @@ -107,8 +107,8 @@ Or open a new terminal. {Locked="shell"} - Open a new terminal for the change to take effect in other terminals and applications. - Open a new terminal for the change to take effect in other terminals and applications. + Other open terminals and applications will pick up this change after they're restarted. + Other open terminals and applications will pick up this change after they're restarted. diff --git a/src/Installer/dotnetup.Library/xlf/Strings.ru.xlf b/src/Installer/dotnetup.Library/xlf/Strings.ru.xlf index 7d4b8c53eb22..50006ae89b60 100644 --- a/src/Installer/dotnetup.Library/xlf/Strings.ru.xlf +++ b/src/Installer/dotnetup.Library/xlf/Strings.ru.xlf @@ -28,8 +28,8 @@ - Error: dotnet executable not found at '{0}'. - Error: dotnet executable not found at '{0}'. + dotnet executable not found at '{0}'. + dotnet executable not found at '{0}'. @@ -107,8 +107,8 @@ Or open a new terminal. {Locked="shell"} - Open a new terminal for the change to take effect in other terminals and applications. - Open a new terminal for the change to take effect in other terminals and applications. + Other open terminals and applications will pick up this change after they're restarted. + Other open terminals and applications will pick up this change after they're restarted. diff --git a/src/Installer/dotnetup.Library/xlf/Strings.tr.xlf b/src/Installer/dotnetup.Library/xlf/Strings.tr.xlf index 3ff70a4828b4..adf5b518ed4f 100644 --- a/src/Installer/dotnetup.Library/xlf/Strings.tr.xlf +++ b/src/Installer/dotnetup.Library/xlf/Strings.tr.xlf @@ -28,8 +28,8 @@ - Error: dotnet executable not found at '{0}'. - Error: dotnet executable not found at '{0}'. + dotnet executable not found at '{0}'. + dotnet executable not found at '{0}'. @@ -107,8 +107,8 @@ Or open a new terminal. {Locked="shell"} - Open a new terminal for the change to take effect in other terminals and applications. - Open a new terminal for the change to take effect in other terminals and applications. + Other open terminals and applications will pick up this change after they're restarted. + Other open terminals and applications will pick up this change after they're restarted. diff --git a/src/Installer/dotnetup.Library/xlf/Strings.zh-Hans.xlf b/src/Installer/dotnetup.Library/xlf/Strings.zh-Hans.xlf index bebd5d21e0db..b8bbf1fb956d 100644 --- a/src/Installer/dotnetup.Library/xlf/Strings.zh-Hans.xlf +++ b/src/Installer/dotnetup.Library/xlf/Strings.zh-Hans.xlf @@ -28,8 +28,8 @@ - Error: dotnet executable not found at '{0}'. - Error: dotnet executable not found at '{0}'. + dotnet executable not found at '{0}'. + dotnet executable not found at '{0}'. @@ -107,8 +107,8 @@ Or open a new terminal. {Locked="shell"} - Open a new terminal for the change to take effect in other terminals and applications. - Open a new terminal for the change to take effect in other terminals and applications. + Other open terminals and applications will pick up this change after they're restarted. + Other open terminals and applications will pick up this change after they're restarted. diff --git a/src/Installer/dotnetup.Library/xlf/Strings.zh-Hant.xlf b/src/Installer/dotnetup.Library/xlf/Strings.zh-Hant.xlf index 611c6bf968df..0d5e28854a18 100644 --- a/src/Installer/dotnetup.Library/xlf/Strings.zh-Hant.xlf +++ b/src/Installer/dotnetup.Library/xlf/Strings.zh-Hant.xlf @@ -28,8 +28,8 @@ - Error: dotnet executable not found at '{0}'. - Error: dotnet executable not found at '{0}'. + dotnet executable not found at '{0}'. + dotnet executable not found at '{0}'. @@ -107,8 +107,8 @@ Or open a new terminal. {Locked="shell"} - Open a new terminal for the change to take effect in other terminals and applications. - Open a new terminal for the change to take effect in other terminals and applications. + Other open terminals and applications will pick up this change after they're restarted. + Other open terminals and applications will pick up this change after they're restarted. diff --git a/test/dotnetup.Tests/DotnetCommandStdinForwardingTests.cs b/test/dotnetup.Tests/DotnetCommandStdinForwardingTests.cs index fbebd3227a8c..4b5c8fa1bd16 100644 --- a/test/dotnetup.Tests/DotnetCommandStdinForwardingTests.cs +++ b/test/dotnetup.Tests/DotnetCommandStdinForwardingTests.cs @@ -48,9 +48,11 @@ public void DotnetCommand_ForwardsStdinToChildProcess() process.StartInfo.RedirectStandardOutput = true; process.StartInfo.RedirectStandardError = true; - // Prepend our temp dir to PATH so dotnetup resolves our fake dotnet - var currentPath = Environment.GetEnvironmentVariable("PATH") ?? string.Empty; - process.StartInfo.Environment["PATH"] = tempDir.FullName + Path.PathSeparator + currentPath; + // Point dotnetup's managed hive at our temp dir so it resolves our fake dotnet. + // dotnetup resolves the dotnet to run from its managed hive (not from PATH), so the + // fake dotnet must live at /dotnet[.exe] — which is exactly where + // CreateStdinEchoFakeDotnet placed it. + process.StartInfo.Environment["DOTNET_TESTHOOK_DEFAULT_DOTNET_PATH"] = tempDir.FullName; process.StartInfo.Environment["DOTNET_NOLOGO"] = "1"; process.StartInfo.Environment["NO_COLOR"] = "1"; diff --git a/test/dotnetup.Tests/DotnetCommandTests.cs b/test/dotnetup.Tests/DotnetCommandTests.cs index ac4c299cb10e..3fb74371e4bf 100644 --- a/test/dotnetup.Tests/DotnetCommandTests.cs +++ b/test/dotnetup.Tests/DotnetCommandTests.cs @@ -150,9 +150,9 @@ public void DotnetCommand_WhenDotnetFound_RunsProcess() } [TestMethod] - public void DotnetCommand_UsesConfiguredInstallType_WhenUserInstall() + public void DotnetCommand_UsesConfiguredPath_WhenDotnetupHive() { - // Arrange - two paths: a configured user install path and a default path + // Arrange - two paths: a configured dotnetup hive path and a default path var userDir = Directory.CreateTempSubdirectory("dotnetup-user-test"); var defaultDir = Directory.CreateTempSubdirectory("dotnetup-default-test"); try @@ -164,13 +164,13 @@ public void DotnetCommand_UsesConfiguredInstallType_WhenUserInstall() defaultInstallPath: defaultDir.FullName, configuredRoot: new DotnetInstallRootConfiguration( new DotnetInstallRoot(userDir.FullName, InstallArchitecture.x64), - InstallType.User)); + IsDotnetupHive: true)); var parseResult = Parser.Parse(["dotnet", "--version"]); var command = new TestableDotnetCommand(parseResult, mock); var exitCode = command.Execute(); - // Should succeed because it used the configured user path, not the default + // Should succeed because it used the configured hive path, not the default exitCode.Should().Be(0); } finally @@ -206,9 +206,10 @@ public void DotnetCommand_FallsBackToDefault_WhenNoConfiguredInstall() } [TestMethod] - public void DotnetCommand_FallsBackToDefault_WhenAdminInstall() + public void DotnetCommand_FallsBackToDefault_WhenNotDotnetupHive() { - // Arrange - configured install is Admin, not User; should fall back to default + // Arrange - configured install is not a dotnetup hive (e.g. an admin or loose install); + // should fall back to default var adminDir = Directory.CreateTempSubdirectory("dotnetup-admin-test"); var defaultDir = Directory.CreateTempSubdirectory("dotnetup-default-test"); try @@ -220,7 +221,7 @@ public void DotnetCommand_FallsBackToDefault_WhenAdminInstall() defaultInstallPath: defaultDir.FullName, configuredRoot: new DotnetInstallRootConfiguration( new DotnetInstallRoot(adminDir.FullName, InstallArchitecture.x64), - InstallType.System)); + IsDotnetupHive: false)); var parseResult = Parser.Parse(["dotnet", "--version"]); var command = new TestableDotnetCommand(parseResult, mock); diff --git a/test/dotnetup.Tests/InstallPathResolverTests.cs b/test/dotnetup.Tests/InstallPathResolverTests.cs index e4020dd3dd30..1a1e7a6bd54c 100644 --- a/test/dotnetup.Tests/InstallPathResolverTests.cs +++ b/test/dotnetup.Tests/InstallPathResolverTests.cs @@ -178,6 +178,32 @@ public void ResolveCurrentInstallRootPath_UsesRealDirectoryWhenParentDirectoryIs resolvedRoot.Should().Be(actualRoot); } + [TestMethod] + [OSCondition(ConditionMode.Exclude, OperatingSystems.Windows)] + public void ResolveRealPath_ResolvesSymlinkedDirectoryToItsRealPath() + { + // Mirrors a symlinked data directory (e.g. LocalApplicationData / XDG_DATA_HOME pointing + // through a symlink): the resolved path must be the real directory so that a default install + // path can be compared symmetrically against the realpath-resolved current install root. + using var testEnvironment = new TestEnvironment(); + string actualDir = Path.Combine(testEnvironment.TempRoot, "real-data", "dotnet"); + Directory.CreateDirectory(actualDir); + + string symlinkedParent = Path.Combine(testEnvironment.TempRoot, "linked-data"); + Directory.CreateSymbolicLink(symlinkedParent, Path.Combine(testEnvironment.TempRoot, "real-data")); + + string resolved = ExecutablePathResolver.ResolveRealPath(Path.Combine(symlinkedParent, "dotnet"))!; + + resolved.Should().Be(actualDir); + } + + [TestMethod] + public void ResolveRealPath_ReturnsNull_WhenPathIsNullOrEmpty() + { + ExecutablePathResolver.ResolveRealPath(null).Should().BeNull(); + ExecutablePathResolver.ResolveRealPath(string.Empty).Should().BeNull(); + } + private static GlobalJsonInfo CreateGlobalJsonInfo(string sdkPath) { // GlobalJsonInfo.SdkPath is computed from GlobalJsonContents.Sdk.Paths relative to GlobalJsonPath diff --git a/test/dotnetup.Tests/UninstallWorkflowTests.cs b/test/dotnetup.Tests/UninstallWorkflowTests.cs index 5c2226247439..5e534a0f0a72 100644 --- a/test/dotnetup.Tests/UninstallWorkflowTests.cs +++ b/test/dotnetup.Tests/UninstallWorkflowTests.cs @@ -20,8 +20,8 @@ public class UninstallWorkflowTests /// /// When no explicit path is provided and dotnet on PATH resolves to an admin install, - /// the uninstall should fall back to the default user install path — not the admin path. - /// Regression test: previously, GetConfiguredInstallType().Path was used unconditionally, + /// the uninstall should fall back to the default hive — not the admin path. + /// Regression test: previously, the configured path was used unconditionally, /// causing uninstall to target "C:\Program Files\dotnet" when the user meant their user install. /// [TestMethod] @@ -29,27 +29,44 @@ public void ResolveInstallPath_AdminInstall_FallsBackToDefault() { var mock = new MockDotnetInstallManager( defaultInstallPath: DefaultUserPath, - configuredRoot: CreateConfig(AdminPath, InstallType.System)); + configuredRoot: CreateConfig(AdminPath, isDotnetupHive: false)); var result = UninstallWorkflow.ResolveInstallPath(null, mock); - result.Should().Be(DefaultUserPath, "system installs on PATH should not be used; default user path should be used instead"); + result.Should().Be(DefaultUserPath, "installs on PATH that dotnetup does not own should not be used; default hive should be used instead"); } /// - /// When dotnet on PATH resolves to a user install, the uninstall should use that path. + /// When dotnet on PATH resolves to the dotnetup-managed hive, the uninstall should use that path. /// [TestMethod] - public void ResolveInstallPath_UserInstall_UsesConfiguredPath() + public void ResolveInstallPath_DotnetupHive_UsesConfiguredPath() { - string userPath = "/home/user/custom-dotnet"; var mock = new MockDotnetInstallManager( defaultInstallPath: DefaultUserPath, - configuredRoot: CreateConfig(userPath, InstallType.User)); + configuredRoot: CreateConfig(DefaultUserPath, isDotnetupHive: true)); var result = UninstallWorkflow.ResolveInstallPath(null, mock); - result.Should().Be(userPath, "user install path from configuration should be used"); + result.Should().Be(DefaultUserPath, "the dotnetup hive on PATH should be used"); + } + + /// + /// A dotnet that lives in a user-writable location but is not a dotnetup hive (e.g. a + /// hand-extracted C:\dotnet that happens to win on PATH) must not be treated as dotnetup's; + /// uninstall should fall back to the default hive rather than target the unmanaged install. + /// + [TestMethod] + public void ResolveInstallPath_UnmanagedUserInstall_FallsBackToDefault() + { + string looseUserPath = "/home/user/custom-dotnet"; + var mock = new MockDotnetInstallManager( + defaultInstallPath: DefaultUserPath, + configuredRoot: CreateConfig(looseUserPath, isDotnetupHive: false)); + + var result = UninstallWorkflow.ResolveInstallPath(null, mock); + + result.Should().Be(DefaultUserPath, "a non-hive install on PATH should not be uninstalled; default hive should be used"); } /// @@ -60,7 +77,7 @@ public void ResolveInstallPath_ExplicitPath_TakesPrecedence() { var mock = new MockDotnetInstallManager( defaultInstallPath: DefaultUserPath, - configuredRoot: CreateConfig(AdminPath, InstallType.System)); + configuredRoot: CreateConfig(AdminPath, isDotnetupHive: false)); var result = UninstallWorkflow.ResolveInstallPath(ExplicitPath, mock); @@ -82,9 +99,9 @@ public void ResolveInstallPath_NoConfiguredInstall_UsesDefault() result.Should().Be(DefaultUserPath); } - private static DotnetInstallRootConfiguration CreateConfig(string path, InstallType installType) + private static DotnetInstallRootConfiguration CreateConfig(string path, bool isDotnetupHive) { var installRoot = new DotnetInstallRoot(path, InstallerUtilities.GetDefaultInstallArchitecture()); - return new DotnetInstallRootConfiguration(installRoot, installType); + return new DotnetInstallRootConfiguration(installRoot, isDotnetupHive); } }