- 
                Notifications
    You must be signed in to change notification settings 
- Fork 4.2k
Consider PATH again when searching for dotnet host #80859
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: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
| // See the LICENSE file in the project root for more information. | ||
|  | ||
| using System; | ||
| using System.IO; | ||
| using System.Runtime.InteropServices; | ||
| using Microsoft.CodeAnalysis.Test.Utilities; | ||
| using Roslyn.Test.Utilities; | ||
| using Roslyn.Utilities; | ||
| using Xunit; | ||
| using Xunit.Abstractions; | ||
|  | ||
| namespace Microsoft.CodeAnalysis.BuildTasks.UnitTests; | ||
|  | ||
| public sealed class RuntimeHostInfoTests(ITestOutputHelper output) | ||
| { | ||
| private readonly ITestOutputHelper _output = output; | ||
|  | ||
| [Fact, WorkItem("https://github.com/dotnet/msbuild/issues/12669")] | ||
| public void DotNetInPath() | ||
| { | ||
| var previousPath = Environment.GetEnvironmentVariable("PATH"); | ||
| try | ||
| { | ||
| using var tempRoot = new TempRoot(); | ||
| var testDir = tempRoot.CreateDirectory(); | ||
| var globalDotNetDir = testDir.CreateDirectory("global-dotnet"); | ||
| var globalDotNetExe = globalDotNetDir.CreateFile($"dotnet{PlatformInformation.ExeExtension}"); | ||
| Environment.SetEnvironmentVariable("PATH", globalDotNetDir.Path); | ||
|  | ||
| Assert.Equal(globalDotNetDir.Path, RuntimeHostInfo.GetToolDotNetRoot(_output.WriteLine)); | ||
| } | ||
| finally | ||
| { | ||
| Environment.SetEnvironmentVariable("PATH", previousPath); | ||
| } | ||
| } | ||
|  | ||
| [Fact, WorkItem("https://github.com/dotnet/msbuild/issues/12669")] | ||
| public void DotNetInPath_None() | ||
| { | ||
| var previousPath = Environment.GetEnvironmentVariable("PATH"); | ||
| try | ||
| { | ||
| Environment.SetEnvironmentVariable("PATH", ""); | ||
|  | ||
| Assert.Null(RuntimeHostInfo.GetToolDotNetRoot(_output.WriteLine)); | ||
| } | ||
| finally | ||
| { | ||
| Environment.SetEnvironmentVariable("PATH", previousPath); | ||
| } | ||
| } | ||
|  | ||
| [Fact, WorkItem("https://github.com/dotnet/msbuild/issues/12669")] | ||
| public void DotNetInPath_Symlinked() | ||
| { | ||
| var previousPath = Environment.GetEnvironmentVariable("PATH"); | ||
| try | ||
| { | ||
| using var tempRoot = new TempRoot(); | ||
| var testDir = tempRoot.CreateDirectory(); | ||
| var globalDotNetDir = testDir.CreateDirectory("global-dotnet"); | ||
| var globalDotNetExe = globalDotNetDir.CreateFile($"dotnet{PlatformInformation.ExeExtension}"); | ||
| var binDir = testDir.CreateDirectory("bin"); | ||
| var symlinkPath = Path.Combine(binDir.Path, $"dotnet{PlatformInformation.ExeExtension}"); | ||
|  | ||
| // Create symlink from binDir to the actual dotnet executable | ||
| File.CreateSymbolicLink(path: symlinkPath, pathToTarget: globalDotNetExe.Path); | ||
|  | ||
| Environment.SetEnvironmentVariable("PATH", binDir.Path); | ||
|  | ||
| Assert.Equal(globalDotNetDir.Path, RuntimeHostInfo.GetToolDotNetRoot(_output.WriteLine)); | ||
| } | ||
| finally | ||
| { | ||
| Environment.SetEnvironmentVariable("PATH", previousPath); | ||
| } | ||
| } | ||
| } | ||
|  | ||
| #if !NET | ||
| file static class NativeMethods | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would convey intent more clearly, to wrap this in  There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, that was my intention, it just got lost in some code move. I guess the BCL method just takes precedence on NET, hence this works even now. | ||
| { | ||
| extension(File) | ||
| { | ||
| /// <remarks> | ||
| /// Only used by tests currently (might need some hardening if this is to be used by production code). | ||
| /// </remarks> | ||
| public static void CreateSymbolicLink(string path, string pathToTarget) | ||
| { | ||
| bool ok = CreateSymbolicLink( | ||
| lpSymlinkFileName: path, | ||
| lpTargetFileName: pathToTarget, | ||
| dwFlags: SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE); | ||
| if (!ok) | ||
| { | ||
| throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error()); | ||
| } | ||
| } | ||
| } | ||
|  | ||
| [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] | ||
| private static extern bool CreateSymbolicLink( | ||
| string lpSymlinkFileName, | ||
| string lpTargetFileName, | ||
| uint dwFlags); | ||
|  | ||
| private const uint SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE = 0x2; | ||
| } | ||
| #endif | ||
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
|  | @@ -5,9 +5,11 @@ | |
| #nullable enable | ||
|  | ||
| using System; | ||
| using System.IO; | ||
| using System.Runtime.InteropServices; | ||
| using System.Runtime.Versioning; | ||
| using System.Text; | ||
| using Microsoft.Win32.SafeHandles; | ||
|  | ||
| namespace Microsoft.CodeAnalysis.CommandLine | ||
| { | ||
|  | @@ -93,5 +95,85 @@ out PROCESS_INFORMATION lpProcessInformation | |
|  | ||
| [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] | ||
| internal static extern IntPtr GetCommandLine(); | ||
|  | ||
| #if !NET | ||
| //------------------------------------------------------------------------------ | ||
| // ResolveLinkTarget | ||
| //------------------------------------------------------------------------------ | ||
| extension(File) | ||
| { | ||
| public static FileSystemInfo? ResolveLinkTarget(string path, bool returnFinalTarget) | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this work on Linux/Mac? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, but on Linux, we run on .NET Core where the  There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah. I think I'm following. These are netframework only. If these are ever used, we expect the caller is running on Windows. I missed the  | ||
| { | ||
| if (!returnFinalTarget) throw new NotSupportedException(); | ||
|  | ||
| using var handle = CreateFileW( | ||
| lpFileName: path, | ||
| dwDesiredAccess: FILE_READ_ATTRIBUTES, | ||
| dwShareMode: FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, | ||
| lpSecurityAttributes: IntPtr.Zero, | ||
| dwCreationDisposition: OPEN_EXISTING, | ||
| dwFlagsAndAttributes: FILE_FLAG_BACKUP_SEMANTICS, // needed for directories | ||
| hTemplateFile: IntPtr.Zero); | ||
|  | ||
| if (handle.IsInvalid) | ||
| { | ||
| return null; | ||
| } | ||
|  | ||
| uint flags = FILE_NAME_NORMALIZED | VOLUME_NAME_DOS; | ||
| uint needed = GetFinalPathNameByHandleW(hFile: handle, lpszFilePath: null, cchFilePath: 0, dwFlags: flags); | ||
| if (needed == 0) return null; | ||
|  | ||
| var sb = new StringBuilder((int)needed + 1); | ||
| uint len = GetFinalPathNameByHandleW(hFile: handle, lpszFilePath: sb, cchFilePath: (uint)sb.Capacity, dwFlags: flags); | ||
| if (len == 0) return null; | ||
|  | ||
| return new FileInfo(TrimWin32ExtendedPrefix(sb.ToString())); | ||
| } | ||
| } | ||
|  | ||
| private static string TrimWin32ExtendedPrefix(string s) | ||
| { | ||
| if (s.StartsWith(@"\\?\UNC\", StringComparison.Ordinal)) | ||
| return @"\\" + s.Substring(8); | ||
| if (s.StartsWith(@"\\?\", StringComparison.Ordinal)) | ||
| return s.Substring(4); | ||
| return s; | ||
| } | ||
|  | ||
| // https://learn.microsoft.com/en-us/windows/win32/fileio/file-access-rights-constants | ||
| private const uint FILE_READ_ATTRIBUTES = 0x0080; | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Where do these values come from? What documentation can I look at to verify them? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In win32 API docs for the corresponding functions: CreateFileW: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I recommend linking liberally to the docs for these constants and functions in the comments. | ||
|  | ||
| // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew | ||
| private const uint FILE_SHARE_READ = 0x00000001; | ||
| private const uint FILE_SHARE_WRITE = 0x00000002; | ||
| private const uint FILE_SHARE_DELETE = 0x00000004; | ||
| private const uint OPEN_EXISTING = 3; | ||
| private const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000; | ||
|  | ||
| // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfinalpathnamebyhandlew | ||
| private const uint VOLUME_NAME_DOS = 0x0; | ||
| private const uint FILE_NAME_NORMALIZED = 0x0; | ||
|  | ||
| // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew | ||
| [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] | ||
| private static extern SafeFileHandle CreateFileW( | ||
| string lpFileName, | ||
| uint dwDesiredAccess, | ||
| uint dwShareMode, | ||
| IntPtr lpSecurityAttributes, | ||
| uint dwCreationDisposition, | ||
| uint dwFlagsAndAttributes, | ||
| IntPtr hTemplateFile); | ||
|  | ||
| // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfinalpathnamebyhandlew | ||
| [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] | ||
| private static extern uint GetFinalPathNameByHandleW( | ||
| SafeFileHandle hFile, | ||
| StringBuilder? lpszFilePath, | ||
| uint cchFilePath, | ||
| uint dwFlags); | ||
| #endif | ||
|  | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.