MSBuild SDK Resolver Improvements (#9547)

1. When there's no global.json, use latest SDK that is compatible with msbuild, not latest SDK overall
2. Respect VS setting to allow / disallow resolving to preview SDKs
This commit is contained in:
Nick Guerrera 2018-08-16 19:21:48 -07:00 committed by GitHub
parent 00e3b0b57c
commit 3ccc3593c8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 856 additions and 197 deletions

View file

@ -11,7 +11,10 @@
<DotnetUserSecretsPackageVersion>2.1.1</DotnetUserSecretsPackageVersion> <DotnetUserSecretsPackageVersion>2.1.1</DotnetUserSecretsPackageVersion>
<DotnetWatchPackageVersion>2.1.1</DotnetWatchPackageVersion> <DotnetWatchPackageVersion>2.1.1</DotnetWatchPackageVersion>
<MicrosoftNETCoreAppPackageVersion>2.1.3-servicing-26724-03</MicrosoftNETCoreAppPackageVersion> <MicrosoftNETCoreAppPackageVersion>2.1.3-servicing-26724-03</MicrosoftNETCoreAppPackageVersion>
<MicrosoftNETCoreDotNetHostResolverPackageVersion>$(MicrosoftNETCoreAppPackageVersion)</MicrosoftNETCoreDotNetHostResolverPackageVersion>
<!-- https://github.com/dotnet/cli/issues/9851 -->
<MicrosoftNETCoreDotNetHostResolverPackageVersion_ForMSBuildSdkResolverOnly>3.0.0-preview1-26816-04</MicrosoftNETCoreDotNetHostResolverPackageVersion_ForMSBuildSdkResolverOnly>
<MicrosoftBuildPackageVersion>15.9.0-preview-000006</MicrosoftBuildPackageVersion> <MicrosoftBuildPackageVersion>15.9.0-preview-000006</MicrosoftBuildPackageVersion>
<MicrosoftBuildFrameworkPackageVersion>$(MicrosoftBuildPackageVersion)</MicrosoftBuildFrameworkPackageVersion> <MicrosoftBuildFrameworkPackageVersion>$(MicrosoftBuildPackageVersion)</MicrosoftBuildFrameworkPackageVersion>
<MicrosoftBuildRuntimePackageVersion>$(MicrosoftBuildPackageVersion)</MicrosoftBuildRuntimePackageVersion> <MicrosoftBuildRuntimePackageVersion>$(MicrosoftBuildPackageVersion)</MicrosoftBuildRuntimePackageVersion>

View file

@ -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();
}
}
}

View file

@ -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

View file

@ -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

View file

@ -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;
}
}
}
}

View file

@ -3,13 +3,21 @@
using Microsoft.Build.Framework; using Microsoft.Build.Framework;
using System; using System;
using System.Collections.Generic; using System.Collections.Concurrent;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
namespace Microsoft.DotNet.MSBuildSdkResolver 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 sealed class DotNetMSBuildSdkResolver : SdkResolver
{ {
public override string Name => "Microsoft.DotNet.MSBuildSdkResolver"; public override string Name => "Microsoft.DotNet.MSBuildSdkResolver";
@ -18,29 +26,59 @@ namespace Microsoft.DotNet.MSBuildSdkResolver
public override int Priority => 5000; public override int Priority => 5000;
private readonly Func<string, string> _getEnvironmentVariable; private readonly Func<string, string> _getEnvironmentVariable;
private readonly VSSettings _vsSettings;
private static readonly ConcurrentDictionary<string, Version> s_minimumMSBuildVersions
= new ConcurrentDictionary<string, Version>();
private static readonly ConcurrentDictionary<CompatibleSdkKey, CompatibleSdkValue> s_compatibleSdks
= new ConcurrentDictionary<CompatibleSdkKey, CompatibleSdkValue>();
public DotNetMSBuildSdkResolver() public DotNetMSBuildSdkResolver()
: this(Environment.GetEnvironmentVariable) : this(Environment.GetEnvironmentVariable, VSSettings.Ambient)
{ {
} }
// Test hook to provide environment variables without polluting the test process. // Test constructor
internal DotNetMSBuildSdkResolver(Func<string, string> getEnvironmentVariable) internal DotNetMSBuildSdkResolver(Func<string, string> getEnvironmentVariable, VSSettings vsSettings)
{ {
_getEnvironmentVariable = getEnvironmentVariable; _getEnvironmentVariable = getEnvironmentVariable;
_vsSettings = vsSettings;
}
private sealed class CachedResult
{
public string MSBuildSdksDir;
public string NETCoreSdkVersion;
} }
public override SdkResult Resolve(SdkReference sdkReference, SdkResolverContext context, SdkResultFactory factory) 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 string msbuildSdksDir = null;
// base directory and report a given version to msbuild (which may be null if unknown. One key use case string netcoreSdkVersion = null;
// 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"); if (context.State is CachedResult priorResult)
string netcoreSdkVersion = _getEnvironmentVariable("DOTNET_MSBUILD_SDK_RESOLVER_SDKS_VER"); {
msbuildSdksDir = priorResult.MSBuildSdksDir;
netcoreSdkVersion = priorResult.NETCoreSdkVersion;
}
if (msbuildSdksDir == null) 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) if (netcoreSdkDir == null)
{ {
return factory.IndicateFailure( return factory.IndicateFailure(
@ -66,21 +104,20 @@ namespace Microsoft.DotNet.MSBuildSdkResolver
}); });
} }
string minimumMSBuildVersionString = GetMinimumMSBuildVersion(netcoreSdkDir); Version minimumMSBuildVersion = GetMinimumMSBuildVersion(netcoreSdkDir);
var minimumMSBuildVersion = Version.Parse(minimumMSBuildVersionString);
if (context.MSBuildVersion < minimumMSBuildVersion) if (context.MSBuildVersion < minimumMSBuildVersion)
{ {
return factory.IndicateFailure( return factory.IndicateFailure(
new[] 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()}." + $" 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" + " Change the .NET Core SDK specified in global.json to an older version that requires the MSBuild"
+ " version currently available." + " version currently available."
}); });
} }
string minimumVSDefinedSDKVersion = GetMinimumVSDefinedSDKVersion(); string minimumVSDefinedSDKVersion = GetMinimumVSDefinedSDKVersion();
if (IsNetCoreSDKSmallerThanTheMinimumVersion(netcoreSdkVersion, minimumVSDefinedSDKVersion)) if (IsNetCoreSDKSmallerThanTheMinimumVersion(netcoreSdkVersion, minimumVSDefinedSDKVersion))
{ {
return factory.IndicateFailure( 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"); string msbuildSdkDir = Path.Combine(msbuildSdksDir, sdkReference.Name, "Sdk");
if (!Directory.Exists(msbuildSdkDir)) if (!Directory.Exists(msbuildSdkDir))
{ {
@ -107,18 +150,114 @@ namespace Microsoft.DotNet.MSBuildSdkResolver
return factory.IndicateSuccess(msbuildSdkDir, netcoreSdkVersion); return factory.IndicateSuccess(msbuildSdkDir, netcoreSdkVersion);
} }
private static string GetMinimumMSBuildVersion(string netcoreSdkDir) private sealed class CompatibleSdkKey : IEquatable<CompatibleSdkKey>
{ {
string minimumVersionFilePath = Path.Combine(netcoreSdkDir, "minimumMSBuildVersion"); public readonly string DotnetExeDirectory;
if (!File.Exists(minimumVersionFilePath)) public readonly Version MSBuildVersion;
public CompatibleSdkKey(string dotnetExeDirectory, Version msbuildVersion)
{ {
// smallest version that had resolver support and also DotnetExeDirectory = dotnetExeDirectory;
// greater than or equal to the version required by any MSBuildVersion = msbuildVersion;
// .NET Core SDK that did not have this file.
return "15.3.0";
} }
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() private static string GetMinimumVSDefinedSDKVersion()
@ -156,13 +295,24 @@ namespace Microsoft.DotNet.MSBuildSdkResolver
return FXVersion.Compare(netCoreSdkFXVersion, minimumFXVersion) < 0; return FXVersion.Compare(netCoreSdkFXVersion, minimumFXVersion) < 0;
} }
private string ResolveNetcoreSdkDirectory(SdkResolverContext context) private NETCoreSdkResolver.Result ResolveNETCoreSdkDirectory(SdkResolverContext context, string dotnetExeDir)
{ {
string exeDir = GetDotnetExeDirectory(); string globalJsonStartDir = Path.GetDirectoryName(context.SolutionFilePath ?? context.ProjectFilePath);
string workingDir = context.SolutionFilePath ?? context.ProjectFilePath; var result = NETCoreSdkResolver.ResolveSdk(dotnetExeDir, globalJsonStartDir, _vsSettings.DisallowPrerelease());
string netcoreSdkDir = Interop.hostfxr_resolve_sdk(exeDir, workingDir);
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() private string GetDotnetExeDirectory()
@ -176,14 +326,12 @@ namespace Microsoft.DotNet.MSBuildSdkResolver
var environmentProvider = new EnvironmentProvider(_getEnvironmentVariable); var environmentProvider = new EnvironmentProvider(_getEnvironmentVariable);
var dotnetExe = environmentProvider.GetCommandPath("dotnet"); var dotnetExe = environmentProvider.GetCommandPath("dotnet");
#if NETSTANDARD2_0
if (dotnetExe != null && !Interop.RunningOnWindows) if (dotnetExe != null && !Interop.RunningOnWindows)
{ {
// e.g. on Linux the 'dotnet' command from PATH is a symlink so we need to // 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 // 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); return Path.GetDirectoryName(dotnetExe);
} }

View file

@ -16,7 +16,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Build.Framework" Version="$(MicrosoftBuildPackageVersion)" PrivateAssets="All" /> <PackageReference Include="Microsoft.Build.Framework" Version="$(MicrosoftBuildPackageVersion)" PrivateAssets="All" />
<PackageReference Include="Microsoft.NETCore.DotNetHostResolver" Version="$(MicrosoftNETCoreDotNetHostResolverPackageVersion)" PrivateAssets="All" /> <PackageReference Include="Microsoft.NETCore.DotNetHostResolver" Version="$(MicrosoftNETCoreDotNetHostResolverPackageVersion_ForMSBuildSdkResolverOnly)" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETStandard'"> <ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETStandard'">
@ -26,6 +26,7 @@
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'"> <ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'">
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
<PackageReference Include="Microsoft.VisualStudio.Setup.Configuration.Interop" Version="1.16.30" />
</ItemGroup> </ItemGroup>
<Target Name="ResolveHostfxrCopyLocalContent" Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'" DependsOnTargets="RunResolvePackageDependencies" BeforeTargets="AssignTargetPaths"> <Target Name="ResolveHostfxrCopyLocalContent" Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'" DependsOnTargets="RunResolvePackageDependencies" BeforeTargets="AssignTargetPaths">
@ -40,4 +41,12 @@
</Content> </Content>
</ItemGroup> </ItemGroup>
</Target> </Target>
<Target Name="LinkVSEmbeddableAssemblies" DependsOnTargets="ResolveReferences" AfterTargets="ResolveReferences">
<ItemGroup>
<ReferencePath Condition="'%(ReferencePath.FileName)' == 'Microsoft.VisualStudio.Setup.Configuration.Interop'">
<EmbedInteropTypes>true</EmbedInteropTypes>
</ReferencePath>
</ItemGroup>
</Target>
</Project> </Project>

View file

@ -9,14 +9,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.MSBuildSdk
EndProject 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}" 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 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}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TestDependencies", "TestDependencies", "{0F45009E-9053-401D-91CA-8046D9EB310B}"
EndProject 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 EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.InternalAbstractions", "..\Microsoft.DotNet.InternalAbstractions\Microsoft.DotNet.InternalAbstractions.csproj", "{A54567A1-E8DE-4B8C-9156-D895B9D016DB}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.InternalAbstractions", "..\Microsoft.DotNet.InternalAbstractions\Microsoft.DotNet.InternalAbstractions.csproj", "{A54567A1-E8DE-4B8C-9156-D895B9D016DB}"
EndProject 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{E548D3D0-50E3-4485-B531-95585A5D0B85}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{7F68DEFE-F2D3-453C-B155-51B674604D29}.Debug|Any CPU.Build.0 = 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 {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}.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.ActiveCfg = Release|Any CPU
{A54567A1-E8DE-4B8C-9156-D895B9D016DB}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
EndGlobalSection EndGlobalSection
GlobalSection(NestedProjects) = preSolution GlobalSection(NestedProjects) = preSolution
{E548D3D0-50E3-4485-B531-95585A5D0B85} = {0F45009E-9053-401D-91CA-8046D9EB310B} {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} {7F68DEFE-F2D3-453C-B155-51B674604D29} = {0F45009E-9053-401D-91CA-8046D9EB310B}
{A54567A1-E8DE-4B8C-9156-D895B9D016DB} = {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 EndGlobalSection
EndGlobal EndGlobal

View file

@ -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
{
/// <summary>
/// Path to .NET Core SDK selected by hostfxr (e.g. C:\Program Files\dotnet\sdk\2.1.300).
/// </summary>
public string ResolvedSdkDirectory;
/// <summary>
/// Path to global.json file that impacted resolution.
/// </summary>
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<string>();
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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -48,7 +48,7 @@ namespace Microsoft.DotNet.Cli.Utils.Tests
var resolver = environment.CreateResolver(); var resolver = environment.CreateResolver();
var result = (MockResult)resolver.Resolve( var result = (MockResult)resolver.Resolve(
new SdkReference("Some.Test.Sdk", null, null), new SdkReference("Some.Test.Sdk", null, null),
new MockContext { ProjectFilePath = environment.TestDirectory.FullName }, new MockContext { ProjectFileDirectory = environment.TestDirectory },
new MockFactory()); new MockFactory());
result.Success.Should().BeTrue(); result.Success.Should().BeTrue();
@ -68,7 +68,7 @@ namespace Microsoft.DotNet.Cli.Utils.Tests
var resolver = environment.CreateResolver(); var resolver = environment.CreateResolver();
var result = (MockResult)resolver.Resolve( var result = (MockResult)resolver.Resolve(
new SdkReference("Some.Test.Sdk", null, "999.99.99"), new SdkReference("Some.Test.Sdk", null, "999.99.99"),
new MockContext { ProjectFilePath = environment.TestDirectory.FullName }, new MockContext { ProjectFileDirectory = environment.TestDirectory },
new MockFactory()); new MockFactory());
result.Success.Should().BeFalse(); result.Success.Should().BeFalse();
@ -81,7 +81,7 @@ namespace Microsoft.DotNet.Cli.Utils.Tests
} }
[Fact] [Fact]
public void ItReturnsNullWhenTheSDKRequiresAHigherVersionOfMSBuildThanTheOneAvailable() public void ItReturnsNullWhenTheSDKRequiresAHigherVersionOfMSBuildThanAnyOneAvailable()
{ {
var environment = new TestEnvironment(); var environment = new TestEnvironment();
var expected = var expected =
@ -94,7 +94,7 @@ namespace Microsoft.DotNet.Cli.Utils.Tests
new MockContext new MockContext
{ {
MSBuildVersion = new Version(1, 0), MSBuildVersion = new Version(1, 0),
ProjectFilePath = environment.TestDirectory.FullName ProjectFileDirectory = environment.TestDirectory
}, },
new MockFactory()); new MockFactory());
@ -107,6 +107,73 @@ namespace Microsoft.DotNet.Cli.Utils.Tests
+ " version that requires the MSBuild version currently available."); + " 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] [Fact]
public void ItReturnsNullWhenTheDefaultVSRequiredSDKVersionIsHigherThanTheSDKVersionAvailable() public void ItReturnsNullWhenTheDefaultVSRequiredSDKVersionIsHigherThanTheSDKVersionAvailable()
{ {
@ -118,7 +185,7 @@ namespace Microsoft.DotNet.Cli.Utils.Tests
var resolver = environment.CreateResolver(); var resolver = environment.CreateResolver();
var result = (MockResult)resolver.Resolve( var result = (MockResult)resolver.Resolve(
new SdkReference("Some.Test.Sdk", null, "1.0.0"), new SdkReference("Some.Test.Sdk", null, "1.0.0"),
new MockContext { ProjectFilePath = environment.TestDirectory.FullName }, new MockContext { ProjectFileDirectory = environment.TestDirectory },
new MockFactory()); new MockFactory());
result.Success.Should().BeFalse(); result.Success.Should().BeFalse();
@ -142,7 +209,7 @@ namespace Microsoft.DotNet.Cli.Utils.Tests
var resolver = environment.CreateResolver(); var resolver = environment.CreateResolver();
var result = (MockResult)resolver.Resolve( var result = (MockResult)resolver.Resolve(
new SdkReference("Some.Test.Sdk", null, "1.0.0"), new SdkReference("Some.Test.Sdk", null, "1.0.0"),
new MockContext { ProjectFilePath = environment.TestDirectory.FullName }, new MockContext { ProjectFileDirectory = environment.TestDirectory },
new MockFactory()); new MockFactory());
result.Success.Should().BeFalse(); result.Success.Should().BeFalse();
@ -165,7 +232,7 @@ namespace Microsoft.DotNet.Cli.Utils.Tests
var resolver = environment.CreateResolver(); var resolver = environment.CreateResolver();
var result = (MockResult)resolver.Resolve( var result = (MockResult)resolver.Resolve(
new SdkReference("Some.Test.Sdk", null, "99.99.99"), new SdkReference("Some.Test.Sdk", null, "99.99.99"),
new MockContext { ProjectFilePath = environment.TestDirectory.FullName }, new MockContext { ProjectFileDirectory = environment.TestDirectory },
new MockFactory()); new MockFactory());
result.Success.Should().BeTrue(); result.Success.Should().BeTrue();
@ -186,7 +253,7 @@ namespace Microsoft.DotNet.Cli.Utils.Tests
var resolver = environment.CreateResolver(); var resolver = environment.CreateResolver();
var result = (MockResult)resolver.Resolve( var result = (MockResult)resolver.Resolve(
new SdkReference("Some.Test.Sdk", null, "99.99.99"), new SdkReference("Some.Test.Sdk", null, "99.99.99"),
new MockContext { ProjectFilePath = environment.TestDirectory.FullName }, new MockContext { ProjectFileDirectory = environment.TestDirectory },
new MockFactory()); new MockFactory());
result.Success.Should().BeTrue(); result.Success.Should().BeTrue();
@ -196,6 +263,159 @@ namespace Microsoft.DotNet.Cli.Utils.Tests
result.Errors.Should().BeNullOrEmpty(); 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 private enum ProgramFiles
{ {
X64, X64,
@ -210,6 +430,8 @@ namespace Microsoft.DotNet.Cli.Utils.Tests
public string PathEnvironmentVariable { get; set; } public string PathEnvironmentVariable { get; set; }
public DirectoryInfo TestDirectory { get; } public DirectoryInfo TestDirectory { get; }
public FileInfo VSSettingsFile { get; set; }
public bool DisallowPrereleaseByDefault { get; set; }
public TestEnvironment(string identifier = "", [CallerMemberName] string callingMethod = "") public TestEnvironment(string identifier = "", [CallerMemberName] string callingMethod = "")
{ {
@ -223,8 +445,12 @@ namespace Microsoft.DotNet.Cli.Utils.Tests
PathEnvironmentVariable = string.Empty; PathEnvironmentVariable = string.Empty;
} }
public SdkResolver CreateResolver() public SdkResolver CreateResolver(bool useAmbientSettings = false)
=> new DotNetMSBuildSdkResolver(GetEnvironmentVariable); => new DotNetMSBuildSdkResolver(
GetEnvironmentVariable,
useAmbientSettings
? VSSettings.Ambient
: new VSSettings(VSSettingsFile?.FullName, DisallowPrereleaseByDefault));
public DirectoryInfo GetSdkDirectory(ProgramFiles programFiles, string sdkName, string sdkVersion) public DirectoryInfo GetSdkDirectory(ProgramFiles programFiles, string sdkName, string sdkVersion)
=> TestDirectory.GetDirectory( => TestDirectory.GetDirectory(
@ -317,6 +543,34 @@ namespace Microsoft.DotNet.Cli.Utils.Tests
string baseDirectory = AppContext.BaseDirectory; string baseDirectory = AppContext.BaseDirectory;
return Path.Combine(baseDirectory, "minimumVSDefinedSDKVersion"); 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 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 string SolutionFilePath { get => base.SolutionFilePath; set => base.SolutionFilePath = value; }
public new Version MSBuildVersion { get => base.MSBuildVersion; set => base.MSBuildVersion = 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() public MockContext()
{ {
MSBuildVersion = new Version(15, 3, 0); MSBuildVersion = new Version(15, 3, 0);
@ -352,12 +612,7 @@ namespace Microsoft.DotNet.Cli.Utils.Tests
Errors = errors; Errors = errors;
} }
public new bool Success public override bool Success { get; protected set; }
{
get => base.Success;
private set => base.Success = value;
}
public override string Version { get; protected set; } public override string Version { get; protected set; }
public override string Path { get; protected set; } public override string Path { get; protected set; }
public IEnumerable<string> Errors { get; } public IEnumerable<string> Errors { get; }

View file

@ -2,6 +2,14 @@
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net461;$(CliTargetFramework)</TargetFrameworks> <TargetFrameworks>net461;$(CliTargetFramework)</TargetFrameworks>
<TargetFrameworks Condition="'$(OS)' != 'Windows_NT'">$(CliTargetFramework)</TargetFrameworks> <TargetFrameworks Condition="'$(OS)' != 'Windows_NT'">$(CliTargetFramework)</TargetFrameworks>
<!--
https://github.com/dotnet/cli/issues/9851: For now test only on net461 on Windows because
we can't load a different hostfxr.dll into the test than the one that is already loaded
on Core.
-->
<TargetFrameworks Condition="'$(OS)' == 'Windows_NT'">net461</TargetFrameworks>
<RuntimeFrameworkVersion>$(MicrosoftNETCoreAppPackageVersion)</RuntimeFrameworkVersion> <RuntimeFrameworkVersion>$(MicrosoftNETCoreAppPackageVersion)</RuntimeFrameworkVersion>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<AssemblyOriginatorKeyFile>../../tools/Key.snk</AssemblyOriginatorKeyFile> <AssemblyOriginatorKeyFile>../../tools/Key.snk</AssemblyOriginatorKeyFile>
@ -19,9 +27,9 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.0.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
<PackageReference Include="xunit" Version="2.2.0" /> <PackageReference Include="xunit" Version="2.2.0" />
<PackageReference Include="runtime.linux-x64.Microsoft.NETCore.DotNetHostResolver" Version="$(MicrosoftNETCoreDotNetHostResolverPackageVersion)" /> <PackageReference Include="runtime.linux-x64.Microsoft.NETCore.DotNetHostResolver" Version="$(MicrosoftNETCoreDotNetHostResolverPackageVersion_ForMSBuildSdkResolverOnly)" />
<PackageReference Include="runtime.osx-x64.Microsoft.NETCore.DotNetHostResolver" Version="$(MicrosoftNETCoreDotNetHostResolverPackageVersion)" /> <PackageReference Include="runtime.osx-x64.Microsoft.NETCore.DotNetHostResolver" Version="$(MicrosoftNETCoreDotNetHostResolverPackageVersion_ForMSBuildSdkResolverOnly)" />
<PackageReference Include="runtime.linux-musl-x64.Microsoft.NETCore.DotNetHostResolver" Version="$(MicrosoftNETCoreDotNetHostResolverPackageVersion)" /> <PackageReference Include="runtime.linux-musl-x64.Microsoft.NETCore.DotNetHostResolver" Version="$(MicrosoftNETCoreDotNetHostResolverPackageVersion_ForMSBuildSdkResolverOnly)" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -32,7 +40,10 @@
<Reference Include="System.Core" /> <Reference Include="System.Core" />
</ItemGroup> </ItemGroup>
<ItemGroup> <!--
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" /> https://github.com/dotnet/cli/issues/9851: We also can't load a different hostfxr.dll on RHEL
-->
<ItemGroup Condition="$(Rid.StartsWith('rhel'))">
<Compile Remove="GivenAnMSBuildSdkResolver.cs" />
</ItemGroup> </ItemGroup>
</Project> </Project>