From 22dcca9ae4105731f5a6ee67ec8c1e916a05390c Mon Sep 17 00:00:00 2001 From: Forgind Date: Thu, 16 Mar 2023 11:57:53 -0700 Subject: [PATCH 1/5] Use dotnet from current process if possible --- src/MSBuildLocator/DotNetSdkLocationHelper.cs | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/src/MSBuildLocator/DotNetSdkLocationHelper.cs b/src/MSBuildLocator/DotNetSdkLocationHelper.cs index 3e6e86cc..358979a3 100644 --- a/src/MSBuildLocator/DotNetSdkLocationHelper.cs +++ b/src/MSBuildLocator/DotNetSdkLocationHelper.cs @@ -92,24 +92,32 @@ private static IEnumerable GetDotNetBasePaths(string workingDirectory) bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); string exeName = isWindows ? "dotnet.exe" : "dotnet"; - // We will generally find the dotnet exe on the path, but on linux, it is often just a 'dotnet' symlink (possibly even to more symlinks) that we have to resolve - // to the real dotnet executable. - // This will work as often as just invoking dotnet from the command line, but we can be more confident in finding a dotnet executable by following - // https://github.com/dotnet/designs/blob/main/accepted/2021/install-location-per-architecture.md - // This can be done using the nethost library. We didn't do this previously, so I did not implement this extension. - foreach (string dir in Environment.GetEnvironmentVariable("PATH").Split(Path.PathSeparator)) + if (Process.GetCurrentProcess().ProcessName.Equals(exeName)) { - string filePath = Path.Combine(dir, exeName); - if (File.Exists(Path.Combine(dir, exeName))) + // We're already in a dotnet process! Use it. + dotnetPath = Process.GetCurrentProcess().MainModule.FileName; + } + else + { + // We will generally find the dotnet exe on the path, but on linux, it is often just a 'dotnet' symlink (possibly even to more symlinks) that we have to resolve + // to the real dotnet executable. + // This will work as often as just invoking dotnet from the command line, but we can be more confident in finding a dotnet executable by following + // https://github.com/dotnet/designs/blob/main/accepted/2021/install-location-per-architecture.md + // This can be done using the nethost library. We didn't do this previously, so I did not implement this extension. + foreach (string dir in Environment.GetEnvironmentVariable("PATH").Split(Path.PathSeparator)) { - dotnetPath = filePath; - break; + string filePath = Path.Combine(dir, exeName); + if (File.Exists(Path.Combine(dir, exeName))) + { + dotnetPath = filePath; + break; + } } - } - if (dotnetPath != null) - { - dotnetPath = Path.GetDirectoryName(isWindows ? dotnetPath : realpath(dotnetPath) ?? dotnetPath); + if (dotnetPath != null) + { + dotnetPath = Path.GetDirectoryName(isWindows ? dotnetPath : realpath(dotnetPath) ?? dotnetPath); + } } string bestSDK = null; From 2d9c6aca9dea088093d7c15bc8552d283fe30ec5 Mon Sep 17 00:00:00 2001 From: Forgind Date: Tue, 21 Mar 2023 16:01:15 -0700 Subject: [PATCH 2/5] ProcessName is always dotnet --- src/MSBuildLocator/DotNetSdkLocationHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MSBuildLocator/DotNetSdkLocationHelper.cs b/src/MSBuildLocator/DotNetSdkLocationHelper.cs index 358979a3..0bd5dbc8 100644 --- a/src/MSBuildLocator/DotNetSdkLocationHelper.cs +++ b/src/MSBuildLocator/DotNetSdkLocationHelper.cs @@ -92,7 +92,7 @@ private static IEnumerable GetDotNetBasePaths(string workingDirectory) bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); string exeName = isWindows ? "dotnet.exe" : "dotnet"; - if (Process.GetCurrentProcess().ProcessName.Equals(exeName)) + if (Process.GetCurrentProcess().ProcessName.Equals("dotnet")) { // We're already in a dotnet process! Use it. dotnetPath = Process.GetCurrentProcess().MainModule.FileName; From 26deca0c962af3ed243d82a08ce3be7a7ffd52e6 Mon Sep 17 00:00:00 2001 From: Forgind Date: Wed, 22 Mar 2023 13:51:50 -0700 Subject: [PATCH 3/5] Combine results instead of if/else --- src/MSBuildLocator/DotNetSdkLocationHelper.cs | 54 +++++++++++-------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/src/MSBuildLocator/DotNetSdkLocationHelper.cs b/src/MSBuildLocator/DotNetSdkLocationHelper.cs index 0bd5dbc8..ad8299e1 100644 --- a/src/MSBuildLocator/DotNetSdkLocationHelper.cs +++ b/src/MSBuildLocator/DotNetSdkLocationHelper.cs @@ -91,35 +91,47 @@ private static IEnumerable GetDotNetBasePaths(string workingDirectory) string dotnetPath = null; bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); string exeName = isWindows ? "dotnet.exe" : "dotnet"; - - if (Process.GetCurrentProcess().ProcessName.Equals("dotnet")) + bool returnedSDKs = false; + + // We will generally find the dotnet exe on the path, but on linux, it is often just a 'dotnet' symlink (possibly even to more symlinks) that we have to resolve + // to the real dotnet executable. + // This will work as often as just invoking dotnet from the command line, but we can be more confident in finding a dotnet executable by following + // https://github.com/dotnet/designs/blob/main/accepted/2021/install-location-per-architecture.md + // This can be done using the nethost library. We didn't do this previously, so I did not implement this extension. + foreach (string dir in Environment.GetEnvironmentVariable("PATH").Split(Path.PathSeparator)) { - // We're already in a dotnet process! Use it. - dotnetPath = Process.GetCurrentProcess().MainModule.FileName; - } - else - { - // We will generally find the dotnet exe on the path, but on linux, it is often just a 'dotnet' symlink (possibly even to more symlinks) that we have to resolve - // to the real dotnet executable. - // This will work as often as just invoking dotnet from the command line, but we can be more confident in finding a dotnet executable by following - // https://github.com/dotnet/designs/blob/main/accepted/2021/install-location-per-architecture.md - // This can be done using the nethost library. We didn't do this previously, so I did not implement this extension. - foreach (string dir in Environment.GetEnvironmentVariable("PATH").Split(Path.PathSeparator)) + string filePath = Path.Combine(dir, exeName); + if (File.Exists(filePath)) { - string filePath = Path.Combine(dir, exeName); - if (File.Exists(Path.Combine(dir, exeName))) - { - dotnetPath = filePath; - break; - } + dotnetPath = filePath; + break; } + } + + if (dotnetPath != null) + { + dotnetPath = Path.GetDirectoryName(isWindows ? dotnetPath : realpath(dotnetPath) ?? dotnetPath); + } - if (dotnetPath != null) + foreach (string path in GetDotNetBasePaths(workingDirectory, dotnetPath)) + { + returnedSDKs = true; + yield return path; + } + + if (!returnedSDKs && Process.GetCurrentProcess().ProcessName.Equals("dotnet")) + { + // We're already in a dotnet process! Try using it. + dotnetPath = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName); + foreach (string path in GetDotNetBasePaths(workingDirectory, dotnetPath)) { - dotnetPath = Path.GetDirectoryName(isWindows ? dotnetPath : realpath(dotnetPath) ?? dotnetPath); + yield return path; } } + } + private static IEnumerable GetDotNetBasePaths(string workingDirectory, string dotnetPath) + { string bestSDK = null; int rc = NativeMethods.hostfxr_resolve_sdk2(exe_dir: dotnetPath, working_dir: workingDirectory, flags: 0, result: (key, value) => { From 1ff4d0cd46177cf5646d8745c486e3a363d78a50 Mon Sep 17 00:00:00 2001 From: Forgind Date: Wed, 22 Mar 2023 13:59:18 -0700 Subject: [PATCH 4/5] Only fail when SDK isn't found --- src/MSBuildLocator/DotNetSdkLocationHelper.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/MSBuildLocator/DotNetSdkLocationHelper.cs b/src/MSBuildLocator/DotNetSdkLocationHelper.cs index ad8299e1..429c95b1 100644 --- a/src/MSBuildLocator/DotNetSdkLocationHelper.cs +++ b/src/MSBuildLocator/DotNetSdkLocationHelper.cs @@ -119,15 +119,21 @@ private static IEnumerable GetDotNetBasePaths(string workingDirectory) yield return path; } - if (!returnedSDKs && Process.GetCurrentProcess().ProcessName.Equals("dotnet")) + if (Process.GetCurrentProcess().ProcessName.Equals("dotnet")) { // We're already in a dotnet process! Try using it. dotnetPath = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName); foreach (string path in GetDotNetBasePaths(workingDirectory, dotnetPath)) { + returnedSDKs = true; yield return path; } } + + if (!returnedSDKs) + { + throw new InvalidOperationException("Failed to find an appropriate version of .NET Core MSBuild. Call to hostfxr_resolve_sdk2 failed. There may be more details in stderr."); + } } private static IEnumerable GetDotNetBasePaths(string workingDirectory, string dotnetPath) @@ -147,7 +153,7 @@ private static IEnumerable GetDotNetBasePaths(string workingDirectory, s } else if (rc != 0) { - throw new InvalidOperationException("Failed to find an appropriate version of .NET Core MSBuild. Call to hostfxr_resolve_sdk2 failed. There may be more details in stderr."); + yield break; } string[] paths = null; @@ -159,7 +165,7 @@ private static IEnumerable GetDotNetBasePaths(string workingDirectory, s // Errors are automatically printed to stderr. We should not continue to try to output anything if we failed. if (rc != 0) { - throw new InvalidOperationException("Failed to find all versions of .NET Core MSBuild. Call to hostfxr_get_available_sdks failed. There may be more details in stderr."); + yield break; } // The paths are sorted in increasing order. We want to return the newest SDKs first, however, From 0bef5bed74bfe95f529ce68ce2735886d39a0861 Mon Sep 17 00:00:00 2001 From: Forgind Date: Thu, 23 Mar 2023 11:49:00 -0700 Subject: [PATCH 5/5] Add deduplication logic --- src/MSBuildLocator/DotNetSdkLocationHelper.cs | 73 ++++++++++++++++--- 1 file changed, 63 insertions(+), 10 deletions(-) diff --git a/src/MSBuildLocator/DotNetSdkLocationHelper.cs b/src/MSBuildLocator/DotNetSdkLocationHelper.cs index 429c95b1..ea00428f 100644 --- a/src/MSBuildLocator/DotNetSdkLocationHelper.cs +++ b/src/MSBuildLocator/DotNetSdkLocationHelper.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Linq; using System.Runtime.InteropServices; using System.Text.RegularExpressions; @@ -14,9 +15,9 @@ namespace Microsoft.Build.Locator { internal static class DotNetSdkLocationHelper { - private static readonly Regex DotNetBasePathRegex = new Regex("Base Path:(.*)$", RegexOptions.Multiline); private static readonly Regex VersionRegex = new Regex(@"^(\d+)\.(\d+)\.(\d+)", RegexOptions.Multiline); - private static readonly Regex SdkRegex = new Regex(@"(\S+) \[(.*?)]$", RegexOptions.Multiline); + private static int rc = 0; + private static bool pinnedSdk = false; public static VisualStudioInstance GetInstance(string dotNetSdkPath) { @@ -92,6 +93,7 @@ private static IEnumerable GetDotNetBasePaths(string workingDirectory) bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); string exeName = isWindows ? "dotnet.exe" : "dotnet"; bool returnedSDKs = false; + HashSet sdkPaths = new HashSet(); // We will generally find the dotnet exe on the path, but on linux, it is often just a 'dotnet' symlink (possibly even to more symlinks) that we have to resolve // to the real dotnet executable. @@ -113,12 +115,24 @@ private static IEnumerable GetDotNetBasePaths(string workingDirectory) dotnetPath = Path.GetDirectoryName(isWindows ? dotnetPath : realpath(dotnetPath) ?? dotnetPath); } + string pathOfPinnedSdk = string.Empty; + foreach (string path in GetDotNetBasePaths(workingDirectory, dotnetPath)) { returnedSDKs = true; - yield return path; + if (pinnedSdk) + { + pinnedSdk = false; + pathOfPinnedSdk = path; + } + else + { + sdkPaths.Add(path); + } } + string pathOfSecondPinnedSdk = string.Empty; + if (Process.GetCurrentProcess().ProcessName.Equals("dotnet")) { // We're already in a dotnet process! Try using it. @@ -126,20 +140,56 @@ private static IEnumerable GetDotNetBasePaths(string workingDirectory) foreach (string path in GetDotNetBasePaths(workingDirectory, dotnetPath)) { returnedSDKs = true; - yield return path; + if (pinnedSdk) + { + pinnedSdk = false; + pathOfSecondPinnedSdk = path; + } + else + { + sdkPaths.Add(path); + } } } - if (!returnedSDKs) + if (!returnedSDKs && rc != 0) { throw new InvalidOperationException("Failed to find an appropriate version of .NET Core MSBuild. Call to hostfxr_resolve_sdk2 failed. There may be more details in stderr."); } + else if (returnedSDKs) + { + List discoveredSDKs = sdkPaths.ToList(); + discoveredSDKs.Sort(); + + if (!string.IsNullOrEmpty(pathOfPinnedSdk)) + { + discoveredSDKs.Add(pathOfPinnedSdk); + } + + if (!string.IsNullOrEmpty(pathOfSecondPinnedSdk)) + { + discoveredSDKs.Add(pathOfSecondPinnedSdk); + } + + // We currently have the SDKs sorted in increasing version number with the pinned SDKs at the end. Reverse that order. + discoveredSDKs.Reverse(); + + return discoveredSDKs; + } + else + { + return Enumerable.Empty(); + } } private static IEnumerable GetDotNetBasePaths(string workingDirectory, string dotnetPath) { string bestSDK = null; - int rc = NativeMethods.hostfxr_resolve_sdk2(exe_dir: dotnetPath, working_dir: workingDirectory, flags: 0, result: (key, value) => + + // Only overwrite the return code if we would set it to a non-zero value so that if we ultimately fail to find any SDKs, we can provide + // a reasonable error. Otherwise, we would only ever see failures from the last call to GetDotNetBasePaths. + int localRC = 0; + localRC = NativeMethods.hostfxr_resolve_sdk2(exe_dir: dotnetPath, working_dir: workingDirectory, flags: 0, result: (key, value) => { if (key == NativeMethods.hostfxr_resolve_sdk2_result_key_t.resolved_sdk_dir) { @@ -147,24 +197,27 @@ private static IEnumerable GetDotNetBasePaths(string workingDirectory, s } }); - if (rc == 0 && bestSDK != null) + if (localRC == 0 && bestSDK != null) { + pinnedSdk = true; yield return bestSDK; } - else if (rc != 0) + else if (localRC != 0) { + rc = localRC; yield break; } string[] paths = null; - rc = NativeMethods.hostfxr_get_available_sdks(exe_dir: dotnetPath, result: (key, value) => + localRC = NativeMethods.hostfxr_get_available_sdks(exe_dir: dotnetPath, result: (key, value) => { paths = value; }); // Errors are automatically printed to stderr. We should not continue to try to output anything if we failed. - if (rc != 0) + if (localRC != 0) { + rc = localRC; yield break; }