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);
}
}