diff --git a/build/Compile.targets b/build/Compile.targets index b2ebb2ebc..b44ed8380 100644 --- a/build/Compile.targets +++ b/build/Compile.targets @@ -12,5 +12,7 @@ + + diff --git a/build/DependencyVersions.props b/build/DependencyVersions.props index 64284b4d5..e025ef04a 100644 --- a/build/DependencyVersions.props +++ b/build/DependencyVersions.props @@ -2,7 +2,7 @@ 2.0.0-preview1-002101-00 - 15.2.0-preview-000093-02 + 15.3.0-preview-000111-01 2.0.0-rc4-61325-08 2.0.0-alpha-20170428-1 4.3.0-beta1-2418 diff --git a/src/Microsoft.DotNet.Cli.Utils/Microsoft.DotNet.Cli.Utils.csproj b/src/Microsoft.DotNet.Cli.Utils/Microsoft.DotNet.Cli.Utils.csproj index 5d14b7b57..bc95703fb 100644 --- a/src/Microsoft.DotNet.Cli.Utils/Microsoft.DotNet.Cli.Utils.csproj +++ b/src/Microsoft.DotNet.Cli.Utils/Microsoft.DotNet.Cli.Utils.csproj @@ -20,7 +20,8 @@ - + + @@ -31,4 +32,4 @@ - \ No newline at end of file + diff --git a/src/Microsoft.DotNet.MSBuildSdkResolver/Interop.Common.cs b/src/Microsoft.DotNet.MSBuildSdkResolver/Interop.Common.cs new file mode 100644 index 000000000..580d1220b --- /dev/null +++ b/src/Microsoft.DotNet.MSBuildSdkResolver/Interop.Common.cs @@ -0,0 +1,36 @@ +// 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 new file mode 100644 index 000000000..d1d567e8b --- /dev/null +++ b/src/Microsoft.DotNet.MSBuildSdkResolver/Interop.NETFramework.cs @@ -0,0 +1,45 @@ +// 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 + { + 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 new file mode 100644 index 000000000..bff6fd84b --- /dev/null +++ b/src/Microsoft.DotNet.MSBuildSdkResolver/Interop.NETStandard.cs @@ -0,0 +1,37 @@ +// 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: Currently, only the NET46 build ships (with Visual Studio/desktop msbuild), +// but the netstandard1.3 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. +#if NETSTANDARD1_3 + +using System.Runtime.InteropServices; +using System.Text; + +namespace Microsoft.DotNet.MSBuildSdkResolver +{ + internal static partial class Interop + { + internal static readonly bool s_runningOnWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + + 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 s_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); + } +} + +#endif // NETSTANDARD1_3 \ No newline at end of file diff --git a/src/Microsoft.DotNet.MSBuildSdkResolver/MSBuildSdkResolver.cs b/src/Microsoft.DotNet.MSBuildSdkResolver/MSBuildSdkResolver.cs new file mode 100644 index 000000000..fefbd46d0 --- /dev/null +++ b/src/Microsoft.DotNet.MSBuildSdkResolver/MSBuildSdkResolver.cs @@ -0,0 +1,123 @@ +// 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 Microsoft.Build.Framework; +using System; +using System.Collections.Generic; +using System.IO; + +namespace Microsoft.DotNet.MSBuildSdkResolver +{ + public sealed class DotNetMSBuildSdkResolver : SdkResolver + { + public override string Name => "Microsoft.DotNet.MSBuildSdkResolver"; + + // Default resolver has priority 10000 and we want to go before it and leave room on either side of us. + public override int Priority => 5000; + + 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 = Environment.GetEnvironmentVariable("DOTNET_MSBUILD_SDK_RESOLVER_SDKS_DIR"); + string netcoreSdkVersion = Environment.GetEnvironmentVariable("DOTNET_MSBUILD_SDK_RESOLVER_SDKS_VER"); + + if (msbuildSdksDir == null) + { + string netcoreSdkDir = ResolveNetcoreSdkDirectory(context); + if (netcoreSdkDir == null) + { + return factory.IndicateFailure( + new[] + { + "Unable to locate the .NET Core SDK. Check that it is installed and that the version" + + "specified in global.json (if any) matches the installed version." + }); + } + + msbuildSdksDir = Path.Combine(netcoreSdkDir, "Sdks"); + netcoreSdkVersion = new DirectoryInfo(netcoreSdkDir).Name;; + } + + string msbuildSdkDir = Path.Combine(msbuildSdksDir, sdkReference.Name, "Sdk"); + if (!Directory.Exists(msbuildSdkDir)) + { + return factory.IndicateFailure( + new[] + { + $"{msbuildSdkDir} not found. Check that a recent enough .NET Core SDK is installed" + + " and/or increase the version specified in global.json. " + }); + } + + return factory.IndicateSuccess(msbuildSdkDir, netcoreSdkVersion); + } + + private string ResolveNetcoreSdkDirectory(SdkResolverContext context) + { + foreach (string exeDir in GetDotnetExeDirectoryCandidates()) + { + string workingDir = context.SolutionFilePath ?? context.ProjectFilePath; + string netcoreSdkDir = Interop.hostfxr_resolve_sdk(exeDir, workingDir); + + if (netcoreSdkDir != null) + { + return netcoreSdkDir; + } + } + + return null; + } + + // Search for [ProgramFiles]\dotnet in this order. Only ProgramFiles is defined on + private static readonly string[] s_programFiles = new[] + { + // "c:\Program Files" on x64 machine regardless process bitness, undefined on x86 machines. + "ProgramW6432", + + // "c:\Program Files (x86)" on x64 machine regardless of process bitness, undefined on x64 machines. + "ProgramFiles(x86)", + + // "c:\Program Files" in x64 process, "c:\Program Files (x86)" in x86 process. + // hostfxr will search this on its own if multilevel lookup is not disable, but + // we do it explicitly to prevent an environment with disabled multilevel lookup + // from crippling desktop msbuild and VS. + "ProgramFiles", + }; + + private List GetDotnetExeDirectoryCandidates() + { + string environmentOverride = Environment.GetEnvironmentVariable("DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR"); + if (environmentOverride != null) + { + return new List(1) { environmentOverride }; + } + + // Initial capacity is 2 because while there are 3 candidates, we expect at most 2 unique ones (x64 + x86) + // Also, N=3 here means that we needn't be concerned with the O(N^2) complexity of the foreach + contains. + var candidates = new List(2); + foreach (string variable in s_programFiles) + { + string directory = Environment.GetEnvironmentVariable(variable); + if (directory == null) + { + continue; + } + + directory = Path.Combine(directory, "dotnet"); + if (!candidates.Contains(directory)) + { + candidates.Add(directory); + } + } + + if (candidates.Count == 0) + { + candidates.Add(null); + } + + return candidates; + } + } +} diff --git a/src/Microsoft.DotNet.MSBuildSdkResolver/Microsoft.DotNet.MSBuildSdkResolver.csproj b/src/Microsoft.DotNet.MSBuildSdkResolver/Microsoft.DotNet.MSBuildSdkResolver.csproj new file mode 100644 index 000000000..b6399659d --- /dev/null +++ b/src/Microsoft.DotNet.MSBuildSdkResolver/Microsoft.DotNet.MSBuildSdkResolver.csproj @@ -0,0 +1,38 @@ + + + + + $(SdkVersion) + netstandard1.3;net46 + netstandard1.3 + AnyCPU + win-x86;win-x64 + true + ../../tools/Key.snk + true + true + false + + + + + + + + + + + + + + + x86/hostfxr.dll + PreserveNewest + + + x64/hostfxr.dll + PreserveNewest + + + + \ No newline at end of file diff --git a/src/Microsoft.DotNet.MSBuildSdkResolver/Microsoft.DotNet.MSBuildSdkResolver.sln b/src/Microsoft.DotNet.MSBuildSdkResolver/Microsoft.DotNet.MSBuildSdkResolver.sln new file mode 100644 index 000000000..4d5062122 --- /dev/null +++ b/src/Microsoft.DotNet.MSBuildSdkResolver/Microsoft.DotNet.MSBuildSdkResolver.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26425.1 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.MSBuildSdkResolver", "Microsoft.DotNet.MSBuildSdkResolver.csproj", "{DCB2A518-7BC6-43F5-BE2C-13B11A1F3961}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.MSBuildSdkResolver.Tests", "..\..\test\Microsoft.DotNet.MSBuildSdkResolver.Tests\Microsoft.DotNet.MSBuildSdkResolver.Tests.csproj", "{CC488F39-E106-4BF4-9599-19A265AFD9AC}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {DCB2A518-7BC6-43F5-BE2C-13B11A1F3961}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DCB2A518-7BC6-43F5-BE2C-13B11A1F3961}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DCB2A518-7BC6-43F5-BE2C-13B11A1F3961}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DCB2A518-7BC6-43F5-BE2C-13B11A1F3961}.Release|Any CPU.Build.0 = Release|Any CPU + {CC488F39-E106-4BF4-9599-19A265AFD9AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CC488F39-E106-4BF4-9599-19A265AFD9AC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CC488F39-E106-4BF4-9599-19A265AFD9AC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CC488F39-E106-4BF4-9599-19A265AFD9AC}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/test/Microsoft.DotNet.Cli.Tests.sln b/test/Microsoft.DotNet.Cli.Tests.sln index 30e4e2f60..ffc08cbd5 100644 --- a/test/Microsoft.DotNet.Cli.Tests.sln +++ b/test/Microsoft.DotNet.Cli.Tests.sln @@ -76,6 +76,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-store.Tests", "dotne EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-back-compat.Tests", "dotnet-back-compat.Tests\dotnet-back-compat.Tests.csproj", "{27351B2F-325B-4843-9F4C-BC53FD06A7B5}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.MSBuildSdkResolver.Tests", "Microsoft.DotNet.MSBuildSdkResolver.Tests\Microsoft.DotNet.MSBuildSdkResolver.Tests.csproj", "{42A0CAB4-FFAD-47D4-9880-C0F4EDCF93DE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -506,6 +508,18 @@ Global {27351B2F-325B-4843-9F4C-BC53FD06A7B5}.Release|x64.Build.0 = Release|Any CPU {27351B2F-325B-4843-9F4C-BC53FD06A7B5}.Release|x86.ActiveCfg = Release|Any CPU {27351B2F-325B-4843-9F4C-BC53FD06A7B5}.Release|x86.Build.0 = Release|Any CPU + {42A0CAB4-FFAD-47D4-9880-C0F4EDCF93DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {42A0CAB4-FFAD-47D4-9880-C0F4EDCF93DE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {42A0CAB4-FFAD-47D4-9880-C0F4EDCF93DE}.Debug|x64.ActiveCfg = Debug|Any CPU + {42A0CAB4-FFAD-47D4-9880-C0F4EDCF93DE}.Debug|x64.Build.0 = Debug|Any CPU + {42A0CAB4-FFAD-47D4-9880-C0F4EDCF93DE}.Debug|x86.ActiveCfg = Debug|Any CPU + {42A0CAB4-FFAD-47D4-9880-C0F4EDCF93DE}.Debug|x86.Build.0 = Debug|Any CPU + {42A0CAB4-FFAD-47D4-9880-C0F4EDCF93DE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {42A0CAB4-FFAD-47D4-9880-C0F4EDCF93DE}.Release|Any CPU.Build.0 = Release|Any CPU + {42A0CAB4-FFAD-47D4-9880-C0F4EDCF93DE}.Release|x64.ActiveCfg = Release|Any CPU + {42A0CAB4-FFAD-47D4-9880-C0F4EDCF93DE}.Release|x64.Build.0 = Release|Any CPU + {42A0CAB4-FFAD-47D4-9880-C0F4EDCF93DE}.Release|x86.ActiveCfg = Release|Any CPU + {42A0CAB4-FFAD-47D4-9880-C0F4EDCF93DE}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/test/Microsoft.DotNet.MSBuildSdkResolver.Tests/GivenAnMSBuildSdkResolver.cs b/test/Microsoft.DotNet.MSBuildSdkResolver.Tests/GivenAnMSBuildSdkResolver.cs new file mode 100644 index 000000000..bfbf14e4a --- /dev/null +++ b/test/Microsoft.DotNet.MSBuildSdkResolver.Tests/GivenAnMSBuildSdkResolver.cs @@ -0,0 +1,70 @@ +// 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.Collections.Generic; +using Microsoft.Build.Framework; +using Xunit; +using System.Linq; +using Xunit.Abstractions; +using System; +using Microsoft.DotNet.MSBuildSdkResolver; + +namespace Microsoft.DotNet.Cli.Utils.Tests +{ + public class GivenAnMSBuildSdkResolver + { + private ITestOutputHelper _logger; + + public GivenAnMSBuildSdkResolver(ITestOutputHelper logger) + { + _logger = logger; + } + + [Fact] + public void ItHasCorrectNameAndPriority() + { + var resolver = new DotNetMSBuildSdkResolver(); + + Assert.Equal(5000, resolver.Priority); + Assert.Equal("Microsoft.DotNet.MSBuildSdkResolver", resolver.Name); + } + + [Fact] + public void ItCallsNativeCodeWithoutCrashing() // WIP: placeholder to get plumbing through + { + var resolver = new DotNetMSBuildSdkResolver(); + var result = (MockResult)resolver.Resolve( + new SdkReference("Microsoft.NET.Sdk", null, null), + new MockContext(), + new MockFactory()); + + _logger.WriteLine($"success: {result.Success}"); + _logger.WriteLine($"errors: {string.Join(Environment.NewLine, result.Errors ?? Array.Empty())}"); + _logger.WriteLine($"warnings: {string.Join(Environment.NewLine, result.Warnings ?? Array.Empty())}"); + _logger.WriteLine($"path: {result.Path}"); + _logger.WriteLine($"version: {result.Version}"); + } + + private sealed class MockContext : SdkResolverContext + { + } + + private sealed class MockFactory : SdkResultFactory + { + public override SdkResult IndicateFailure(IEnumerable errors, IEnumerable warnings = null) + => new MockResult { Success = false, Errors = errors, Warnings = warnings }; + + public override SdkResult IndicateSuccess(string path, string version, IEnumerable warnings = null) + => new MockResult { Success = true, Path = path, Version = version, Warnings = warnings }; + } + + private sealed class MockResult : SdkResult + { + public new bool Success { get => base.Success; set => base.Success = value; } + public string Version { get; set; } + public string Path { get; set; } + public IEnumerable Errors { get; set; } + public IEnumerable Warnings { get; set; } + } + } +} diff --git a/test/Microsoft.DotNet.MSBuildSdkResolver.Tests/Microsoft.DotNet.MSBuildSdkResolver.Tests.csproj b/test/Microsoft.DotNet.MSBuildSdkResolver.Tests/Microsoft.DotNet.MSBuildSdkResolver.Tests.csproj new file mode 100644 index 000000000..e302505e4 --- /dev/null +++ b/test/Microsoft.DotNet.MSBuildSdkResolver.Tests/Microsoft.DotNet.MSBuildSdkResolver.Tests.csproj @@ -0,0 +1,29 @@ + + + + + net46;$(CliTargetFramework) + $(CliTargetFramework) + $(CLI_SharedFrameworkVersion) + Exe + ../../tools/Key.snk + true + true + + + + + + + + + + + + + + + + + + diff --git a/test/Microsoft.DotNet.MSBuildSdkResolver.Tests/xunit.runner.json b/test/Microsoft.DotNet.MSBuildSdkResolver.Tests/xunit.runner.json new file mode 100644 index 000000000..34b2fe2cd --- /dev/null +++ b/test/Microsoft.DotNet.MSBuildSdkResolver.Tests/xunit.runner.json @@ -0,0 +1,3 @@ +{ + "shadowCopy": false +} \ No newline at end of file