Prefer DOTNET_ROOT over directory traversal when finding muxer#53405
Prefer DOTNET_ROOT over directory traversal when finding muxer#53405JamieMagee wants to merge 1 commit intodotnet:mainfrom
Conversation
The Muxer constructor walks up two directories from AppContext.BaseDirectory to find the dotnet host. The runtime resolves symlinks on that path, so on systems that compose multiple SDK versions into one directory via symlinks (Nix, Guix), it ends up in the wrong root and can only see one runtime version. Move the DOTNET_HOST_PATH and DOTNET_ROOT checks ahead of the directory traversal so package managers can set the root explicitly. When neither variable is set, the old heuristic still runs. Also stops _muxerPath from being set to a non-muxer process path (e.g. testhost) when all other lookups fail. Ref: NixOS/nixpkgs#464575 Ref: dotnet#51693
There was a problem hiding this comment.
Pull request overview
This PR updates Muxer discovery to prefer explicit environment configuration (DOTNET_HOST_PATH, DOTNET_ROOT) over the existing directory-traversal heuristic, improving correctness for installations that rely on symlink composition (e.g., NixOS/Guix). It also avoids incorrectly treating the current process path (e.g., testhost) as the muxer when discovery fails.
Changes:
- Prioritize
DOTNET_HOST_PATHandDOTNET_ROOTchecks before walking up fromAppContext.BaseDirectory. - Keep the existing “two directories up” heuristic as a fallback when env vars are not usable.
- Restrict the final fallback to only use
Environment.ProcessPathwhen the current process is actuallydotnet/dotnet.exe.
Comments suppressed due to low confidence (1)
src/Cli/Microsoft.DotNet.Cli.Utils/Muxer.cs:62
- DOTNET_ROOT is accepted as long as it’s non-null, but an empty string (or relative path) can result in
Path.Combine(dotnetRoot, muxerFileName)producing a relativedotnet/dotnet.exelookup in the current working directory. To avoid accidentally binding to an unrelateddotnetin CWD, consider treating null/empty/whitespace as unset and (optionally) requiringPath.IsPathRooted(dotnetRoot)/Directory.Exists(dotnetRoot)before using it.
var dotnetRoot = Environment.GetEnvironmentVariable("DOTNET_ROOT");
if (dotnetRoot is not null)
{
string rootMuxer = Path.Combine(dotnetRoot, muxerFileName);
if (File.Exists(rootMuxer))
{
_muxerPath = rootMuxer;
}
}
| string? dotnetHostPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH"); | ||
| if (dotnetHostPath is not null && File.Exists(dotnetHostPath)) | ||
| { | ||
| string muxerPathMaybe = Path.Combine(rootPath, muxerFileName); | ||
| if (File.Exists(muxerPathMaybe)) | ||
| _muxerPath = dotnetHostPath; | ||
| } |
There was a problem hiding this comment.
The DOTNET_HOST_PATH branch only checks File.Exists, but doesn’t validate that the value actually points to the dotnet muxer (e.g., filename matches muxerFileName). If DOTNET_HOST_PATH is set to some other existing executable, this will set _muxerPath incorrectly and downstream ProcessStartInfo calls will try to run the wrong binary. Consider additionally validating Path.GetFileName(dotnetHostPath) against muxerFileName (and optionally requiring an absolute path) before accepting it.
This issue also appears on line 54 of the same file.
| // Check environment variables first to allow package managers and | ||
| // other tools to explicitly set the dotnet root. This is needed | ||
| // when the SDK is installed via symlinks (e.g. Nix, Guix) where | ||
| // the directory-traversal heuristic below would resolve symlinks | ||
| // and find the wrong root directory. | ||
| string? dotnetHostPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH"); | ||
| if (dotnetHostPath is not null && File.Exists(dotnetHostPath)) | ||
| { | ||
| string muxerPathMaybe = Path.Combine(rootPath, muxerFileName); | ||
| if (File.Exists(muxerPathMaybe)) | ||
| _muxerPath = dotnetHostPath; | ||
| } | ||
|
|
||
| if (_muxerPath is null) | ||
| { | ||
| var dotnetRoot = Environment.GetEnvironmentVariable("DOTNET_ROOT"); | ||
| if (dotnetRoot is not null) | ||
| { | ||
| _muxerPath = muxerPathMaybe; | ||
| string rootMuxer = Path.Combine(dotnetRoot, muxerFileName); | ||
| if (File.Exists(rootMuxer)) | ||
| { | ||
| _muxerPath = rootMuxer; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (_muxerPath is null) | ||
| { | ||
| // Best-effort search for muxer. | ||
| // SDK sets DOTNET_HOST_PATH as absolute path to current dotnet executable | ||
| // Most scenarios are running dotnet.dll as the app | ||
| // Root directory with muxer should be two above app base: <root>/sdk/<version> | ||
| string? rootPath = Path.GetDirectoryName(Path.GetDirectoryName(AppContext.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar))); | ||
| if (rootPath is not null) | ||
| { | ||
| string muxerPathMaybe = Path.Combine(rootPath, muxerFileName); | ||
| if (File.Exists(muxerPathMaybe)) | ||
| { | ||
| _muxerPath = muxerPathMaybe; | ||
| } | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
The muxer discovery behavior changed materially (env var precedence, and avoiding fallback to non-dotnet process paths). There are unit tests for this assembly, but none covering Muxer resolution. Adding tests that set DOTNET_HOST_PATH / DOTNET_ROOT to temp locations (and verifying the chosen path) would help prevent regressions, especially for the symlinked-root scenario described in the PR.
|
This The design adds a paths property to The Muxer class is a separate thing. It finds the This patch puts the env vars back in front. When they aren't set, the normal case for the local-sdk design, the directory traversal still runs and the behavior is identical to what PR #47664 shipped. The One thing worth calling out: |
|
Correct - DOTNET_ROOT is not used by the raw muxer to locate runtimes, it always looks next to itself. I do think we may need SDK-tooling-specific knobs for these kinds of concepts - this is part of the reason I tried to unify the important path calculations in my other PR. |
|
@marcpopMSFT @baronfel What about a dedicated variable instead? Something like
When |
The
Muxerconstructor walks up two directories fromAppContext.BaseDirectoryto find the dotnet host. The runtime resolves symlinks on that path, so on systems that compose multiple SDK versions into one directory via symlinks (NixOS, Guix, etc.), it ends up in the wrong root and can only see one runtime version.Move the
DOTNET_HOST_PATHandDOTNET_ROOTchecks ahead of the directory traversal so package managers can set the root explicitly. When neither variable is set, the old heuristic still runs.Also stops
_muxerPathfrom being set to a non-muxer process path (e.g. testhost) when all other lookups fail.Ref: NixOS/nixpkgs#464575
Ref: #51693