From b53cd42c5a7097b52a9e7d56ceb721444fbb347a Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Wed, 23 Aug 2023 14:23:20 +0200 Subject: [PATCH 1/7] Fix hostfxr resolution --- .github/workflows/pull-request.yml | 1 - samples/BuilderApp/BuilderApp.csproj | 2 +- .../Microsoft.Build.Locator.Tests.csproj | 4 +- src/MSBuildLocator/DotNetSdkLocationHelper.cs | 263 +++++++++++++----- .../Microsoft.Build.Locator.csproj | 4 +- 5 files changed, 194 insertions(+), 80 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index b978f021..9fd2fef6 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -21,7 +21,6 @@ jobs: uses: actions/setup-dotnet@v1 with: dotnet-version: | - 3.1.x 6.0.x - name: Restore run: dotnet restore -bl:restore.binlog diff --git a/samples/BuilderApp/BuilderApp.csproj b/samples/BuilderApp/BuilderApp.csproj index f8adc233..9035d7de 100644 --- a/samples/BuilderApp/BuilderApp.csproj +++ b/samples/BuilderApp/BuilderApp.csproj @@ -2,7 +2,7 @@ Exe - net472;netcoreapp3.1;net6.0 + net472;net6.0 false false diff --git a/src/MSBuildLocator.Tests/Microsoft.Build.Locator.Tests.csproj b/src/MSBuildLocator.Tests/Microsoft.Build.Locator.Tests.csproj index d16f1656..4114e127 100644 --- a/src/MSBuildLocator.Tests/Microsoft.Build.Locator.Tests.csproj +++ b/src/MSBuildLocator.Tests/Microsoft.Build.Locator.Tests.csproj @@ -1,14 +1,14 @@ - net472;netcoreapp3.1 + net472;net6.0 false true ..\MSBuildLocator\key.snk - + diff --git a/src/MSBuildLocator/DotNetSdkLocationHelper.cs b/src/MSBuildLocator/DotNetSdkLocationHelper.cs index 116ab6cf..ef4b9d8b 100644 --- a/src/MSBuildLocator/DotNetSdkLocationHelper.cs +++ b/src/MSBuildLocator/DotNetSdkLocationHelper.cs @@ -5,61 +5,52 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; +using System.Linq; +using System.Reflection; using System.Runtime.InteropServices; using System.Text.RegularExpressions; +#nullable enable + 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 readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + private static readonly string ExeName = IsWindows ? "dotnet.exe" : "dotnet"; + private static readonly string? DotnetPath = ResolveDotnetPath(); - public static VisualStudioInstance GetInstance(string dotNetSdkPath) - { - if (string.IsNullOrWhiteSpace(dotNetSdkPath)) - { - return null; - } + static DotNetSdkLocationHelper() => LoadHostFxr(); - if (!File.Exists(Path.Combine(dotNetSdkPath, "Microsoft.Build.dll"))) - { + public static VisualStudioInstance? GetInstance(string dotNetSdkPath) + { + if (string.IsNullOrWhiteSpace(dotNetSdkPath) || !File.Exists(Path.Combine(dotNetSdkPath, "Microsoft.Build.dll"))) return null; - } string versionPath = Path.Combine(dotNetSdkPath, ".version"); if (!File.Exists(versionPath)) - { return null; - } // Preview versions contain a hyphen after the numeric part of the version. Version.TryParse doesn't accept that. Match versionMatch = VersionRegex.Match(File.ReadAllText(versionPath)); if (!versionMatch.Success) - { return null; - } if (!int.TryParse(versionMatch.Groups[1].Value, out int major) || !int.TryParse(versionMatch.Groups[2].Value, out int minor) || !int.TryParse(versionMatch.Groups[3].Value, out int patch)) - { return null; - } - + // Components of the SDK often have dependencies on the runtime they shipped with, including that several tasks that shipped // in the .NET 5 SDK rely on the .NET 5.0 runtime. Assuming the runtime that shipped with a particular SDK has the same version, // this ensures that we don't choose an SDK that doesn't work with the runtime of the chosen application. This is not guaranteed // to always work but should work for now. if (major > Environment.Version.Major || (major == Environment.Version.Major && minor > Environment.Version.Minor)) - { return null; - } return new VisualStudioInstance( name: ".NET Core SDK", @@ -78,90 +69,214 @@ public static IEnumerable GetInstances(string workingDirec } } - private static string realpath(string path) + private static IEnumerable GetDotNetBasePaths(string workingDirectory) { - IntPtr ptr = NativeMethods.realpath(path, IntPtr.Zero); - string result = Marshal.PtrToStringAuto(ptr); - NativeMethods.free(ptr); - return result; + string? bestSDK = GetSdkFromGlobalSettings(workingDirectory); + if (!string.IsNullOrEmpty(bestSDK)) + yield return bestSDK; + + string[] dotnetPaths = GetAllAvailableSDKs(); + // We want to return the newest SDKs first, however, so iterate over the list in reverse order. + // If basePath is disqualified because it was later + // than the runtime version, this ensures that RegisterDefaults will return the latest valid + // SDK instead of the earliest installed. + for (int i = dotnetPaths.Length - 1; i >= 0; i--) + { + if (dotnetPaths[i] != bestSDK) + yield return dotnetPaths[i]; + } + } + private static void LoadHostFxr() + { + var isOSX = RuntimeInformation.IsOSPlatform(OSPlatform.OSX); + if (isOSX) + { + var loadContext = System.Runtime.Loader.AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()); + if (loadContext != null) + loadContext.ResolvingUnmanagedDll += HostFxrResolver; + } } - private static IEnumerable GetDotNetBasePaths(string workingDirectory) + private static IntPtr HostFxrResolver(Assembly assembly, string libraryName) { - string dotnetPath = null; - bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - string exeName = isWindows ? "dotnet.exe" : "dotnet"; + var hostFxrLibName = "libhostfxr.dylib"; - // 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 (!hostFxrLibName.Equals(libraryName, StringComparison.Ordinal) || string.IsNullOrEmpty(DotnetPath)) + return IntPtr.Zero; + + var hostFxrRoot = Path.Combine(DotnetPath, "host", "fxr"); + if (Directory.Exists(hostFxrRoot)) { - string filePath = Path.Combine(dir, exeName); - if (File.Exists(filePath)) + // Agreed to load hostfxr from the highest version + var hostFxrAssemblyDirectory = Directory.GetDirectories(hostFxrRoot) + .OrderByDescending(d => d) + .FirstOrDefault(); + + if (hostFxrAssemblyDirectory != null) { - if (!isWindows) - { - filePath = realpath(filePath) ?? filePath; - if (!File.Exists(filePath)) - { - continue; - } - } - - dotnetPath = Path.GetDirectoryName(filePath); - break; + var hostfxrAssembly = Directory.GetFiles(hostFxrAssemblyDirectory) + .Where(filePath => hostFxrLibName.Equals(libraryName, StringComparison.Ordinal)) + .FirstOrDefault(); + + if (hostfxrAssembly != null) + return NativeLibrary.TryLoad(hostfxrAssembly, out var handle) ? handle : IntPtr.Zero; } } - if (dotnetPath is null) - { - throw new InvalidOperationException("Could not find the dotnet executable. Is it on the PATH?"); - } + return IntPtr.Zero; + } - string bestSDK = null; - int rc = NativeMethods.hostfxr_resolve_sdk2(exe_dir: dotnetPath, working_dir: workingDirectory, flags: 0, result: (key, value) => + private static string SdkResolutionExceptionMessage(string methodName) => $"Failed to find all versions of .NET Core MSBuild. Call to {methodName}. There may be more details in stderr."; + + /// + /// Determines the directory location of the SDK accounting for + /// global.json and multi-level lookup policy. + /// + private static string? GetSdkFromGlobalSettings(string workingDirectory) + { + string? resolvedSdk = null; + int rc = 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) { - bestSDK = value; + resolvedSdk = value; } }); - if (rc == 0 && bestSDK != null) + if (rc != 0) + throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_resolve_sdk2))); + + return resolvedSdk; + } + + private static string ResolveDotnetPath() + { + string? dotnetPath = GetDotnetPathFromROOT(); + + if (string.IsNullOrEmpty(dotnetPath)) { - yield return bestSDK; + string? dotnetExePath = GetCurrentProcessPath(); + var isRunFromDotnetExecutable = !string.IsNullOrEmpty(dotnetExePath) + && Path.GetFileName(dotnetExePath).Equals(ExeName, StringComparison.InvariantCultureIgnoreCase); + if (isRunFromDotnetExecutable) + dotnetPath = Path.GetDirectoryName(dotnetExePath); + + else + { + dotnetPath = FindDotnetPathFromEnvVariable("DOTNET_HOST_PATH") + ?? FindDotnetPathFromEnvVariable("DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR") + ?? GetDotnetPathFromPATH(); + } } - else if (rc != 0) + + if (string.IsNullOrEmpty(dotnetPath)) + throw new InvalidOperationException("Could not find the dotnet executable. Is it set on the DOTNET_ROOT?"); + + return dotnetPath; + } + + private static string? GetDotnetPathFromROOT() + { + // 32-bit architecture has (x86) suffix + string envVarName = (IntPtr.Size == 4) ? "DOTNET_ROOT(x86)" : "DOTNET_ROOT"; + var dotnetPath = FindDotnetPathFromEnvVariable(envVarName); + + return dotnetPath; + } + + private static string? GetDotnetPathFromHOST() + { + var dotnetPath = FindDotnetPathFromEnvVariable("DOTNET_HOST_PATH"); + if (dotnetPath == null) { - 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."); + dotnetPath ??= FindDotnetPathFromEnvVariable("DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR"); } - string[] paths = null; - rc = NativeMethods.hostfxr_get_available_sdks(exe_dir: dotnetPath, result: (key, value) => + return dotnetPath; + } + + private static string? GetCurrentProcessPath() + { + string? processPath = null; +#if NET6_0_OR_GREATER + processPath = Environment.ProcessPath; +#else + processPath = Process.GetCurrentProcess().MainModule.FileName; +#endif + return processPath; + } + + private static string? GetDotnetPathFromPATH() + { + string? dotnetPath = null; + // 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 could be done using the nethost library, but this is currently shipped as metadata package (Microsoft.NETCore.DotNetAppHost) and requires the customers + // to specify for resolving runtime assembly. + var paths = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) ?? Array.Empty(); + foreach (string dir in paths) { - paths = value; - }); + string? filePath = ValidatePath(dir); + if (string.IsNullOrEmpty(filePath)) + continue; + + dotnetPath = dir; + } + + return dotnetPath; + } + + /// + /// Returns the list of all available SDKs ordered by ascending version. + /// + private static string[] GetAllAvailableSDKs() + { + string[]? resolvedPaths = null; + int rc = NativeMethods.hostfxr_get_available_sdks(exe_dir: DotnetPath, result: (key, value) => resolvedPaths = value); // 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."); - } + throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_get_available_sdks))); - // The paths are sorted in increasing order. We want to return the newest SDKs first, however, - // so iterate over the list in reverse order. If basePath is disqualified because it was later - // than the runtime version, this ensures that RegisterDefaults will return the latest valid - // SDK instead of the earliest installed. - for (int i = paths.Length - 1; i >= 0; i--) + return resolvedPaths ?? Array.Empty(); + } + + /// + /// This native method call determines the actual location of path, including + /// resolving symbolic links. + /// + private static string? realpath(string path) + { + IntPtr ptr = NativeMethods.realpath(path, IntPtr.Zero); + string? result = Marshal.PtrToStringAuto(ptr); + NativeMethods.free(ptr); + return result; + } + + private static string? FindDotnetPathFromEnvVariable(string environmentVariable) + { + string? dotnetPath = Environment.GetEnvironmentVariable(environmentVariable); + + return string.IsNullOrEmpty(dotnetPath) ? null : ValidatePath(dotnetPath); + } + + private static string? ValidatePath(string dotnetPath) + { + string fullPathToDotnetFromRoot = Path.Combine(dotnetPath, ExeName); + if (File.Exists(fullPathToDotnetFromRoot)) { - if (paths[i] != bestSDK) + if (!IsWindows) { - yield return paths[i]; + fullPathToDotnetFromRoot = realpath(fullPathToDotnetFromRoot) ?? fullPathToDotnetFromRoot; + return File.Exists(fullPathToDotnetFromRoot) ? Path.GetDirectoryName(fullPathToDotnetFromRoot) : null; } + + return dotnetPath; } + + return null; } } } diff --git a/src/MSBuildLocator/Microsoft.Build.Locator.csproj b/src/MSBuildLocator/Microsoft.Build.Locator.csproj index b7c193ef..056f7ca5 100644 --- a/src/MSBuildLocator/Microsoft.Build.Locator.csproj +++ b/src/MSBuildLocator/Microsoft.Build.Locator.csproj @@ -2,7 +2,7 @@ Library - net46;netcoreapp3.1 + net46;net6.0 full false @@ -21,7 +21,7 @@ - + From c8dccd1c392ee4a2b2d26078d8e1638e156359db Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Wed, 23 Aug 2023 14:42:47 +0200 Subject: [PATCH 2/7] fix review comments --- src/MSBuildLocator/DotNetSdkLocationHelper.cs | 101 +++++++----------- 1 file changed, 36 insertions(+), 65 deletions(-) diff --git a/src/MSBuildLocator/DotNetSdkLocationHelper.cs b/src/MSBuildLocator/DotNetSdkLocationHelper.cs index d26467a9..3a2fde39 100644 --- a/src/MSBuildLocator/DotNetSdkLocationHelper.cs +++ b/src/MSBuildLocator/DotNetSdkLocationHelper.cs @@ -18,8 +18,9 @@ namespace Microsoft.Build.Locator internal static class DotNetSdkLocationHelper { private static readonly Regex VersionRegex = new Regex(@"^(\d+)\.(\d+)\.(\d+)", RegexOptions.Multiline); - private static readonly Regex SdkRegex = new Regex(@"(\S+) \[(.*?)]$", RegexOptions.Multiline); private static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + private static readonly string ExeName = IsWindows ? "dotnet.exe" : "dotnet"; + private static readonly string DotnetPath = ResolveDotnetPath(); static DotNetSdkLocationHelper() => LoadHostFxr(); @@ -68,11 +69,7 @@ public static IEnumerable GetInstances(string workingDirec } } - /// - /// This native method call determines the actual location of path, including - /// resolving symbolic links. - /// - private static string realpath(string path) + public static IEnumerable GetDotNetBasePaths(string workingDirectory) { string? bestSDK = GetSdkFromGlobalSettings(workingDirectory); if (!string.IsNullOrEmpty(bestSDK)) @@ -89,6 +86,7 @@ private static string realpath(string path) yield return dotnetPaths[i]; } } + private static void LoadHostFxr() { var isOSX = RuntimeInformation.IsOSPlatform(OSPlatform.OSX); @@ -100,9 +98,38 @@ private static void LoadHostFxr() } } - private static string FindDotnetFromEnvironmentVariable(string environmentVariable, string exeName) + private static IntPtr HostFxrResolver(Assembly assembly, string libraryName) { - string dotnet_root = Environment.GetEnvironmentVariable(environmentVariable); + var hostFxrLibName = "libhostfxr.dylib"; + + if (string.Equals(hostFxrLibName, libraryName, StringComparison.Ordinal)) + return IntPtr.Zero; + + var hostFxrRoot = Path.Combine(DotnetPath, "host", "fxr"); + if (Directory.Exists(hostFxrRoot)) + { + // Agreed to load hostfxr from the highest version + var hostFxrAssemblyDirectory = Directory.GetDirectories(hostFxrRoot) + .OrderByDescending(d => d) + .FirstOrDefault(); + + if (hostFxrAssemblyDirectory != null) + { + var hostfxrAssembly = Directory.GetFiles(hostFxrAssemblyDirectory) + .Where(filePath => filePath.Contains(hostFxrLibName, StringComparison.Ordinal)) + .FirstOrDefault(); + + if (hostfxrAssembly != null) + return NativeLibrary.TryLoad(hostfxrAssembly, out var handle) ? handle : IntPtr.Zero; + } + } + + return IntPtr.Zero; + } + + private static string? FindDotnetFromEnvironmentVariable(string environmentVariable, string exeName) + { + string? dotnet_root = Environment.GetEnvironmentVariable(environmentVariable); if (!string.IsNullOrEmpty(dotnet_root)) { string fullPathToDotnetFromRoot = Path.Combine(dotnet_root, exeName); @@ -121,60 +148,6 @@ private static string FindDotnetFromEnvironmentVariable(string environmentVariab return null; } - private static IEnumerable GetDotNetBasePaths(string workingDirectory) - { - string dotnetPath = null; - string exeName = IsWindows ? "dotnet.exe" : "dotnet"; - - // First check for the DOTNET_ROOT environment variable, as it's often there as with, for example, dotnet format. - if (IntPtr.Size == 4) - { - // 32-bit architecture - dotnetPath ??= FindDotnetFromEnvironmentVariable("DOTNET_ROOT(x86)", exeName); - } - else if (IntPtr.Size == 8) - { - // 64-bit architecture - dotnetPath ??= FindDotnetFromEnvironmentVariable("DOTNET_ROOT", exeName); - } - - if (dotnetPath is null) - { - // 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)) - { - if (!IsWindows) - { - filePath = realpath(filePath) ?? filePath; - if (File.Exists(filePath)) - { - dotnetPath = Path.GetDirectoryName(filePath); - break; - } - else - { - continue; - } - } - - dotnetPath = dir; - break; - } - } - } - - if (dotnetPath is null) - { - throw new InvalidOperationException("Could not find the dotnet executable. Is it on the PATH?"); - } - private static string SdkResolutionExceptionMessage(string methodName) => $"Failed to find all versions of .NET Core MSBuild. Call to {methodName}. There may be more details in stderr."; /// @@ -187,9 +160,7 @@ private static IEnumerable GetDotNetBasePaths(string workingDirectory) int rc = 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) - { resolvedSdk = value; - } }); if (rc != 0) @@ -207,9 +178,9 @@ private static string ResolveDotnetPath() string? dotnetExePath = GetCurrentProcessPath(); var isRunFromDotnetExecutable = !string.IsNullOrEmpty(dotnetExePath) && Path.GetFileName(dotnetExePath).Equals(ExeName, StringComparison.InvariantCultureIgnoreCase); + if (isRunFromDotnetExecutable) dotnetPath = Path.GetDirectoryName(dotnetExePath); - else { dotnetPath = FindDotnetPathFromEnvVariable("DOTNET_HOST_PATH") From 2a6bb81e9006167e6798b1c6fa990c2e46d4878c Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Wed, 23 Aug 2023 14:51:07 +0200 Subject: [PATCH 3/7] fix version sorting, Lazy loading for dotnet_path, cleanup --- src/MSBuildLocator/DotNetSdkLocationHelper.cs | 160 +++++++++--------- .../Microsoft.Build.Locator.csproj | 4 + 2 files changed, 87 insertions(+), 77 deletions(-) diff --git a/src/MSBuildLocator/DotNetSdkLocationHelper.cs b/src/MSBuildLocator/DotNetSdkLocationHelper.cs index 3a2fde39..7661c483 100644 --- a/src/MSBuildLocator/DotNetSdkLocationHelper.cs +++ b/src/MSBuildLocator/DotNetSdkLocationHelper.cs @@ -3,12 +3,14 @@ #if NETCOREAPP +using NuGet.Versioning; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; +using System.Runtime.Loader; using System.Text.RegularExpressions; #nullable enable @@ -20,29 +22,35 @@ internal static class DotNetSdkLocationHelper private static readonly Regex VersionRegex = new Regex(@"^(\d+)\.(\d+)\.(\d+)", RegexOptions.Multiline); private static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); private static readonly string ExeName = IsWindows ? "dotnet.exe" : "dotnet"; - private static readonly string DotnetPath = ResolveDotnetPath(); - - static DotNetSdkLocationHelper() => LoadHostFxr(); + private static readonly Lazy DotnetPath = new(() => ResolveDotnetPath()); public static VisualStudioInstance? GetInstance(string dotNetSdkPath) { if (string.IsNullOrWhiteSpace(dotNetSdkPath) || !File.Exists(Path.Combine(dotNetSdkPath, "Microsoft.Build.dll"))) + { return null; + } string versionPath = Path.Combine(dotNetSdkPath, ".version"); if (!File.Exists(versionPath)) + { return null; + } // Preview versions contain a hyphen after the numeric part of the version. Version.TryParse doesn't accept that. Match versionMatch = VersionRegex.Match(File.ReadAllText(versionPath)); if (!versionMatch.Success) + { return null; + } if (!int.TryParse(versionMatch.Groups[1].Value, out int major) || !int.TryParse(versionMatch.Groups[2].Value, out int minor) || !int.TryParse(versionMatch.Groups[3].Value, out int patch)) + { return null; + } // Components of the SDK often have dependencies on the runtime they shipped with, including that several tasks that shipped // in the .NET 5 SDK rely on the .NET 5.0 runtime. Assuming the runtime that shipped with a particular SDK has the same version, @@ -50,7 +58,9 @@ internal static class DotNetSdkLocationHelper // to always work but should work for now. if (major > Environment.Version.Major || (major == Environment.Version.Major && minor > Environment.Version.Minor)) + { return null; + } return new VisualStudioInstance( name: ".NET Core SDK", @@ -65,87 +75,91 @@ public static IEnumerable GetInstances(string workingDirec { var dotnetSdk = GetInstance(basePath); if (dotnetSdk != null) + { yield return dotnetSdk; + } } } - public static IEnumerable GetDotNetBasePaths(string workingDirectory) + private static IEnumerable GetDotNetBasePaths(string workingDirectory) { - string? bestSDK = GetSdkFromGlobalSettings(workingDirectory); - if (!string.IsNullOrEmpty(bestSDK)) - yield return bestSDK; - - string[] dotnetPaths = GetAllAvailableSDKs(); - // We want to return the newest SDKs first, however, so iterate over the list in reverse order. - // If basePath is disqualified because it was later - // than the runtime version, this ensures that RegisterDefaults will return the latest valid - // SDK instead of the earliest installed. - for (int i = dotnetPaths.Length - 1; i >= 0; i--) + try { - if (dotnetPaths[i] != bestSDK) - yield return dotnetPaths[i]; + AddUnmanagedDllResolver(); + + string? bestSDK = GetSdkFromGlobalSettings(workingDirectory); + if (!string.IsNullOrEmpty(bestSDK)) + { + yield return bestSDK; + } + + string[] dotnetPaths = GetAllAvailableSDKs(); + // We want to return the newest SDKs first, however, so iterate over the list in reverse order. + // If basePath is disqualified because it was later + // than the runtime version, this ensures that RegisterDefaults will return the latest valid + // SDK instead of the earliest installed. + for (int i = dotnetPaths.Length - 1; i >= 0; i--) + { + if (dotnetPaths[i] != bestSDK) + { + yield return dotnetPaths[i]; + } + } + } + finally + { + RemoveUnmanagedDllResolver(); } } - private static void LoadHostFxr() + private static void AddUnmanagedDllResolver() => ModifyUnmanagedDllResolver(loadContext => loadContext.ResolvingUnmanagedDll += HostFxrResolver); + + private static void RemoveUnmanagedDllResolver() => ModifyUnmanagedDllResolver(loadContext => loadContext.ResolvingUnmanagedDll -= HostFxrResolver); + + private static void ModifyUnmanagedDllResolver(Action resolverAction) { - var isOSX = RuntimeInformation.IsOSPlatform(OSPlatform.OSX); - if (isOSX) + if (!IsWindows) { - var loadContext = System.Runtime.Loader.AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()); + var loadContext = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()); if (loadContext != null) - loadContext.ResolvingUnmanagedDll += HostFxrResolver; + { + resolverAction(loadContext); + } } } private static IntPtr HostFxrResolver(Assembly assembly, string libraryName) { - var hostFxrLibName = "libhostfxr.dylib"; + var hostFxrLibName = "libhostfxr"; + var libExtention = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "dylib" : "so"; - if (string.Equals(hostFxrLibName, libraryName, StringComparison.Ordinal)) + if (!hostFxrLibName.Equals(libraryName, StringComparison.OrdinalIgnoreCase)) + { return IntPtr.Zero; + } - var hostFxrRoot = Path.Combine(DotnetPath, "host", "fxr"); + var hostFxrRoot = Path.Combine(DotnetPath.Value, "host", "fxr"); if (Directory.Exists(hostFxrRoot)) { // Agreed to load hostfxr from the highest version var hostFxrAssemblyDirectory = Directory.GetDirectories(hostFxrRoot) - .OrderByDescending(d => d) - .FirstOrDefault(); + .Select(str => NuGetVersion.Parse(str)) + .Max(); - if (hostFxrAssemblyDirectory != null) + if (hostFxrAssemblyDirectory != null && !string.IsNullOrEmpty(hostFxrAssemblyDirectory.OriginalVersion)) { - var hostfxrAssembly = Directory.GetFiles(hostFxrAssemblyDirectory) - .Where(filePath => filePath.Contains(hostFxrLibName, StringComparison.Ordinal)) + var hostfxrAssembly = Directory.GetFiles(hostFxrAssemblyDirectory.OriginalVersion) + .Where(filePath => filePath.Equals(Path.Combine(hostFxrLibName, libExtention), StringComparison.OrdinalIgnoreCase)) .FirstOrDefault(); if (hostfxrAssembly != null) - return NativeLibrary.TryLoad(hostfxrAssembly, out var handle) ? handle : IntPtr.Zero; - } - } - - return IntPtr.Zero; - } - - private static string? FindDotnetFromEnvironmentVariable(string environmentVariable, string exeName) - { - string? dotnet_root = Environment.GetEnvironmentVariable(environmentVariable); - if (!string.IsNullOrEmpty(dotnet_root)) - { - string fullPathToDotnetFromRoot = Path.Combine(dotnet_root, exeName); - if (File.Exists(fullPathToDotnetFromRoot)) - { - if (!IsWindows) { - fullPathToDotnetFromRoot = realpath(fullPathToDotnetFromRoot) ?? fullPathToDotnetFromRoot; - return File.Exists(fullPathToDotnetFromRoot) ? Path.GetDirectoryName(fullPathToDotnetFromRoot) : null; + return NativeLibrary.TryLoad(hostfxrAssembly, out var handle) ? handle : IntPtr.Zero; } - - return dotnet_root; } } - return null; + return IntPtr.Zero; } private static string SdkResolutionExceptionMessage(string methodName) => $"Failed to find all versions of .NET Core MSBuild. Call to {methodName}. There may be more details in stderr."; @@ -157,14 +171,18 @@ private static IntPtr HostFxrResolver(Assembly assembly, string libraryName) private static string? GetSdkFromGlobalSettings(string workingDirectory) { string? resolvedSdk = null; - int rc = NativeMethods.hostfxr_resolve_sdk2(exe_dir: DotnetPath, working_dir: workingDirectory, flags: 0, result: (key, value) => + int rc = NativeMethods.hostfxr_resolve_sdk2(exe_dir: DotnetPath.Value, working_dir: workingDirectory, flags: 0, result: (key, value) => { if (key == NativeMethods.hostfxr_resolve_sdk2_result_key_t.resolved_sdk_dir) + { resolvedSdk = value; + } }); if (rc != 0) + { throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_resolve_sdk2))); + } return resolvedSdk; } @@ -180,7 +198,9 @@ private static string ResolveDotnetPath() && Path.GetFileName(dotnetExePath).Equals(ExeName, StringComparison.InvariantCultureIgnoreCase); if (isRunFromDotnetExecutable) + { dotnetPath = Path.GetDirectoryName(dotnetExePath); + } else { dotnetPath = FindDotnetPathFromEnvVariable("DOTNET_HOST_PATH") @@ -190,7 +210,9 @@ private static string ResolveDotnetPath() } if (string.IsNullOrEmpty(dotnetPath)) + { throw new InvalidOperationException("Could not find the dotnet executable. Is it set on the DOTNET_ROOT?"); + } return dotnetPath; } @@ -204,27 +226,7 @@ private static string ResolveDotnetPath() return dotnetPath; } - private static string? GetDotnetPathFromHOST() - { - var dotnetPath = FindDotnetPathFromEnvVariable("DOTNET_HOST_PATH"); - if (dotnetPath == null) - { - dotnetPath ??= FindDotnetPathFromEnvVariable("DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR"); - } - - return dotnetPath; - } - - private static string? GetCurrentProcessPath() - { - string? processPath = null; -#if NET6_0_OR_GREATER - processPath = Environment.ProcessPath; -#else - processPath = Process.GetCurrentProcess().MainModule.FileName; -#endif - return processPath; - } + private static string? GetCurrentProcessPath() => Environment.ProcessPath; private static string? GetDotnetPathFromPATH() { @@ -239,10 +241,11 @@ private static string ResolveDotnetPath() foreach (string dir in paths) { string? filePath = ValidatePath(dir); - if (string.IsNullOrEmpty(filePath)) - continue; - - dotnetPath = dir; + if (!string.IsNullOrEmpty(filePath)) + { + dotnetPath = filePath; + break; + } } return dotnetPath; @@ -254,11 +257,13 @@ private static string ResolveDotnetPath() private static string[] GetAllAvailableSDKs() { string[]? resolvedPaths = null; - int rc = NativeMethods.hostfxr_get_available_sdks(exe_dir: DotnetPath, result: (key, value) => resolvedPaths = value); + int rc = NativeMethods.hostfxr_get_available_sdks(exe_dir: DotnetPath.Value, result: (key, value) => resolvedPaths = value); // Errors are automatically printed to stderr. We should not continue to try to output anything if we failed. if (rc != 0) + { throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_get_available_sdks))); + } return resolvedPaths ?? Array.Empty(); } @@ -272,6 +277,7 @@ private static string[] GetAllAvailableSDKs() IntPtr ptr = NativeMethods.realpath(path, IntPtr.Zero); string? result = Marshal.PtrToStringAuto(ptr); NativeMethods.free(ptr); + return result; } diff --git a/src/MSBuildLocator/Microsoft.Build.Locator.csproj b/src/MSBuildLocator/Microsoft.Build.Locator.csproj index 056f7ca5..79155661 100644 --- a/src/MSBuildLocator/Microsoft.Build.Locator.csproj +++ b/src/MSBuildLocator/Microsoft.Build.Locator.csproj @@ -43,4 +43,8 @@ + + + + From fce84f493790d86d2fe472d8d6d3952b5c62c8fe Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Thu, 24 Aug 2023 19:58:14 +0200 Subject: [PATCH 4/7] add SemanticVersion parser and cover it with UTs --- .../SemanticVersionParserTests.cs | 110 ++++++++++++ src/MSBuildLocator/DotNetSdkLocationHelper.cs | 17 +- .../Microsoft.Build.Locator.csproj | 4 - src/MSBuildLocator/Utils/SemanticVersion.cs | 78 +++++++++ .../Utils/SemanticVersionParser.cs | 162 ++++++++++++++++++ src/MSBuildLocator/Utils/VersionComparer.cs | 151 ++++++++++++++++ 6 files changed, 510 insertions(+), 12 deletions(-) create mode 100644 src/MSBuildLocator.Tests/SemanticVersionParserTests.cs create mode 100644 src/MSBuildLocator/Utils/SemanticVersion.cs create mode 100644 src/MSBuildLocator/Utils/SemanticVersionParser.cs create mode 100644 src/MSBuildLocator/Utils/VersionComparer.cs diff --git a/src/MSBuildLocator.Tests/SemanticVersionParserTests.cs b/src/MSBuildLocator.Tests/SemanticVersionParserTests.cs new file mode 100644 index 00000000..d1f2f05b --- /dev/null +++ b/src/MSBuildLocator.Tests/SemanticVersionParserTests.cs @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#if NETCOREAPP + +using Shouldly; +using System.Linq; +using Xunit; + +namespace Microsoft.Build.Locator.Tests +{ + public class SemanticVersionParserTests + { + private readonly SemanticVersionParser _testedInstance; + public SemanticVersionParserTests() => _testedInstance = new SemanticVersionParser(); + + [Fact] + public void TryParseTest_ReleaseVersion() + { + var version = "7.0.333"; + + var isParsed = _testedInstance.TryParse(version, out var parsedVerion); + + parsedVerion.ShouldNotBeNull(); + isParsed.ShouldBeTrue(); + parsedVerion.Major.ShouldBe(7); + parsedVerion.Minor.ShouldBe(0); + parsedVerion.Patch.ShouldBe(333); + parsedVerion.ReleaseLabels.ShouldBeEmpty(); + } + + [Fact] + public void TryParseTest_PreviewVersion() + { + var version = "8.0.0-preview.6.23329.7"; + + var isParsed = _testedInstance.TryParse(version, out var parsedVerion); + + parsedVerion.ShouldNotBeNull(); + isParsed.ShouldBeTrue(); + parsedVerion.Major.ShouldBe(8); + parsedVerion.Minor.ShouldBe(0); + parsedVerion.Patch.ShouldBe(0); + parsedVerion.ReleaseLabels.ShouldBe(new[] { "preview", "6", "23329", "7" }); + } + + [Fact] + public void TryParseTest_InvalidInput_LeadingZero() + { + var version = "0.0-preview.6"; + + var isParsed = _testedInstance.TryParse(version, out var parsedVerion); + + Assert.Null(parsedVerion); + isParsed.ShouldBeFalse(); + } + + [Fact] + public void TryParseTest_InvalidInput_FourPartsVersion() + { + var version = "5.0.3.4"; + + var isParsed = _testedInstance.TryParse(version, out var parsedVerion); + + Assert.Null(parsedVerion); + isParsed.ShouldBeFalse(); + } + + [Fact] + public void VersionSortingTest_WithPreview() + { + var versions = new[] { "7.0.7", "8.0.0-preview.6.23329.7", "8.0.0-preview.3.23174.8" }; + + var maxVersion = versions.Select(v => _testedInstance.TryParse(v, out var parsedVerion) ? parsedVerion : null).Max(); + + maxVersion.OriginalValue.ShouldBe("8.0.0-preview.6.23329.7"); + } + + [Fact] + public void VersionSortingTest_ReleaseOnly() + { + var versions = new[] { "7.0.7", "3.7.2", "10.0.0" }; + + var maxVersion = versions.Select(v => _testedInstance.TryParse(v, out var parsedVerion) ? parsedVerion : null).Max(); + + maxVersion.OriginalValue.ShouldBe("10.0.0"); + } + + [Fact] + public void VersionSortingTest_WithInvalidFolderNames() + { + var versions = new[] { "7.0.7", "3.7.2", "dummy", "5.7.8.9" }; + + var maxVersion = versions.Select(v => _testedInstance.TryParse(v, out var parsedVerion) ? parsedVerion : null).Max(); + + maxVersion.OriginalValue.ShouldBe("7.0.7"); + } + + [Fact] + public void VersionSortingTest_WithAllInvalidFolderNames() + { + var versions = new[] { "dummy", "5.7.8.9" }; + + var maxVersion = versions.Select(v => _testedInstance.TryParse(v, out var parsedVerion) ? parsedVerion : null).Max(); + + maxVersion.ShouldBeNull(); + } + } +} +#endif \ No newline at end of file diff --git a/src/MSBuildLocator/DotNetSdkLocationHelper.cs b/src/MSBuildLocator/DotNetSdkLocationHelper.cs index 7661c483..46c8ca95 100644 --- a/src/MSBuildLocator/DotNetSdkLocationHelper.cs +++ b/src/MSBuildLocator/DotNetSdkLocationHelper.cs @@ -3,7 +3,6 @@ #if NETCOREAPP -using NuGet.Versioning; using System; using System.Collections.Generic; using System.IO; @@ -50,7 +49,7 @@ internal static class DotNetSdkLocationHelper !int.TryParse(versionMatch.Groups[3].Value, out int patch)) { return null; - } + } // Components of the SDK often have dependencies on the runtime they shipped with, including that several tasks that shipped // in the .NET 5 SDK rely on the .NET 5.0 runtime. Assuming the runtime that shipped with a particular SDK has the same version, @@ -118,6 +117,7 @@ private static IEnumerable GetDotNetBasePaths(string workingDirectory) private static void ModifyUnmanagedDllResolver(Action resolverAction) { + // For Windows hostfxr is loaded in the process. if (!IsWindows) { var loadContext = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()); @@ -133,7 +133,7 @@ private static IntPtr HostFxrResolver(Assembly assembly, string libraryName) var hostFxrLibName = "libhostfxr"; var libExtention = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "dylib" : "so"; - if (!hostFxrLibName.Equals(libraryName, StringComparison.OrdinalIgnoreCase)) + if (!hostFxrLibName.Equals(libraryName)) { return IntPtr.Zero; } @@ -141,15 +141,16 @@ private static IntPtr HostFxrResolver(Assembly assembly, string libraryName) var hostFxrRoot = Path.Combine(DotnetPath.Value, "host", "fxr"); if (Directory.Exists(hostFxrRoot)) { - // Agreed to load hostfxr from the highest version + var versionParser = new SemanticVersionParser(); + // Load hostfxr from the highest version, because it should be backward-compatible var hostFxrAssemblyDirectory = Directory.GetDirectories(hostFxrRoot) - .Select(str => NuGetVersion.Parse(str)) + .Select(str => versionParser.TryParse(str, out var version) ? version : null) .Max(); - if (hostFxrAssemblyDirectory != null && !string.IsNullOrEmpty(hostFxrAssemblyDirectory.OriginalVersion)) + if (hostFxrAssemblyDirectory != null && !string.IsNullOrEmpty(hostFxrAssemblyDirectory.OriginalValue)) { - var hostfxrAssembly = Directory.GetFiles(hostFxrAssemblyDirectory.OriginalVersion) - .Where(filePath => filePath.Equals(Path.Combine(hostFxrLibName, libExtention), StringComparison.OrdinalIgnoreCase)) + var hostfxrAssembly = Directory.GetFiles(hostFxrAssemblyDirectory.OriginalValue) + .Where(filePath => filePath.Equals(Path.Combine(hostFxrLibName, libExtention))) .FirstOrDefault(); if (hostfxrAssembly != null) diff --git a/src/MSBuildLocator/Microsoft.Build.Locator.csproj b/src/MSBuildLocator/Microsoft.Build.Locator.csproj index 79155661..056f7ca5 100644 --- a/src/MSBuildLocator/Microsoft.Build.Locator.csproj +++ b/src/MSBuildLocator/Microsoft.Build.Locator.csproj @@ -43,8 +43,4 @@ - - - - diff --git a/src/MSBuildLocator/Utils/SemanticVersion.cs b/src/MSBuildLocator/Utils/SemanticVersion.cs new file mode 100644 index 00000000..17c7cb19 --- /dev/null +++ b/src/MSBuildLocator/Utils/SemanticVersion.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.Build.Locator +{ + internal class SemanticVersion : IComparable + { + private readonly IEnumerable _releaseLabels; + private Version _version; + private string _originalValue; + + public SemanticVersion(Version version, IEnumerable releaseLabels, string originalValue) + { + _version = version ?? throw new ArgumentNullException(nameof(version)); + + if (releaseLabels != null) + { + _releaseLabels = releaseLabels.ToArray(); + } + + _originalValue = originalValue; + } + + /// + /// Major version X (X.y.z) + /// + public int Major => _version.Major; + + /// + /// Minor version Y (x.Y.z) + /// + public int Minor => _version.Minor; + + /// + /// Patch version Z (x.y.Z) + /// + public int Patch => _version.Build; + + /// + /// A collection of pre-release labels attached to the version. + /// + public IEnumerable ReleaseLabels => _releaseLabels ?? Enumerable.Empty(); + + public string OriginalValue => _originalValue; + + /// + /// The full pre-release label for the version. + /// + public string Release => _releaseLabels != null ? String.Join(".", _releaseLabels) : String.Empty; + + /// + /// True if pre-release labels exist for the version. + /// + public bool IsPrerelease + { + get + { + if (ReleaseLabels != null) + { + var enumerator = ReleaseLabels.GetEnumerator(); + return (enumerator.MoveNext() && !String.IsNullOrEmpty(enumerator.Current)); + } + + return false; + } + } + + /// + /// Compare versions. + /// + public int CompareTo(SemanticVersion other) + { + var comparer = new VersionComparer(); + return comparer.Compare(this, other); + } + } +} diff --git a/src/MSBuildLocator/Utils/SemanticVersionParser.cs b/src/MSBuildLocator/Utils/SemanticVersionParser.cs new file mode 100644 index 00000000..3778b773 --- /dev/null +++ b/src/MSBuildLocator/Utils/SemanticVersionParser.cs @@ -0,0 +1,162 @@ +#if NETCOREAPP + +using System; +using System.Linq; + +namespace Microsoft.Build.Locator +{ + internal class SemanticVersionParser + { + /// + /// Parse a version string + /// + /// false if the version wasn't parsed + public bool TryParse(string value, out SemanticVersion version) + { + version = null; + + if (value != null) + { + var (versionString, releaseLabels) = ParseSections(value); + + if (Version.TryParse(versionString, out Version systemVersion)) + { + // validate the version string + string[] parts = versionString.Split('.'); + + // versions must be 3 parts + if (parts.Length != 3) + { + return false; + } + + foreach (var part in parts) + { + if (!IsValidPart(part, false)) + { + // leading zeros are not allowed + return false; + } + } + + if (releaseLabels != null && !releaseLabels.All(s => IsValidPart(s, false))) + { + return false; + } + + version = new SemanticVersion( + NormalizeVersionValue(systemVersion), + releaseLabels, + value); + + return true; + } + } + + return false; + } + + private bool IsLetterOrDigitOrDash(char c) + { + int x = (int)c; + + // "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-" + return (x >= 48 && x <= 57) || (x >= 65 && x <= 90) || (x >= 97 && x <= 122) || x == 45; + } + + private bool IsValidPart(string s, bool allowLeadingZeros) => IsValidPart(s.ToCharArray(), allowLeadingZeros); + + private bool IsValidPart(char[] chars, bool allowLeadingZeros) + { + bool result = true; + + if (chars.Length == 0) + { + // empty labels are not allowed + result = false; + } + + // 0 is fine, but 00 is not. + // 0A counts as an alpha numeric string where zeros are not counted + if (!allowLeadingZeros && chars.Length > 1 && chars[0] == '0' && chars.All(c => Char.IsDigit(c))) + { + // no leading zeros in labels allowed + result = false; + } + else + { + result &= chars.All(c => IsLetterOrDigitOrDash(c)); + } + + return result; + } + + /// + /// Parse the version string into version/release + /// + private (string Version, string[] ReleaseLabels) ParseSections(string value) + { + string versionString = null; + string[] releaseLabels = null; + + int dashPos = -1; + int plusPos = -1; + + char[] chars = value.ToCharArray(); + + bool end; + for (int i = 0; i < chars.Length; i++) + { + end = (i == chars.Length - 1); + + if (dashPos < 0) + { + if (end || chars[i] == '-' || chars[i] == '+') + { + int endPos = i + (end ? 1 : 0); + versionString = value.Substring(0, endPos); + + dashPos = i; + + if (chars[i] == '+') + { + plusPos = i; + } + } + } + else if (plusPos < 0) + { + if (end || chars[i] == '+') + { + int start = dashPos + 1; + int endPos = i + (end ? 1 : 0); + string releaseLabel = value.Substring(start, endPos - start); + + releaseLabels = releaseLabel.Split('.'); + + plusPos = i; + } + } + } + + return (versionString, releaseLabels); + } + + private Version NormalizeVersionValue(Version version) + { + Version normalized = version; + + if (version.Build < 0 || version.Revision < 0) + { + normalized = new Version( + version.Major, + version.Minor, + Math.Max(version.Build, 0), + Math.Max(version.Revision, 0)); + } + + return normalized; + } + } +} +#endif \ No newline at end of file diff --git a/src/MSBuildLocator/Utils/VersionComparer.cs b/src/MSBuildLocator/Utils/VersionComparer.cs new file mode 100644 index 00000000..55f0805c --- /dev/null +++ b/src/MSBuildLocator/Utils/VersionComparer.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; + +namespace Microsoft.Build.Locator +{ + internal sealed class VersionComparer + { + /// + /// Determines if both versions are equal. + /// + public bool Equals(SemanticVersion x, SemanticVersion y) + { + return Compare(x, y) == 0; + } + + /// + /// Compare versions. + /// + public int Compare(SemanticVersion x, SemanticVersion y) + { + if (Object.ReferenceEquals(x, y)) + { + return 0; + } + + if (Object.ReferenceEquals(y, null)) + { + return 1; + } + + if (Object.ReferenceEquals(x, null)) + { + return -1; + } + + if (x != null && y != null) + { + // compare version + int result = x.Major.CompareTo(y.Major); + if (result != 0) + { + return result; + } + + result = x.Minor.CompareTo(y.Minor); + if (result != 0) + { + return result; + } + + result = x.Patch.CompareTo(y.Patch); + if (result != 0) + { + return result; + } + + // compare release labels + if (x.IsPrerelease && !y.IsPrerelease) + { + return -1; + } + + if (!x.IsPrerelease && y.IsPrerelease) + { + return 1; + } + + if (x.IsPrerelease && y.IsPrerelease) + { + result = CompareReleaseLabels(x.ReleaseLabels, y.ReleaseLabels); + if (result != 0) + { + return result; + } + } + } + + return 0; + } + + /// + /// Compares sets of release labels. + /// + private static int CompareReleaseLabels(IEnumerable version1, IEnumerable version2) + { + int result = 0; + + IEnumerator a = version1.GetEnumerator(); + IEnumerator b = version2.GetEnumerator(); + + bool aExists = a.MoveNext(); + bool bExists = b.MoveNext(); + + while (aExists || bExists) + { + if (!aExists && bExists) + { + return -1; + } + + if (aExists && !bExists) + { + return 1; + } + + result = CompareRelease(a.Current, b.Current); + + if (result != 0) + { + return result; + } + + aExists = a.MoveNext(); + bExists = b.MoveNext(); + } + + return result; + } + + /// + /// Release labels are compared as numbers if they are numeric, otherwise they will be compared + /// as strings. + /// + private static int CompareRelease(string version1, string version2) + { + int result; + + // check if the identifiers are numeric + bool v1IsNumeric = Int32.TryParse(version1, out int version1Num); + bool v2IsNumeric = Int32.TryParse(version2, out int version2Num); + + // if both are numeric compare them as numbers + if (v1IsNumeric && v2IsNumeric) + { + result = version1Num.CompareTo(version2Num); + } + else if (v1IsNumeric || v2IsNumeric) + { + // numeric labels come before alpha labels + result = v1IsNumeric ? -1 : 1; + } + else + { + // Ignoring 2.0.0 case sensitive compare. Everything will be compared case insensitively as 2.0.1 specifies. + result = StringComparer.OrdinalIgnoreCase.Compare(version1, version2); + } + + return result; + } + } +} From cede57eca7e804b1f8f4812cbd9156b57a989999 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Fri, 25 Aug 2023 07:43:00 +0200 Subject: [PATCH 5/7] fix review comments --- .../SemanticVersionParserTests.cs | 19 +++++++-------- src/MSBuildLocator/DotNetSdkLocationHelper.cs | 7 ++---- src/MSBuildLocator/Utils/SemanticVersion.cs | 5 +++- .../Utils/SemanticVersionParser.cs | 23 ++++++++++++------- src/MSBuildLocator/Utils/VersionComparer.cs | 10 ++++---- 5 files changed, 34 insertions(+), 30 deletions(-) diff --git a/src/MSBuildLocator.Tests/SemanticVersionParserTests.cs b/src/MSBuildLocator.Tests/SemanticVersionParserTests.cs index d1f2f05b..f9e57b3f 100644 --- a/src/MSBuildLocator.Tests/SemanticVersionParserTests.cs +++ b/src/MSBuildLocator.Tests/SemanticVersionParserTests.cs @@ -11,15 +11,12 @@ namespace Microsoft.Build.Locator.Tests { public class SemanticVersionParserTests { - private readonly SemanticVersionParser _testedInstance; - public SemanticVersionParserTests() => _testedInstance = new SemanticVersionParser(); - [Fact] public void TryParseTest_ReleaseVersion() { var version = "7.0.333"; - var isParsed = _testedInstance.TryParse(version, out var parsedVerion); + var isParsed = SemanticVersionParser.TryParse(version, out var parsedVerion); parsedVerion.ShouldNotBeNull(); isParsed.ShouldBeTrue(); @@ -34,7 +31,7 @@ public void TryParseTest_PreviewVersion() { var version = "8.0.0-preview.6.23329.7"; - var isParsed = _testedInstance.TryParse(version, out var parsedVerion); + var isParsed = SemanticVersionParser.TryParse(version, out var parsedVerion); parsedVerion.ShouldNotBeNull(); isParsed.ShouldBeTrue(); @@ -49,7 +46,7 @@ public void TryParseTest_InvalidInput_LeadingZero() { var version = "0.0-preview.6"; - var isParsed = _testedInstance.TryParse(version, out var parsedVerion); + var isParsed = SemanticVersionParser.TryParse(version, out var parsedVerion); Assert.Null(parsedVerion); isParsed.ShouldBeFalse(); @@ -60,7 +57,7 @@ public void TryParseTest_InvalidInput_FourPartsVersion() { var version = "5.0.3.4"; - var isParsed = _testedInstance.TryParse(version, out var parsedVerion); + var isParsed = SemanticVersionParser.TryParse(version, out var parsedVerion); Assert.Null(parsedVerion); isParsed.ShouldBeFalse(); @@ -71,7 +68,7 @@ public void VersionSortingTest_WithPreview() { var versions = new[] { "7.0.7", "8.0.0-preview.6.23329.7", "8.0.0-preview.3.23174.8" }; - var maxVersion = versions.Select(v => _testedInstance.TryParse(v, out var parsedVerion) ? parsedVerion : null).Max(); + var maxVersion = versions.Select(v => SemanticVersionParser.TryParse(v, out var parsedVerion) ? parsedVerion : null).Max(); maxVersion.OriginalValue.ShouldBe("8.0.0-preview.6.23329.7"); } @@ -81,7 +78,7 @@ public void VersionSortingTest_ReleaseOnly() { var versions = new[] { "7.0.7", "3.7.2", "10.0.0" }; - var maxVersion = versions.Select(v => _testedInstance.TryParse(v, out var parsedVerion) ? parsedVerion : null).Max(); + var maxVersion = versions.Max(v => SemanticVersionParser.TryParse(v, out var parsedVerion) ? parsedVerion : null); maxVersion.OriginalValue.ShouldBe("10.0.0"); } @@ -91,7 +88,7 @@ public void VersionSortingTest_WithInvalidFolderNames() { var versions = new[] { "7.0.7", "3.7.2", "dummy", "5.7.8.9" }; - var maxVersion = versions.Select(v => _testedInstance.TryParse(v, out var parsedVerion) ? parsedVerion : null).Max(); + var maxVersion = versions.Max(v => SemanticVersionParser.TryParse(v, out var parsedVerion) ? parsedVerion : null); maxVersion.OriginalValue.ShouldBe("7.0.7"); } @@ -101,7 +98,7 @@ public void VersionSortingTest_WithAllInvalidFolderNames() { var versions = new[] { "dummy", "5.7.8.9" }; - var maxVersion = versions.Select(v => _testedInstance.TryParse(v, out var parsedVerion) ? parsedVerion : null).Max(); + var maxVersion = versions.Max(v => SemanticVersionParser.TryParse(v, out var parsedVerion) ? parsedVerion : null); maxVersion.ShouldBeNull(); } diff --git a/src/MSBuildLocator/DotNetSdkLocationHelper.cs b/src/MSBuildLocator/DotNetSdkLocationHelper.cs index 46c8ca95..37874a04 100644 --- a/src/MSBuildLocator/DotNetSdkLocationHelper.cs +++ b/src/MSBuildLocator/DotNetSdkLocationHelper.cs @@ -141,17 +141,14 @@ private static IntPtr HostFxrResolver(Assembly assembly, string libraryName) var hostFxrRoot = Path.Combine(DotnetPath.Value, "host", "fxr"); if (Directory.Exists(hostFxrRoot)) { - var versionParser = new SemanticVersionParser(); // Load hostfxr from the highest version, because it should be backward-compatible var hostFxrAssemblyDirectory = Directory.GetDirectories(hostFxrRoot) - .Select(str => versionParser.TryParse(str, out var version) ? version : null) - .Max(); + .Max(str => SemanticVersionParser.TryParse(str, out var version) ? version : null); if (hostFxrAssemblyDirectory != null && !string.IsNullOrEmpty(hostFxrAssemblyDirectory.OriginalValue)) { var hostfxrAssembly = Directory.GetFiles(hostFxrAssemblyDirectory.OriginalValue) - .Where(filePath => filePath.Equals(Path.Combine(hostFxrLibName, libExtention))) - .FirstOrDefault(); + .FirstOrDefault(filePath => filePath.Equals(Path.Combine(hostFxrLibName, libExtention))); if (hostfxrAssembly != null) { diff --git a/src/MSBuildLocator/Utils/SemanticVersion.cs b/src/MSBuildLocator/Utils/SemanticVersion.cs index 17c7cb19..4a3bcb11 100644 --- a/src/MSBuildLocator/Utils/SemanticVersion.cs +++ b/src/MSBuildLocator/Utils/SemanticVersion.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; using System.Collections.Generic; using System.Linq; diff --git a/src/MSBuildLocator/Utils/SemanticVersionParser.cs b/src/MSBuildLocator/Utils/SemanticVersionParser.cs index 3778b773..9bea0025 100644 --- a/src/MSBuildLocator/Utils/SemanticVersionParser.cs +++ b/src/MSBuildLocator/Utils/SemanticVersionParser.cs @@ -1,17 +1,24 @@ -#if NETCOREAPP +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#if NETCOREAPP using System; using System.Linq; namespace Microsoft.Build.Locator { - internal class SemanticVersionParser + /// + /// Converts string in its semantic version. + /// The basic parser logic is taken from https://github.com/NuGetArchive/NuGet.Versioning/releases/tag/rc-preview1. + /// + internal static class SemanticVersionParser { /// /// Parse a version string /// /// false if the version wasn't parsed - public bool TryParse(string value, out SemanticVersion version) + public static bool TryParse(string value, out SemanticVersion version) { version = null; @@ -56,7 +63,7 @@ public bool TryParse(string value, out SemanticVersion version) return false; } - private bool IsLetterOrDigitOrDash(char c) + private static bool IsLetterOrDigitOrDash(char c) { int x = (int)c; @@ -64,9 +71,9 @@ private bool IsLetterOrDigitOrDash(char c) return (x >= 48 && x <= 57) || (x >= 65 && x <= 90) || (x >= 97 && x <= 122) || x == 45; } - private bool IsValidPart(string s, bool allowLeadingZeros) => IsValidPart(s.ToCharArray(), allowLeadingZeros); + private static bool IsValidPart(string s, bool allowLeadingZeros) => IsValidPart(s.ToCharArray(), allowLeadingZeros); - private bool IsValidPart(char[] chars, bool allowLeadingZeros) + private static bool IsValidPart(char[] chars, bool allowLeadingZeros) { bool result = true; @@ -94,7 +101,7 @@ private bool IsValidPart(char[] chars, bool allowLeadingZeros) /// /// Parse the version string into version/release /// - private (string Version, string[] ReleaseLabels) ParseSections(string value) + private static (string Version, string[] ReleaseLabels) ParseSections(string value) { string versionString = null; string[] releaseLabels = null; @@ -142,7 +149,7 @@ private bool IsValidPart(char[] chars, bool allowLeadingZeros) return (versionString, releaseLabels); } - private Version NormalizeVersionValue(Version version) + private static Version NormalizeVersionValue(Version version) { Version normalized = version; diff --git a/src/MSBuildLocator/Utils/VersionComparer.cs b/src/MSBuildLocator/Utils/VersionComparer.cs index 55f0805c..b5dabba2 100644 --- a/src/MSBuildLocator/Utils/VersionComparer.cs +++ b/src/MSBuildLocator/Utils/VersionComparer.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; using System.Collections.Generic; namespace Microsoft.Build.Locator @@ -8,10 +11,7 @@ internal sealed class VersionComparer /// /// Determines if both versions are equal. /// - public bool Equals(SemanticVersion x, SemanticVersion y) - { - return Compare(x, y) == 0; - } + public bool Equals(SemanticVersion x, SemanticVersion y) => Compare(x, y) == 0; /// /// Compare versions. From 4ced06a2a794fca9062f735c088d092af0ae185e Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Fri, 25 Aug 2023 09:47:37 +0200 Subject: [PATCH 6/7] fix issue with combining fileName and extension --- src/MSBuildLocator/DotNetSdkLocationHelper.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/MSBuildLocator/DotNetSdkLocationHelper.cs b/src/MSBuildLocator/DotNetSdkLocationHelper.cs index 37874a04..1f038a8a 100644 --- a/src/MSBuildLocator/DotNetSdkLocationHelper.cs +++ b/src/MSBuildLocator/DotNetSdkLocationHelper.cs @@ -131,7 +131,7 @@ private static void ModifyUnmanagedDllResolver(Action resol private static IntPtr HostFxrResolver(Assembly assembly, string libraryName) { var hostFxrLibName = "libhostfxr"; - var libExtention = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "dylib" : "so"; + var libExtension = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "dylib" : "so"; if (!hostFxrLibName.Equals(libraryName)) { @@ -147,8 +147,7 @@ private static IntPtr HostFxrResolver(Assembly assembly, string libraryName) if (hostFxrAssemblyDirectory != null && !string.IsNullOrEmpty(hostFxrAssemblyDirectory.OriginalValue)) { - var hostfxrAssembly = Directory.GetFiles(hostFxrAssemblyDirectory.OriginalValue) - .FirstOrDefault(filePath => filePath.Equals(Path.Combine(hostFxrLibName, libExtention))); + var hostfxrAssembly = Path.Combine(hostFxrAssemblyDirectory.OriginalValue, Path.ChangeExtension(hostFxrLibName, libExtension)); if (hostfxrAssembly != null) { From 94f96d71ec339892e45264fed2f9624fb45eca79 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Fri, 25 Aug 2023 12:04:09 +0200 Subject: [PATCH 7/7] update check for hostFxrAssembly presence --- src/MSBuildLocator/DotNetSdkLocationHelper.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/MSBuildLocator/DotNetSdkLocationHelper.cs b/src/MSBuildLocator/DotNetSdkLocationHelper.cs index 1f038a8a..fab49876 100644 --- a/src/MSBuildLocator/DotNetSdkLocationHelper.cs +++ b/src/MSBuildLocator/DotNetSdkLocationHelper.cs @@ -147,11 +147,11 @@ private static IntPtr HostFxrResolver(Assembly assembly, string libraryName) if (hostFxrAssemblyDirectory != null && !string.IsNullOrEmpty(hostFxrAssemblyDirectory.OriginalValue)) { - var hostfxrAssembly = Path.Combine(hostFxrAssemblyDirectory.OriginalValue, Path.ChangeExtension(hostFxrLibName, libExtension)); + var hostFxrAssembly = Path.Combine(hostFxrAssemblyDirectory.OriginalValue, Path.ChangeExtension(hostFxrLibName, libExtension)); - if (hostfxrAssembly != null) + if (File.Exists(hostFxrAssembly)) { - return NativeLibrary.TryLoad(hostfxrAssembly, out var handle) ? handle : IntPtr.Zero; + return NativeLibrary.TryLoad(hostFxrAssembly, out var handle) ? handle : IntPtr.Zero; } } }