diff --git a/build/DependencyVersions.props b/build/DependencyVersions.props
index 0b0cf7b09..3d0d589e4 100644
--- a/build/DependencyVersions.props
+++ b/build/DependencyVersions.props
@@ -11,7 +11,10 @@
2.1.1
2.1.1
2.1.3-servicing-26724-03
- $(MicrosoftNETCoreAppPackageVersion)
+
+
+ 3.0.0-preview1-26816-04
+
15.9.0-preview-000006
$(MicrosoftBuildPackageVersion)
$(MicrosoftBuildPackageVersion)
diff --git a/src/Microsoft.DotNet.MSBuildSdkResolver/Interop.Common.cs b/src/Microsoft.DotNet.MSBuildSdkResolver/Interop.Common.cs
deleted file mode 100644
index 580d1220b..000000000
--- a/src/Microsoft.DotNet.MSBuildSdkResolver/Interop.Common.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright (c) .NET Foundation and contributors. All rights reserved.
-// Licensed under the MIT license. See LICENSE file in the project root for full license information.
-
-using System;
-using System.Diagnostics;
-using System.Text;
-
-namespace Microsoft.DotNet.MSBuildSdkResolver
-{
- internal static partial class Interop
- {
- internal static string hostfxr_resolve_sdk(string exe_dir, string working_dir)
- {
- var buffer = new StringBuilder(capacity: 64);
-
- for (;;)
- {
- int size = hostfxr_resolve_sdk(exe_dir, working_dir, buffer, buffer.Capacity);
- if (size <= 0)
- {
- Debug.Assert(size == 0);
- return null;
- }
-
- if (size <= buffer.Capacity)
- {
- break;
- }
-
- buffer.Capacity = size;
- }
-
- return buffer.ToString();
- }
- }
-}
\ No newline at end of file
diff --git a/src/Microsoft.DotNet.MSBuildSdkResolver/Interop.NETFramework.cs b/src/Microsoft.DotNet.MSBuildSdkResolver/Interop.NETFramework.cs
deleted file mode 100644
index 2101637f3..000000000
--- a/src/Microsoft.DotNet.MSBuildSdkResolver/Interop.NETFramework.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright (c) .NET Foundation and contributors. All rights reserved.
-// Licensed under the MIT license. See LICENSE file in the project root for full license information.
-
-#if NET46
-
-using System;
-using System.IO;
-using System.Runtime.InteropServices;
-using System.Text;
-
-namespace Microsoft.DotNet.MSBuildSdkResolver
-{
- internal static partial class Interop
- {
- internal static readonly bool RunningOnWindows = true;
-
- static Interop()
- {
- PreloadLibrary("hostfxr.dll");
- }
-
- // MSBuild SDK resolvers are required to be AnyCPU, but we have a native dependency and .NETFramework does not
- // have a built-in facility for dynamically loading user native dlls for the appropriate platform. We therefore
- // preload the version with the correct architecture (from a corresponding sub-folder relative to us) on static
- // construction so that subsequent P/Invokes can find it.
- private static void PreloadLibrary(string dllFileName)
- {
- string basePath = Path.GetDirectoryName(typeof(Interop).Assembly.Location);
- string architecture = IntPtr.Size == 8 ? "x64" : "x86";
- string dllPath = Path.Combine(basePath, architecture, dllFileName);
-
- // return value is intentially ignored as we let the subsequent P/Invokes fail naturally.
- LoadLibraryExW(dllPath, IntPtr.Zero, LOAD_WITH_ALTERED_SEARCH_PATH);
- }
-
- // lpFileName passed to LoadLibraryEx must be a full path.
- private const int LOAD_WITH_ALTERED_SEARCH_PATH = 0x8;
-
- [DllImport("kernel32", CharSet = CharSet.Unicode, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
- private static extern IntPtr LoadLibraryExW(string lpFileName, IntPtr hFile, int dwFlags);
-
- [DllImport("hostfxr", CharSet = CharSet.Unicode, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
- private static extern int hostfxr_resolve_sdk(string exe_dir, string working_dir, [Out] StringBuilder buffer, int buffer_size);
- }
-}
-
-#endif // NET46
\ No newline at end of file
diff --git a/src/Microsoft.DotNet.MSBuildSdkResolver/Interop.NETStandard.cs b/src/Microsoft.DotNet.MSBuildSdkResolver/Interop.NETStandard.cs
deleted file mode 100644
index 245f153a0..000000000
--- a/src/Microsoft.DotNet.MSBuildSdkResolver/Interop.NETStandard.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-// Copyright (c) .NET Foundation and contributors. All rights reserved.
-// Licensed under the MIT license. See LICENSE file in the project root for full license information.
-
-// NOTE: the NET46 build ships with Visual Studio/desktop msbuild on Windows.
-// The netstandard1.5 adaptation here acts a proof-of-concept for cross-platform
-// portability of the underlying hostfxr API and gives us build and test coverage
-// on non-Windows machines. It also ships with msbuild on Mono.
-#if NETSTANDARD2_0
-
-using System;
-using System.Runtime.InteropServices;
-using System.Text;
-
-namespace Microsoft.DotNet.MSBuildSdkResolver
-{
- internal static partial class Interop
- {
- internal static readonly bool RunningOnWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
-
- internal static string realpath(string path)
- {
- var ptr = unix_realpath(path, IntPtr.Zero);
- var result = Marshal.PtrToStringAnsi(ptr); // uses UTF8 on Unix
- unix_free(ptr);
- return result;
- }
-
- private static int hostfxr_resolve_sdk(string exe_dir, string working_dir, [Out] StringBuilder buffer, int buffer_size)
- {
- // hostfxr string encoding is platform -specific so dispatch to the
- // appropriately annotated P/Invoke for the current platform.
- return RunningOnWindows
- ? windows_hostfxr_resolve_sdk(exe_dir, working_dir, buffer, buffer_size)
- : unix_hostfxr_resolve_sdk(exe_dir, working_dir, buffer, buffer_size);
- }
-
- [DllImport("hostfxr", EntryPoint = nameof(hostfxr_resolve_sdk), CharSet = CharSet.Unicode, ExactSpelling=true, CallingConvention = CallingConvention.Cdecl)]
- private static extern int windows_hostfxr_resolve_sdk(string exe_dir, string working_dir, [Out] StringBuilder buffer, int buffer_size);
-
- // CharSet.Ansi is UTF8 on Unix
- [DllImport("hostfxr", EntryPoint = nameof(hostfxr_resolve_sdk), CharSet = CharSet.Ansi, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
- private static extern int unix_hostfxr_resolve_sdk(string exe_dir, string working_dir, [Out] StringBuilder buffer, int buffer_size);
-
- // CharSet.Ansi is UTF8 on Unix
- [DllImport("libc", EntryPoint = nameof(realpath), CharSet = CharSet.Ansi, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
- private static extern IntPtr unix_realpath(string path, IntPtr buffer);
-
- [DllImport("libc", EntryPoint = "free", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
- private static extern void unix_free(IntPtr ptr);
- }
-}
-
-#endif // NETSTANDARD2_0
diff --git a/src/Microsoft.DotNet.MSBuildSdkResolver/Interop.cs b/src/Microsoft.DotNet.MSBuildSdkResolver/Interop.cs
new file mode 100644
index 000000000..cda6dd3d0
--- /dev/null
+++ b/src/Microsoft.DotNet.MSBuildSdkResolver/Interop.cs
@@ -0,0 +1,135 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace Microsoft.DotNet.MSBuildSdkResolver
+{
+ internal static partial class Interop
+ {
+ internal static readonly bool RunningOnWindows =
+#if NET46
+ // Not using RuntimeInformation on NET46 to avoid non-in-box framework API,
+ // which create deployment problems for the resolver.
+ Path.DirectorySeparatorChar == '\\';
+#else
+ RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
+#endif
+
+ static Interop()
+ {
+ if (RunningOnWindows)
+ {
+ PreloadWindowsLibrary("hostfxr.dll");
+ }
+ }
+
+ // MSBuild SDK resolvers are required to be AnyCPU, but we have a native dependency and .NETFramework does not
+ // have a built-in facility for dynamically loading user native dlls for the appropriate platform. We therefore
+ // preload the version with the correct architecture (from a corresponding sub-folder relative to us) on static
+ // construction so that subsequent P/Invokes can find it.
+ private static void PreloadWindowsLibrary(string dllFileName)
+ {
+ string basePath = Path.GetDirectoryName(typeof(Interop).Assembly.Location);
+ string architecture = IntPtr.Size == 8 ? "x64" : "x86";
+ string dllPath = Path.Combine(basePath, architecture, dllFileName);
+
+ // return value is intentially ignored as we let the subsequent P/Invokes fail naturally.
+ LoadLibraryExW(dllPath, IntPtr.Zero, LOAD_WITH_ALTERED_SEARCH_PATH);
+ }
+
+ // lpFileName passed to LoadLibraryEx must be a full path.
+ private const int LOAD_WITH_ALTERED_SEARCH_PATH = 0x8;
+
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
+ private static extern IntPtr LoadLibraryExW(string lpFileName, IntPtr hFile, int dwFlags);
+
+
+ [Flags]
+ internal enum hostfxr_resolve_sdk2_flags_t : int
+ {
+ disallow_prerelease = 0x1,
+ }
+
+ internal enum hostfxr_resolve_sdk2_result_key_t : int
+ {
+ resolved_sdk_dir = 0,
+ global_json_path = 1,
+ }
+
+ internal static class Windows
+ {
+ private const CharSet UTF16 = CharSet.Unicode;
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = UTF16)]
+ internal delegate void hostfxr_resolve_sdk2_result_fn(
+ hostfxr_resolve_sdk2_result_key_t key,
+ string value);
+
+ [DllImport("hostfxr", CharSet = UTF16, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
+ internal static extern int hostfxr_resolve_sdk2(
+ string exe_dir,
+ string working_dir,
+ hostfxr_resolve_sdk2_flags_t flags,
+ hostfxr_resolve_sdk2_result_fn result);
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = UTF16)]
+ internal delegate void hostfxr_get_available_sdks_result_fn(
+ int sdk_count,
+ [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)]
+ string[] sdk_dirs);
+
+ [DllImport("hostfxr", CharSet = UTF16, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
+ internal static extern int hostfxr_get_available_sdks(
+ string exe_dir,
+ hostfxr_get_available_sdks_result_fn result);
+ }
+
+ internal static class Unix
+ {
+ // Ansi marhsaling on Unix is actually UTF8
+ private const CharSet UTF8 = CharSet.Ansi;
+ private static string PtrToStringUTF8(IntPtr ptr) => Marshal.PtrToStringAnsi(ptr);
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = UTF8)]
+ internal delegate void hostfxr_resolve_sdk2_result_fn(
+ hostfxr_resolve_sdk2_result_key_t key,
+ string value);
+
+ [DllImport("hostfxr", CharSet = UTF8, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
+ internal static extern int hostfxr_resolve_sdk2(
+ string exe_dir,
+ string working_dir,
+ hostfxr_resolve_sdk2_flags_t flags,
+ hostfxr_resolve_sdk2_result_fn result);
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = UTF8)]
+ internal delegate void hostfxr_get_available_sdks_result_fn(
+ int sdk_count,
+ [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)]
+ string[] sdk_dirs);
+
+ [DllImport("hostfxr", CharSet = UTF8, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
+ internal static extern int hostfxr_get_available_sdks(
+ string exe_dir,
+ hostfxr_get_available_sdks_result_fn result);
+
+ [DllImport("libc", CharSet = UTF8, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
+ private static extern IntPtr realpath(string path, IntPtr buffer);
+
+ [DllImport("libc", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
+ private static extern void free(IntPtr ptr);
+
+ internal static string realpath(string path)
+ {
+ var ptr = realpath(path, IntPtr.Zero);
+ var result = PtrToStringUTF8(ptr);
+ free(ptr);
+ return result;
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.DotNet.MSBuildSdkResolver/MSBuildSdkResolver.cs b/src/Microsoft.DotNet.MSBuildSdkResolver/MSBuildSdkResolver.cs
index a115a867b..ce648ed7e 100644
--- a/src/Microsoft.DotNet.MSBuildSdkResolver/MSBuildSdkResolver.cs
+++ b/src/Microsoft.DotNet.MSBuildSdkResolver/MSBuildSdkResolver.cs
@@ -3,13 +3,21 @@
using Microsoft.Build.Framework;
using System;
-using System.Collections.Generic;
+using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using System.Reflection;
namespace Microsoft.DotNet.MSBuildSdkResolver
{
+ // Thread-safety note:
+ // 1. MSBuild can call the same resolver instance in parallel on multiple threads.
+ // 2. Nevertheless, in the IDE, project re-evaluation can create new instances for each evaluation.
+ //
+ // As such, all state (instance or static) must be guarded against concurrent access/updates.
+ // Caches of minimum versions, compatible SDKs are static to benefit multiple IDE evaluations.
+ // VSSettings are also effectively static (singleton instance that can be swapped by tests).
+
public sealed class DotNetMSBuildSdkResolver : SdkResolver
{
public override string Name => "Microsoft.DotNet.MSBuildSdkResolver";
@@ -18,29 +26,59 @@ namespace Microsoft.DotNet.MSBuildSdkResolver
public override int Priority => 5000;
private readonly Func _getEnvironmentVariable;
+ private readonly VSSettings _vsSettings;
+
+ private static readonly ConcurrentDictionary s_minimumMSBuildVersions
+ = new ConcurrentDictionary();
+
+ private static readonly ConcurrentDictionary s_compatibleSdks
+ = new ConcurrentDictionary();
public DotNetMSBuildSdkResolver()
- : this(Environment.GetEnvironmentVariable)
+ : this(Environment.GetEnvironmentVariable, VSSettings.Ambient)
{
}
- // Test hook to provide environment variables without polluting the test process.
- internal DotNetMSBuildSdkResolver(Func getEnvironmentVariable)
+ // Test constructor
+ internal DotNetMSBuildSdkResolver(Func getEnvironmentVariable, VSSettings vsSettings)
{
_getEnvironmentVariable = getEnvironmentVariable;
+ _vsSettings = vsSettings;
+ }
+
+ private sealed class CachedResult
+ {
+ public string MSBuildSdksDir;
+ public string NETCoreSdkVersion;
}
public override SdkResult Resolve(SdkReference sdkReference, SdkResolverContext context, SdkResultFactory factory)
{
- // These are overrides that are used to force the resolved SDK tasks and targets to come from a given
- // base directory and report a given version to msbuild (which may be null if unknown. One key use case
- // for this is to test SDK tasks and targets without deploying them inside the .NET Core SDK.
- string msbuildSdksDir = _getEnvironmentVariable("DOTNET_MSBUILD_SDK_RESOLVER_SDKS_DIR");
- string netcoreSdkVersion = _getEnvironmentVariable("DOTNET_MSBUILD_SDK_RESOLVER_SDKS_VER");
+ string msbuildSdksDir = null;
+ string netcoreSdkVersion = null;
+
+ if (context.State is CachedResult priorResult)
+ {
+ msbuildSdksDir = priorResult.MSBuildSdksDir;
+ netcoreSdkVersion = priorResult.NETCoreSdkVersion;
+ }
if (msbuildSdksDir == null)
{
- string netcoreSdkDir = ResolveNetcoreSdkDirectory(context);
+ // These are overrides that are used to force the resolved SDK tasks and targets to come from a given
+ // base directory and report a given version to msbuild (which may be null if unknown. One key use case
+ // for this is to test SDK tasks and targets without deploying them inside the .NET Core SDK.
+ msbuildSdksDir = _getEnvironmentVariable("DOTNET_MSBUILD_SDK_RESOLVER_SDKS_DIR");
+ netcoreSdkVersion = _getEnvironmentVariable("DOTNET_MSBUILD_SDK_RESOLVER_SDKS_VER");
+ }
+
+ if (msbuildSdksDir == null)
+ {
+ string dotnetExeDir = GetDotnetExeDirectory();
+ var resolverResult = ResolveNETCoreSdkDirectory(context, dotnetExeDir);
+ string netcoreSdkDir = resolverResult.ResolvedSdkDirectory;
+ string globalJsonPath = resolverResult.GlobalJsonPath;
+
if (netcoreSdkDir == null)
{
return factory.IndicateFailure(
@@ -66,21 +104,20 @@ namespace Microsoft.DotNet.MSBuildSdkResolver
});
}
- string minimumMSBuildVersionString = GetMinimumMSBuildVersion(netcoreSdkDir);
- var minimumMSBuildVersion = Version.Parse(minimumMSBuildVersionString);
+ Version minimumMSBuildVersion = GetMinimumMSBuildVersion(netcoreSdkDir);
if (context.MSBuildVersion < minimumMSBuildVersion)
{
return factory.IndicateFailure(
new[]
{
- $"Version {netcoreSdkVersion} of the .NET Core SDK requires at least version {minimumMSBuildVersionString}"
+ $"Version {netcoreSdkVersion} of the .NET Core SDK requires at least version {minimumMSBuildVersion.ToString()}"
+ $" of MSBuild. The current available version of MSBuild is {context.MSBuildVersion.ToString()}."
+ " Change the .NET Core SDK specified in global.json to an older version that requires the MSBuild"
+ " version currently available."
});
}
- string minimumVSDefinedSDKVersion = GetMinimumVSDefinedSDKVersion();
+ string minimumVSDefinedSDKVersion = GetMinimumVSDefinedSDKVersion();
if (IsNetCoreSDKSmallerThanTheMinimumVersion(netcoreSdkVersion, minimumVSDefinedSDKVersion))
{
return factory.IndicateFailure(
@@ -93,6 +130,12 @@ namespace Microsoft.DotNet.MSBuildSdkResolver
}
}
+ context.State = new CachedResult
+ {
+ MSBuildSdksDir = msbuildSdksDir,
+ NETCoreSdkVersion = netcoreSdkVersion
+ };
+
string msbuildSdkDir = Path.Combine(msbuildSdksDir, sdkReference.Name, "Sdk");
if (!Directory.Exists(msbuildSdkDir))
{
@@ -107,18 +150,114 @@ namespace Microsoft.DotNet.MSBuildSdkResolver
return factory.IndicateSuccess(msbuildSdkDir, netcoreSdkVersion);
}
- private static string GetMinimumMSBuildVersion(string netcoreSdkDir)
+ private sealed class CompatibleSdkKey : IEquatable
{
- string minimumVersionFilePath = Path.Combine(netcoreSdkDir, "minimumMSBuildVersion");
- if (!File.Exists(minimumVersionFilePath))
+ public readonly string DotnetExeDirectory;
+ public readonly Version MSBuildVersion;
+
+ public CompatibleSdkKey(string dotnetExeDirectory, Version msbuildVersion)
{
- // smallest version that had resolver support and also
- // greater than or equal to the version required by any
- // .NET Core SDK that did not have this file.
- return "15.3.0";
+ DotnetExeDirectory = dotnetExeDirectory;
+ MSBuildVersion = msbuildVersion;
}
- return File.ReadLines(minimumVersionFilePath).First().Trim();
+ public bool Equals(CompatibleSdkKey other)
+ {
+ return other != null
+ && DotnetExeDirectory == other.DotnetExeDirectory
+ && MSBuildVersion == other.MSBuildVersion;
+ }
+
+ public override bool Equals(object obj)
+ {
+ return Equals(obj as CompatibleSdkValue);
+ }
+
+ public override int GetHashCode()
+ {
+ int h1 = DotnetExeDirectory.GetHashCode();
+ int h2 = MSBuildVersion.GetHashCode();
+ return ((h1 << 5) + h1) ^ h2;
+ }
+ }
+
+ private sealed class CompatibleSdkValue
+ {
+ public readonly string MostRecentCompatible;
+ public readonly string MostRecentCompatibleNonPreview;
+
+ public CompatibleSdkValue(string mostRecentCompatible, string mostRecentCompatibleNonPreview)
+ {
+ MostRecentCompatible = mostRecentCompatible;
+ MostRecentCompatibleNonPreview = mostRecentCompatibleNonPreview;
+ }
+ }
+
+ private string GetMostCompatibleSdk(string dotnetExeDirectory, Version msbuildVersion)
+ {
+ CompatibleSdkValue sdks = GetMostCompatibleSdks(dotnetExeDirectory, msbuildVersion);
+ if (_vsSettings.DisallowPrerelease())
+ {
+ return sdks.MostRecentCompatibleNonPreview;
+ }
+
+ return sdks.MostRecentCompatible;
+ }
+
+ private CompatibleSdkValue GetMostCompatibleSdks(string dotnetExeDirectory, Version msbuildVersion)
+ {
+ return s_compatibleSdks.GetOrAdd(
+ new CompatibleSdkKey(dotnetExeDirectory, msbuildVersion),
+ key =>
+ {
+ string mostRecent = null;
+ string mostRecentNonPreview = null;
+
+ string[] availableSdks = NETCoreSdkResolver.GetAvailableSdks(key.DotnetExeDirectory);
+ for (int i = availableSdks.Length - 1; i >= 0; i--)
+ {
+ string netcoreSdkDir = availableSdks[i];
+ string netcoreSdkVersion = new DirectoryInfo(netcoreSdkDir).Name;
+ Version minimumMSBuildVersion = GetMinimumMSBuildVersion(netcoreSdkDir);
+
+ if (key.MSBuildVersion < minimumMSBuildVersion)
+ {
+ continue;
+ }
+
+ if (mostRecent == null)
+ {
+ mostRecent = netcoreSdkDir;
+ }
+
+ if (netcoreSdkVersion.IndexOf('-') < 0)
+ {
+ mostRecentNonPreview = netcoreSdkDir;
+ break;
+ }
+ }
+
+ return new CompatibleSdkValue(mostRecent, mostRecentNonPreview);
+ });
+ }
+
+ private Version GetMinimumMSBuildVersion(string netcoreSdkDir)
+ {
+ return s_minimumMSBuildVersions.GetOrAdd(
+ netcoreSdkDir,
+ dir =>
+ {
+ string minimumVersionFilePath = Path.Combine(netcoreSdkDir, "minimumMSBuildVersion");
+ if (!File.Exists(minimumVersionFilePath))
+ {
+ // smallest version that had resolver support and also
+ // greater than or equal to the version required by any
+ // .NET Core SDK that did not have this file.
+ return new Version(15, 3, 0);
+ }
+
+ return Version.Parse(File.ReadLines(minimumVersionFilePath).First().Trim());
+ });
}
private static string GetMinimumVSDefinedSDKVersion()
@@ -156,13 +295,24 @@ namespace Microsoft.DotNet.MSBuildSdkResolver
return FXVersion.Compare(netCoreSdkFXVersion, minimumFXVersion) < 0;
}
- private string ResolveNetcoreSdkDirectory(SdkResolverContext context)
+ private NETCoreSdkResolver.Result ResolveNETCoreSdkDirectory(SdkResolverContext context, string dotnetExeDir)
{
- string exeDir = GetDotnetExeDirectory();
- string workingDir = context.SolutionFilePath ?? context.ProjectFilePath;
- string netcoreSdkDir = Interop.hostfxr_resolve_sdk(exeDir, workingDir);
+ string globalJsonStartDir = Path.GetDirectoryName(context.SolutionFilePath ?? context.ProjectFilePath);
+ var result = NETCoreSdkResolver.ResolveSdk(dotnetExeDir, globalJsonStartDir, _vsSettings.DisallowPrerelease());
- return netcoreSdkDir;
+ if (result.ResolvedSdkDirectory != null
+ && result.GlobalJsonPath == null
+ && context.MSBuildVersion < GetMinimumMSBuildVersion(result.ResolvedSdkDirectory))
+ {
+ string mostCompatible = GetMostCompatibleSdk(dotnetExeDir, context.MSBuildVersion);
+
+ if (mostCompatible != null)
+ {
+ result.ResolvedSdkDirectory = mostCompatible;
+ }
+ }
+
+ return result;
}
private string GetDotnetExeDirectory()
@@ -176,14 +326,12 @@ namespace Microsoft.DotNet.MSBuildSdkResolver
var environmentProvider = new EnvironmentProvider(_getEnvironmentVariable);
var dotnetExe = environmentProvider.GetCommandPath("dotnet");
-#if NETSTANDARD2_0
if (dotnetExe != null && !Interop.RunningOnWindows)
{
// e.g. on Linux the 'dotnet' command from PATH is a symlink so we need to
// resolve it to get the actual path to the binary
- dotnetExe = Interop.realpath(dotnetExe) ?? dotnetExe;
+ dotnetExe = Interop.Unix.realpath(dotnetExe) ?? dotnetExe;
}
-#endif
return Path.GetDirectoryName(dotnetExe);
}
diff --git a/src/Microsoft.DotNet.MSBuildSdkResolver/Microsoft.DotNet.MSBuildSdkResolver.csproj b/src/Microsoft.DotNet.MSBuildSdkResolver/Microsoft.DotNet.MSBuildSdkResolver.csproj
index 79c1f4797..894771928 100644
--- a/src/Microsoft.DotNet.MSBuildSdkResolver/Microsoft.DotNet.MSBuildSdkResolver.csproj
+++ b/src/Microsoft.DotNet.MSBuildSdkResolver/Microsoft.DotNet.MSBuildSdkResolver.csproj
@@ -16,7 +16,7 @@
-
+
@@ -26,6 +26,7 @@
+
@@ -40,4 +41,12 @@
+
+
+
+
+ true
+
+
+
diff --git a/src/Microsoft.DotNet.MSBuildSdkResolver/Microsoft.DotNet.MSBuildSdkResolver.sln b/src/Microsoft.DotNet.MSBuildSdkResolver/Microsoft.DotNet.MSBuildSdkResolver.sln
index 08c449079..21c171b2e 100644
--- a/src/Microsoft.DotNet.MSBuildSdkResolver/Microsoft.DotNet.MSBuildSdkResolver.sln
+++ b/src/Microsoft.DotNet.MSBuildSdkResolver/Microsoft.DotNet.MSBuildSdkResolver.sln
@@ -9,14 +9,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.MSBuildSdk
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.Tools.Tests.Utilities", "..\..\test\Microsoft.DotNet.Tools.Tests.Utilities\Microsoft.DotNet.Tools.Tests.Utilities.csproj", "{E548D3D0-50E3-4485-B531-95585A5D0B85}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.TestFramework", "..\Microsoft.DotNet.TestFramework\Microsoft.DotNet.TestFramework.csproj", "{182FFFA6-AE8F-431C-9B17-2F30B2A8FE42}"
-EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TestDependencies", "TestDependencies", "{0F45009E-9053-401D-91CA-8046D9EB310B}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.DotNet.Cli.Utils", "..\Microsoft.DotNet.Cli.Utils\Microsoft.DotNet.Cli.Utils.csproj", "{7F68DEFE-F2D3-453C-B155-51B674604D29}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.Cli.Utils", "..\Microsoft.DotNet.Cli.Utils\Microsoft.DotNet.Cli.Utils.csproj", "{7F68DEFE-F2D3-453C-B155-51B674604D29}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.InternalAbstractions", "..\Microsoft.DotNet.InternalAbstractions\Microsoft.DotNet.InternalAbstractions.csproj", "{A54567A1-E8DE-4B8C-9156-D895B9D016DB}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.TestFramework", "..\..\test\Microsoft.DotNet.TestFramework\Microsoft.DotNet.TestFramework.csproj", "{47C99775-27DF-4452-A1A3-2182FFA19BF2}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -35,10 +35,6 @@ Global
{E548D3D0-50E3-4485-B531-95585A5D0B85}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E548D3D0-50E3-4485-B531-95585A5D0B85}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E548D3D0-50E3-4485-B531-95585A5D0B85}.Release|Any CPU.Build.0 = Release|Any CPU
- {182FFFA6-AE8F-431C-9B17-2F30B2A8FE42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {182FFFA6-AE8F-431C-9B17-2F30B2A8FE42}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {182FFFA6-AE8F-431C-9B17-2F30B2A8FE42}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {182FFFA6-AE8F-431C-9B17-2F30B2A8FE42}.Release|Any CPU.Build.0 = Release|Any CPU
{7F68DEFE-F2D3-453C-B155-51B674604D29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7F68DEFE-F2D3-453C-B155-51B674604D29}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7F68DEFE-F2D3-453C-B155-51B674604D29}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -47,14 +43,21 @@ Global
{A54567A1-E8DE-4B8C-9156-D895B9D016DB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A54567A1-E8DE-4B8C-9156-D895B9D016DB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A54567A1-E8DE-4B8C-9156-D895B9D016DB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {47C99775-27DF-4452-A1A3-2182FFA19BF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {47C99775-27DF-4452-A1A3-2182FFA19BF2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {47C99775-27DF-4452-A1A3-2182FFA19BF2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {47C99775-27DF-4452-A1A3-2182FFA19BF2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{E548D3D0-50E3-4485-B531-95585A5D0B85} = {0F45009E-9053-401D-91CA-8046D9EB310B}
- {182FFFA6-AE8F-431C-9B17-2F30B2A8FE42} = {0F45009E-9053-401D-91CA-8046D9EB310B}
{7F68DEFE-F2D3-453C-B155-51B674604D29} = {0F45009E-9053-401D-91CA-8046D9EB310B}
{A54567A1-E8DE-4B8C-9156-D895B9D016DB} = {0F45009E-9053-401D-91CA-8046D9EB310B}
+ {47C99775-27DF-4452-A1A3-2182FFA19BF2} = {0F45009E-9053-401D-91CA-8046D9EB310B}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {DAE5F0E8-9018-4F6B-BB7F-541CA5E0E98B}
EndGlobalSection
EndGlobal
diff --git a/src/Microsoft.DotNet.MSBuildSdkResolver/NETCoreSdkResolver.cs b/src/Microsoft.DotNet.MSBuildSdkResolver/NETCoreSdkResolver.cs
new file mode 100644
index 000000000..e00c618d9
--- /dev/null
+++ b/src/Microsoft.DotNet.MSBuildSdkResolver/NETCoreSdkResolver.cs
@@ -0,0 +1,76 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Diagnostics;
+
+namespace Microsoft.DotNet.MSBuildSdkResolver
+{
+ internal static class NETCoreSdkResolver
+ {
+ public sealed class Result
+ {
+ ///
+ /// Path to .NET Core SDK selected by hostfxr (e.g. C:\Program Files\dotnet\sdk\2.1.300).
+ ///
+ public string ResolvedSdkDirectory;
+
+ ///
+ /// Path to global.json file that impacted resolution.
+ ///
+ public string GlobalJsonPath;
+
+ public void Initialize(Interop.hostfxr_resolve_sdk2_result_key_t key, string value)
+ {
+ switch (key)
+ {
+ case Interop.hostfxr_resolve_sdk2_result_key_t.resolved_sdk_dir:
+ ResolvedSdkDirectory = value;
+ break;
+ case Interop.hostfxr_resolve_sdk2_result_key_t.global_json_path:
+ GlobalJsonPath = value;
+ break;
+ }
+ }
+ }
+
+ public static Result ResolveSdk(
+ string dotnetExeDirectory,
+ string globalJsonStartDirectory,
+ bool disallowPrerelease = false)
+ {
+ var result = new Result();
+ var flags = disallowPrerelease ? Interop.hostfxr_resolve_sdk2_flags_t.disallow_prerelease : 0;
+
+ int errorCode = Interop.RunningOnWindows
+ ? Interop.Windows.hostfxr_resolve_sdk2(dotnetExeDirectory, globalJsonStartDirectory, flags, result.Initialize)
+ : Interop.Unix.hostfxr_resolve_sdk2(dotnetExeDirectory, globalJsonStartDirectory, flags, result.Initialize);
+
+ Debug.Assert((errorCode == 0) == (result.ResolvedSdkDirectory != null));
+ return result;
+ }
+
+ private sealed class SdkList
+ {
+ public string[] Entries;
+
+ public void Initialize(int count, string[] entries)
+ {
+ entries = entries ?? Array.Empty();
+ Debug.Assert(count == entries.Length);
+ Entries = entries;
+ }
+ }
+
+ public static string[] GetAvailableSdks(string dotnetExeDirectory)
+ {
+ var list = new SdkList();
+
+ int errorCode = Interop.RunningOnWindows
+ ? Interop.Windows.hostfxr_get_available_sdks(dotnetExeDirectory, list.Initialize)
+ : Interop.Unix.hostfxr_get_available_sdks(dotnetExeDirectory, list.Initialize);
+
+ return list.Entries;
+ }
+ }
+}
diff --git a/src/Microsoft.DotNet.MSBuildSdkResolver/VSSettings.cs b/src/Microsoft.DotNet.MSBuildSdkResolver/VSSettings.cs
new file mode 100644
index 000000000..46dfda2e6
--- /dev/null
+++ b/src/Microsoft.DotNet.MSBuildSdkResolver/VSSettings.cs
@@ -0,0 +1,155 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Runtime.InteropServices;
+
+#if NET46
+using Microsoft.VisualStudio.Setup.Configuration;
+#endif
+
+namespace Microsoft.DotNet.MSBuildSdkResolver
+{
+ internal sealed class VSSettings
+ {
+ private readonly object _lock = new object();
+ private readonly string _settingsFilePath;
+ private readonly bool _disallowPrereleaseByDefault;
+ private FileInfo _settingsFile;
+ private bool _disallowPrerelease;
+
+ // In the product, this singleton is used. It must be safe to use in parallel on multiple threads.
+ // In tests, mock instances can be created with the test constructor below.
+ public static readonly VSSettings Ambient = new VSSettings();
+
+ private VSSettings()
+ {
+#if NET46
+ if (!Interop.RunningOnWindows)
+ {
+ return;
+ }
+
+ string instanceId;
+ string installationVersion;
+ bool isPrerelease;
+
+ try
+ {
+ var configuration = new SetupConfiguration();
+ var instance = configuration.GetInstanceForCurrentProcess();
+
+ instanceId = instance.GetInstanceId();
+ installationVersion = instance.GetInstallationVersion();
+ isPrerelease = ((ISetupInstanceCatalog)instance).IsPrerelease();
+ }
+ catch (COMException)
+ {
+ return;
+ }
+
+ var version = Version.Parse(installationVersion);
+
+ _settingsFilePath = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+ "Microsoft",
+ "VisualStudio",
+ version.Major + ".0_" + instanceId,
+ "sdk.txt");
+
+ _disallowPrereleaseByDefault = !isPrerelease;
+ _disallowPrerelease = _disallowPrereleaseByDefault;
+#endif
+ }
+
+ // Test constructor
+ public VSSettings(string settingsFilePath, bool disallowPrereleaseByDefault)
+ {
+ _settingsFilePath = settingsFilePath;
+ _disallowPrereleaseByDefault = disallowPrereleaseByDefault;
+ _disallowPrerelease = _disallowPrereleaseByDefault;
+ }
+
+ public bool DisallowPrerelease()
+ {
+ if (_settingsFilePath != null)
+ {
+ Refresh();
+ }
+
+ return _disallowPrerelease;
+ }
+
+ private void Refresh()
+ {
+ Debug.Assert(_settingsFilePath != null);
+
+ var file = new FileInfo(_settingsFilePath);
+
+ // NB: All calls to Exists and LastWriteTimeUtc below will not hit the disk
+ // They will return data obtained during Refresh() here.
+ file.Refresh();
+
+ lock (_lock)
+ {
+ // File does not exist -> use default.
+ if (!file.Exists)
+ {
+ _disallowPrerelease = _disallowPrereleaseByDefault;
+ _settingsFile = file;
+ return;
+ }
+
+ // File has not changed -> reuse prior read.
+ if (_settingsFile?.Exists == true && file.LastWriteTimeUtc <= _settingsFile.LastWriteTimeUtc)
+ {
+ return;
+ }
+
+ // File has changed -> read from disk
+ // If we encounter an I/O exception, assume writer is in the process of updating file,
+ // ignore the exception, and use stale settings until the next resolution.
+ try
+ {
+ ReadFromDisk();
+ _settingsFile = file;
+ return;
+ }
+ catch (IOException) { }
+ catch (UnauthorizedAccessException) { }
+ }
+ }
+
+ private void ReadFromDisk()
+ {
+ using (var reader = new StreamReader(_settingsFilePath))
+ {
+ string line;
+ while ((line = reader.ReadLine()) != null)
+ {
+ int indexOfEquals = line.IndexOf('=');
+ if (indexOfEquals < 0 || indexOfEquals == (line.Length - 1))
+ {
+ continue;
+ }
+
+ string key = line.Substring(0, indexOfEquals).Trim();
+ string value = line.Substring(indexOfEquals + 1).Trim();
+
+ if (key.Equals("UsePreviews", StringComparison.OrdinalIgnoreCase)
+ && bool.TryParse(value, out bool usePreviews))
+ {
+ _disallowPrerelease = !usePreviews;
+ return;
+ }
+ }
+ }
+
+ // File does not have UsePreviews entry -> use default
+ _disallowPrerelease = _disallowPrereleaseByDefault;
+ }
+ }
+}
+
diff --git a/test/Microsoft.DotNet.MSBuildSdkResolver.Tests/GivenAnMSBuildSdkResolver.cs b/test/Microsoft.DotNet.MSBuildSdkResolver.Tests/GivenAnMSBuildSdkResolver.cs
index bea3dde7e..55cac8275 100644
--- a/test/Microsoft.DotNet.MSBuildSdkResolver.Tests/GivenAnMSBuildSdkResolver.cs
+++ b/test/Microsoft.DotNet.MSBuildSdkResolver.Tests/GivenAnMSBuildSdkResolver.cs
@@ -48,7 +48,7 @@ namespace Microsoft.DotNet.Cli.Utils.Tests
var resolver = environment.CreateResolver();
var result = (MockResult)resolver.Resolve(
new SdkReference("Some.Test.Sdk", null, null),
- new MockContext { ProjectFilePath = environment.TestDirectory.FullName },
+ new MockContext { ProjectFileDirectory = environment.TestDirectory },
new MockFactory());
result.Success.Should().BeTrue();
@@ -68,7 +68,7 @@ namespace Microsoft.DotNet.Cli.Utils.Tests
var resolver = environment.CreateResolver();
var result = (MockResult)resolver.Resolve(
new SdkReference("Some.Test.Sdk", null, "999.99.99"),
- new MockContext { ProjectFilePath = environment.TestDirectory.FullName },
+ new MockContext { ProjectFileDirectory = environment.TestDirectory },
new MockFactory());
result.Success.Should().BeFalse();
@@ -81,7 +81,7 @@ namespace Microsoft.DotNet.Cli.Utils.Tests
}
[Fact]
- public void ItReturnsNullWhenTheSDKRequiresAHigherVersionOfMSBuildThanTheOneAvailable()
+ public void ItReturnsNullWhenTheSDKRequiresAHigherVersionOfMSBuildThanAnyOneAvailable()
{
var environment = new TestEnvironment();
var expected =
@@ -94,7 +94,7 @@ namespace Microsoft.DotNet.Cli.Utils.Tests
new MockContext
{
MSBuildVersion = new Version(1, 0),
- ProjectFilePath = environment.TestDirectory.FullName
+ ProjectFileDirectory = environment.TestDirectory
},
new MockFactory());
@@ -107,6 +107,73 @@ namespace Microsoft.DotNet.Cli.Utils.Tests
+ " version that requires the MSBuild version currently available.");
}
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ItReturnsHighestSdkAvailableThatIsCompatibleWithMSBuild(bool disallowPreviews)
+ {
+ var environment = new TestEnvironment(identifier: disallowPreviews.ToString())
+ {
+ DisallowPrereleaseByDefault = disallowPreviews
+ };
+
+ var compatibleRtm = environment.CreateSdkDirectory(ProgramFiles.X64, "Some.Test.Sdk", "98.98.98", new Version(19, 0, 0, 0));
+ var compatiblePreview = environment.CreateSdkDirectory(ProgramFiles.X64, "Some.Test.Sdk", "99.99.99-preview", new Version(20, 0, 0, 0));
+ var incompatible = environment.CreateSdkDirectory(ProgramFiles.X64, "Some.Test.Sdk", "100.100.100", new Version(21, 0, 0, 0));
+
+ environment.CreateMuxerAndAddToPath(ProgramFiles.X64);
+
+ var resolver = environment.CreateResolver();
+ var result = (MockResult)resolver.Resolve(
+ new SdkReference("Some.Test.Sdk", null, null),
+ new MockContext
+ {
+ MSBuildVersion = new Version(20, 0, 0, 0),
+ ProjectFileDirectory = environment.TestDirectory,
+ },
+ new MockFactory());
+
+ result.Success.Should().BeTrue();
+ result.Path.Should().Be((disallowPreviews ? compatibleRtm : compatiblePreview).FullName);
+ result.Version.Should().Be(disallowPreviews ? "98.98.98" : "99.99.99-preview");
+ result.Warnings.Should().BeNullOrEmpty();
+ result.Errors.Should().BeNullOrEmpty();
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ItDoesNotReturnHighestSdkAvailableThatIsCompatibleWithMSBuildWhenVersionInGlobalJsonCannotBeFound(bool disallowPreviews)
+ {
+ var environment = new TestEnvironment(callingMethod: "ItDoesNotReturnHighest___", identifier: disallowPreviews.ToString())
+ {
+ DisallowPrereleaseByDefault = disallowPreviews
+ };
+
+ var compatibleRtm = environment.CreateSdkDirectory(ProgramFiles.X64, "Some.Test.Sdk", "98.98.98", new Version(19, 0, 0, 0));
+ var compatiblePreview = environment.CreateSdkDirectory(ProgramFiles.X64, "Some.Test.Sdk", "99.99.99-preview", new Version(20, 0, 0, 0));
+ var incompatible = environment.CreateSdkDirectory(ProgramFiles.X64, "Some.Test.Sdk", "100.100.100", new Version(21, 0, 0, 0));
+
+ environment.CreateMuxerAndAddToPath(ProgramFiles.X64);
+ environment.CreateGlobalJson(environment.TestDirectory, "1.2.3");
+
+ var resolver = environment.CreateResolver();
+ var result = (MockResult)resolver.Resolve(
+ new SdkReference("Some.Test.Sdk", null, null),
+ new MockContext
+ {
+ MSBuildVersion = new Version(20, 0, 0, 0),
+ ProjectFileDirectory = environment.TestDirectory,
+ },
+ new MockFactory());
+
+ result.Success.Should().BeFalse();
+ result.Path.Should().BeNull();
+ result.Version.Should().BeNull();;
+ result.Warnings.Should().BeNullOrEmpty();
+ result.Errors.Should().NotBeEmpty();
+ }
+
[Fact]
public void ItReturnsNullWhenTheDefaultVSRequiredSDKVersionIsHigherThanTheSDKVersionAvailable()
{
@@ -118,7 +185,7 @@ namespace Microsoft.DotNet.Cli.Utils.Tests
var resolver = environment.CreateResolver();
var result = (MockResult)resolver.Resolve(
new SdkReference("Some.Test.Sdk", null, "1.0.0"),
- new MockContext { ProjectFilePath = environment.TestDirectory.FullName },
+ new MockContext { ProjectFileDirectory = environment.TestDirectory },
new MockFactory());
result.Success.Should().BeFalse();
@@ -142,7 +209,7 @@ namespace Microsoft.DotNet.Cli.Utils.Tests
var resolver = environment.CreateResolver();
var result = (MockResult)resolver.Resolve(
new SdkReference("Some.Test.Sdk", null, "1.0.0"),
- new MockContext { ProjectFilePath = environment.TestDirectory.FullName },
+ new MockContext { ProjectFileDirectory = environment.TestDirectory },
new MockFactory());
result.Success.Should().BeFalse();
@@ -165,7 +232,7 @@ namespace Microsoft.DotNet.Cli.Utils.Tests
var resolver = environment.CreateResolver();
var result = (MockResult)resolver.Resolve(
new SdkReference("Some.Test.Sdk", null, "99.99.99"),
- new MockContext { ProjectFilePath = environment.TestDirectory.FullName },
+ new MockContext { ProjectFileDirectory = environment.TestDirectory },
new MockFactory());
result.Success.Should().BeTrue();
@@ -186,7 +253,7 @@ namespace Microsoft.DotNet.Cli.Utils.Tests
var resolver = environment.CreateResolver();
var result = (MockResult)resolver.Resolve(
new SdkReference("Some.Test.Sdk", null, "99.99.99"),
- new MockContext { ProjectFilePath = environment.TestDirectory.FullName },
+ new MockContext { ProjectFileDirectory = environment.TestDirectory },
new MockFactory());
result.Success.Should().BeTrue();
@@ -196,6 +263,159 @@ namespace Microsoft.DotNet.Cli.Utils.Tests
result.Errors.Should().BeNullOrEmpty();
}
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ItDisallowsPreviewsBasedOnDefault(bool disallowPreviewsByDefault)
+ {
+ var environment = new TestEnvironment(identifier: disallowPreviewsByDefault.ToString());
+ var rtm = environment.CreateSdkDirectory(ProgramFiles.X64, "Some.Test.Sdk", "10.0.0");
+ var preview = environment.CreateSdkDirectory(ProgramFiles.X64, "Some.Test.Sdk", "11.0.0-preview1");
+ var expected = disallowPreviewsByDefault ? rtm : preview;
+
+ environment.CreateMuxerAndAddToPath(ProgramFiles.X64);
+ environment.DisallowPrereleaseByDefault = disallowPreviewsByDefault;
+
+ var resolver = environment.CreateResolver();
+ var result = (MockResult)resolver.Resolve(
+ new SdkReference("Some.Test.Sdk", null, null),
+ new MockContext { ProjectFileDirectory = environment.TestDirectory },
+ new MockFactory());
+
+ result.Success.Should().BeTrue();
+ result.Path.Should().Be(expected.FullName);
+ result.Version.Should().Be(disallowPreviewsByDefault ? "10.0.0" : "11.0.0-preview1");
+ result.Warnings.Should().BeNullOrEmpty();
+ result.Errors.Should().BeNullOrEmpty();
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ItDisallowsPreviewsBasedOnFile(bool disallowPreviews)
+ {
+ var environment = new TestEnvironment(identifier: disallowPreviews.ToString());
+ var rtm = environment.CreateSdkDirectory(ProgramFiles.X64, "Some.Test.Sdk", "10.0.0");
+ var preview = environment.CreateSdkDirectory(ProgramFiles.X64, "Some.Test.Sdk", "11.0.0-preview1");
+ var expected = disallowPreviews ? rtm : preview;
+
+ environment.CreateMuxerAndAddToPath(ProgramFiles.X64);
+ environment.DisallowPrereleaseByDefault = !disallowPreviews;
+ environment.CreateVSSettingsFile(disallowPreviews);
+
+ var resolver = environment.CreateResolver();
+ var result = (MockResult)resolver.Resolve(
+ new SdkReference("Some.Test.Sdk", null, null),
+ new MockContext { ProjectFileDirectory = environment.TestDirectory },
+ new MockFactory());
+
+ result.Success.Should().BeTrue();
+ result.Path.Should().Be(expected.FullName);
+ result.Version.Should().Be(disallowPreviews ? "10.0.0" : "11.0.0-preview1");
+ result.Warnings.Should().BeNullOrEmpty();
+ result.Errors.Should().BeNullOrEmpty();
+ }
+
+ [Fact]
+ public void ItObservesChangesToVSSettingsFile()
+ {
+ var environment = new TestEnvironment();
+ var rtm = environment.CreateSdkDirectory(ProgramFiles.X64, "Some.Test.Sdk", "10.0.0");
+ var preview = environment.CreateSdkDirectory(ProgramFiles.X64, "Some.Test.Sdk", "11.0.0-preview1");
+
+ environment.CreateMuxerAndAddToPath(ProgramFiles.X64);
+ environment.CreateVSSettingsFile(disallowPreviews: true);
+ var resolver = environment.CreateResolver();
+
+ void Check(bool disallowPreviews, string message)
+ {
+ // check twice because file-up-to-date is a separate code path
+ for (int i = 0; i < 2; i++)
+ {
+
+ var result = (MockResult)resolver.Resolve(
+ new SdkReference("Some.Test.Sdk", null, null),
+ new MockContext { ProjectFileDirectory = environment.TestDirectory },
+ new MockFactory());
+
+ string m = $"{message} ({i})";
+ var expected = disallowPreviews ? rtm : preview;
+ result.Success.Should().BeTrue(m);
+ result.Path.Should().Be(expected.FullName, m);
+ result.Version.Should().Be(disallowPreviews ? "10.0.0" : "11.0.0-preview1", m);
+ result.Warnings.Should().BeNullOrEmpty(m);
+ result.Errors.Should().BeNullOrEmpty(m);
+ }
+ }
+
+ environment.DeleteVSSettingsFile();
+ Check(disallowPreviews: false, message: "default with no file");
+
+ environment.CreateVSSettingsFile(disallowPreviews: true);
+ Check(disallowPreviews: true, message: "file changed to disallow previews");
+
+ environment.CreateVSSettingsFile(disallowPreviews: false);
+ Check(disallowPreviews: false, message: "file changed to not disallow previews");
+
+ environment.CreateVSSettingsFile(disallowPreviews: true);
+ Check(disallowPreviews: true, message: "file changed back to disallow previews");
+
+ environment.DeleteVSSettingsFile();
+ Check(disallowPreviews: false, message: "file deleted to return to default");
+ }
+
+ [Fact]
+ public void ItAllowsPreviewWhenGlobalJsonHasPreviewIrrespectiveOfSetting()
+ {
+ var environment = new TestEnvironment();
+ var rtm = environment.CreateSdkDirectory(ProgramFiles.X64, "Some.Test.Sdk", "10.0.0");
+ var preview = environment.CreateSdkDirectory(ProgramFiles.X64, "Some.Test.Sdk", "11.0.0-preview1");
+
+ environment.CreateMuxerAndAddToPath(ProgramFiles.X64);
+ environment.DisallowPrereleaseByDefault = true;
+ environment.CreateGlobalJson(environment.TestDirectory, "11.0.0-preview1");
+
+ var resolver = environment.CreateResolver();
+ var result = (MockResult)resolver.Resolve(
+ new SdkReference("Some.Test.Sdk", null, null),
+ new MockContext { ProjectFileDirectory = environment.TestDirectory },
+ new MockFactory());
+
+ result.Success.Should().BeTrue();
+ result.Path.Should().Be(preview.FullName);
+ result.Version.Should().Be("11.0.0-preview1");
+ result.Warnings.Should().BeNullOrEmpty();
+ result.Errors.Should().BeNullOrEmpty();
+ }
+
+ [Fact]
+ public void ItRespectsAmbientVSSettings()
+ {
+ // When run in test explorer in VS, this will actually locate the settings for the current VS instance
+ // based on location of testhost executable. This gives us some coverage threw that path but we cannot
+ // fix our expectations since the behavior will vary (by design) based on the current VS instance's settings.
+ var vsSettings = VSSettings.Ambient;
+
+ var environment = new TestEnvironment();
+ var rtm = environment.CreateSdkDirectory(ProgramFiles.X64, "Some.Test.Sdk", "10.0.0");
+ var preview = environment.CreateSdkDirectory(ProgramFiles.X64, "Some.Test.Sdk", "11.0.0-preview1");
+ var expected = vsSettings.DisallowPrerelease() ? rtm : preview;
+
+ environment.CreateMuxerAndAddToPath(ProgramFiles.X64);
+
+ var resolver = environment.CreateResolver(useAmbientSettings: true);
+ var result = (MockResult)resolver.Resolve(
+ new SdkReference("Some.Test.Sdk", null, null),
+ new MockContext { ProjectFileDirectory = environment.TestDirectory },
+ new MockFactory());
+
+ result.Success.Should().BeTrue();
+ result.Path.Should().Be(expected.FullName);
+ result.Version.Should().Be(vsSettings.DisallowPrerelease() ? "10.0.0" : "11.0.0-preview1");
+ result.Warnings.Should().BeNullOrEmpty();
+ result.Errors.Should().BeNullOrEmpty();
+ }
+
private enum ProgramFiles
{
X64,
@@ -210,6 +430,8 @@ namespace Microsoft.DotNet.Cli.Utils.Tests
public string PathEnvironmentVariable { get; set; }
public DirectoryInfo TestDirectory { get; }
+ public FileInfo VSSettingsFile { get; set; }
+ public bool DisallowPrereleaseByDefault { get; set; }
public TestEnvironment(string identifier = "", [CallerMemberName] string callingMethod = "")
{
@@ -223,8 +445,12 @@ namespace Microsoft.DotNet.Cli.Utils.Tests
PathEnvironmentVariable = string.Empty;
}
- public SdkResolver CreateResolver()
- => new DotNetMSBuildSdkResolver(GetEnvironmentVariable);
+ public SdkResolver CreateResolver(bool useAmbientSettings = false)
+ => new DotNetMSBuildSdkResolver(
+ GetEnvironmentVariable,
+ useAmbientSettings
+ ? VSSettings.Ambient
+ : new VSSettings(VSSettingsFile?.FullName, DisallowPrereleaseByDefault));
public DirectoryInfo GetSdkDirectory(ProgramFiles programFiles, string sdkName, string sdkVersion)
=> TestDirectory.GetDirectory(
@@ -317,6 +543,34 @@ namespace Microsoft.DotNet.Cli.Utils.Tests
string baseDirectory = AppContext.BaseDirectory;
return Path.Combine(baseDirectory, "minimumVSDefinedSDKVersion");
}
+
+ public void CreateVSSettingsFile(bool disallowPreviews)
+ {
+ VSSettingsFile = TestDirectory.GetFile("sdk.txt");
+
+ // Guard against tests writing too fast for the up-to-date check
+ // It happens more often on Unix due to https://github.com/dotnet/corefx/issues/12403
+ var lastWriteTimeUtc = VSSettingsFile.Exists ? VSSettingsFile.LastWriteTimeUtc : DateTime.MinValue;
+ for (int sleep = 10; sleep < 3000; sleep *= 2)
+ {
+ File.WriteAllText(VSSettingsFile.FullName, $"UsePreviews={!disallowPreviews}");
+ VSSettingsFile.Refresh();
+
+ if (VSSettingsFile.LastWriteTimeUtc > lastWriteTimeUtc)
+ {
+ return;
+ }
+
+ System.Threading.Thread.Sleep(sleep);
+ }
+
+ throw new InvalidOperationException("LastWriteTime is not changing.");
+ }
+
+ public void DeleteVSSettingsFile()
+ {
+ VSSettingsFile.Delete();
+ }
}
private sealed class MockContext : SdkResolverContext
@@ -325,6 +579,12 @@ namespace Microsoft.DotNet.Cli.Utils.Tests
public new string SolutionFilePath { get => base.SolutionFilePath; set => base.SolutionFilePath = value; }
public new Version MSBuildVersion { get => base.MSBuildVersion; set => base.MSBuildVersion = value; }
+ public DirectoryInfo ProjectFileDirectory
+ {
+ get => new DirectoryInfo(Path.GetDirectoryName(ProjectFilePath));
+ set => ProjectFilePath = value.GetFile("test.csproj").FullName;
+ }
+
public MockContext()
{
MSBuildVersion = new Version(15, 3, 0);
@@ -352,12 +612,7 @@ namespace Microsoft.DotNet.Cli.Utils.Tests
Errors = errors;
}
- public new bool Success
- {
- get => base.Success;
- private set => base.Success = value;
- }
-
+ public override bool Success { get; protected set; }
public override string Version { get; protected set; }
public override string Path { get; protected set; }
public IEnumerable Errors { get; }
diff --git a/test/Microsoft.DotNet.MSBuildSdkResolver.Tests/Microsoft.DotNet.MSBuildSdkResolver.Tests.csproj b/test/Microsoft.DotNet.MSBuildSdkResolver.Tests/Microsoft.DotNet.MSBuildSdkResolver.Tests.csproj
index b480a818f..1b0636beb 100644
--- a/test/Microsoft.DotNet.MSBuildSdkResolver.Tests/Microsoft.DotNet.MSBuildSdkResolver.Tests.csproj
+++ b/test/Microsoft.DotNet.MSBuildSdkResolver.Tests/Microsoft.DotNet.MSBuildSdkResolver.Tests.csproj
@@ -2,6 +2,14 @@
net461;$(CliTargetFramework)
$(CliTargetFramework)
+
+
+ net461
+
$(MicrosoftNETCoreAppPackageVersion)
Exe
../../tools/Key.snk
@@ -19,9 +27,9 @@
-
-
-
+
+
+
@@ -32,7 +40,10 @@
-
-
+
+
+