diff --git a/src/Resolvers/Microsoft.DotNet.MSBuildSdkResolver/MSBuildSdkResolver.cs b/src/Resolvers/Microsoft.DotNet.MSBuildSdkResolver/MSBuildSdkResolver.cs index a4a7b555b3bf..1f9535fc7fce 100644 --- a/src/Resolvers/Microsoft.DotNet.MSBuildSdkResolver/MSBuildSdkResolver.cs +++ b/src/Resolvers/Microsoft.DotNet.MSBuildSdkResolver/MSBuildSdkResolver.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Reflection; +using System.Text.Json; using Microsoft.Build.Framework; using Microsoft.DotNet.Cli; using Microsoft.DotNet.Configurer; @@ -27,23 +28,28 @@ public sealed class DotNetMSBuildSdkResolver : SdkResolver private readonly Func _getEnvironmentVariable; private readonly Func? _getCurrentProcessPath; + private readonly Func _getMsbuildRuntime; private readonly NETCoreSdkResolver _netCoreSdkResolver; + private const string DotnetHost = "DOTNET_HOST_PATH"; + private const string MSBuildTaskHostRuntimeVersion = "SdkResolverMSBuildTaskHostRuntimeVersion"; + private static CachingWorkloadResolver _staticWorkloadResolver = new(); private bool _shouldLog = false; public DotNetMSBuildSdkResolver() - : this(Environment.GetEnvironmentVariable, null, VSSettings.Ambient) + : this(Environment.GetEnvironmentVariable, null, GetMSbuildRuntimeVersion, VSSettings.Ambient) { } // Test constructor - public DotNetMSBuildSdkResolver(Func getEnvironmentVariable, Func? getCurrentProcessPath, VSSettings vsSettings) + public DotNetMSBuildSdkResolver(Func getEnvironmentVariable, Func? getCurrentProcessPath, Func getMsbuildRuntime, VSSettings vsSettings) { _getEnvironmentVariable = getEnvironmentVariable; _getCurrentProcessPath = getCurrentProcessPath; _netCoreSdkResolver = new NETCoreSdkResolver(getEnvironmentVariable, vsSettings); + _getMsbuildRuntime = getMsbuildRuntime; if (_getEnvironmentVariable(EnvironmentVariableNames.DOTNET_MSBUILD_SDK_RESOLVER_ENABLE_LOG) is string val && (string.Equals(val, "true", StringComparison.OrdinalIgnoreCase) || @@ -189,6 +195,32 @@ private sealed class CachedState minimumVSDefinedSDKVersion); } + string? dotnetExe = dotnetRoot != null ? + Path.Combine(dotnetRoot, Constants.DotNetExe) : + null; + if (File.Exists(dotnetExe)) + { + propertiesToAdd ??= new Dictionary(); + propertiesToAdd.Add(DotnetHost, dotnetExe); + } + else + { + logger?.LogMessage($"Could not set '{DotnetHost}' because dotnet executable '{dotnetExe}' does not exist."); + } + + string? runtimeVersion = dotnetRoot != null ? + _getMsbuildRuntime(resolverResult.ResolvedSdkDirectory, dotnetRoot) : + null; + if (!string.IsNullOrEmpty(runtimeVersion)) + { + propertiesToAdd ??= new Dictionary(); + propertiesToAdd.Add(MSBuildTaskHostRuntimeVersion, runtimeVersion); + } + else + { + logger?.LogMessage($"Could not set '{MSBuildTaskHostRuntimeVersion}' because runtime version could not be determined."); + } + if (resolverResult.FailedToResolveSDKSpecifiedInGlobalJson) { logger?.LogMessage($"Could not resolve SDK specified in '{resolverResult.GlobalJsonPath}'. Ignoring global.json for this resolution."); @@ -207,10 +239,7 @@ private sealed class CachedState warnings.Add(Strings.GlobalJsonResolutionFailed); } - if (propertiesToAdd == null) - { - propertiesToAdd = new Dictionary(); - } + propertiesToAdd ??= new Dictionary(); propertiesToAdd.Add("SdkResolverHonoredGlobalJson", "false"); propertiesToAdd.Add("SdkResolverGlobalJsonPath", resolverResult.GlobalJsonPath); @@ -254,6 +283,28 @@ private sealed class CachedState return factory.IndicateSuccess(msbuildSdkDir, netcoreSdkVersion, propertiesToAdd, itemsToAdd, warnings); } + private static string? GetMSbuildRuntimeVersion(string sdkDirectory, string dotnetRoot) + { + // 1. Get the runtime version from the MSBuild.runtimeconfig.json file + string runtimeConfigPath = Path.Combine(sdkDirectory, "MSBuild.runtimeconfig.json"); + if (!File.Exists(runtimeConfigPath)) return null; + + using var stream = File.OpenRead(runtimeConfigPath); + using var jsonDoc = JsonDocument.Parse(stream); + + JsonElement root = jsonDoc.RootElement; + if (!root.TryGetProperty("runtimeOptions", out JsonElement runtimeOptions) || + !runtimeOptions.TryGetProperty("framework", out JsonElement framework)) return null; + + string? runtimeName = framework.GetProperty("name").GetString(); + string? runtimeVersion = framework.GetProperty("version").GetString(); + + // 2. Check that the runtime version is installed (in shared folder) + return (!string.IsNullOrEmpty(runtimeName) && !string.IsNullOrEmpty(runtimeVersion) && + Directory.Exists(Path.Combine(dotnetRoot, "shared", runtimeName, runtimeVersion))) + ? runtimeVersion : null; + } + private static SdkResult Failure(SdkResultFactory factory, ResolverLogger? logger, SdkLogger sdkLogger, string format, params object?[] args) { string error = string.Format(format, args); diff --git a/src/Resolvers/Microsoft.DotNet.NativeWrapper/Constants.cs b/src/Resolvers/Microsoft.DotNet.NativeWrapper/Constants.cs index 29d0dbcd3daa..998663b58204 100644 --- a/src/Resolvers/Microsoft.DotNet.NativeWrapper/Constants.cs +++ b/src/Resolvers/Microsoft.DotNet.NativeWrapper/Constants.cs @@ -7,6 +7,7 @@ internal static class Constants { public const string HostFxr = "hostfxr"; public const string DotNet = "dotnet"; + public const string DotNetExe = "dotnet.exe"; public const string PATH = "PATH"; public const string DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR = "DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR"; diff --git a/test/Microsoft.DotNet.MSBuildSdkResolver.Tests/GivenAnMSBuildSdkResolver.cs b/test/Microsoft.DotNet.MSBuildSdkResolver.Tests/GivenAnMSBuildSdkResolver.cs index 7c29127040f7..ba6bee057b4d 100644 --- a/test/Microsoft.DotNet.MSBuildSdkResolver.Tests/GivenAnMSBuildSdkResolver.cs +++ b/test/Microsoft.DotNet.MSBuildSdkResolver.Tests/GivenAnMSBuildSdkResolver.cs @@ -13,6 +13,8 @@ namespace Microsoft.DotNet.Cli.Utils.Tests { public class GivenAnMSBuildSdkResolver : SdkTest { + private const string DotnetHost = "DOTNET_HOST_PATH"; + private const string MSBuildTaskHostRuntimeVersion = "SdkResolverMSBuildTaskHostRuntimeVersion"; public GivenAnMSBuildSdkResolver(ITestOutputHelper logger) : base(logger) { @@ -200,7 +202,18 @@ public void ItReturnsHighestSdkAvailableThatIsCompatibleWithMSBuild(bool disallo result.Success.Should().BeTrue($"No error expected. Error encountered: {string.Join(Environment.NewLine, result.Errors ?? new string[] { })}. Mocked Process Path: {environment.ProcessPath}. Mocked Path: {environment.PathEnvironmentVariable}"); result.Path.Should().Be((disallowPreviews ? compatibleRtm : compatiblePreview).FullName); result.AdditionalPaths.Should().BeNull(); - result.PropertiesToAdd.Should().BeNull(); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // DotnetHost is the path to dotnet.exe. Can be only on Windows. + result.PropertiesToAdd.Count.Should().Be(2); + result.PropertiesToAdd.Should().ContainKey(DotnetHost); + } + else + { + result.PropertiesToAdd.Count.Should().Be(1); + } + result.PropertiesToAdd.Should().ContainKey(MSBuildTaskHostRuntimeVersion); + result.PropertiesToAdd[MSBuildTaskHostRuntimeVersion].Should().Be("mockRuntimeVersion"); result.Version.Should().Be(disallowPreviews ? "98.98.98" : "99.99.99-preview"); result.Warnings.Should().BeNullOrEmpty(); result.Errors.Should().BeNullOrEmpty(); @@ -274,9 +287,20 @@ public void ItReturnsHighestSdkAvailableThatIsCompatibleWithMSBuildWhenVersionIn result.Success.Should().BeTrue($"No error expected. Error encountered: {string.Join(Environment.NewLine, result.Errors ?? new string[] { })}. Mocked Process Path: {environment.ProcessPath}. Mocked Path: {environment.PathEnvironmentVariable}"); result.Path.Should().Be((disallowPreviews ? compatibleRtm : compatiblePreview).FullName); result.AdditionalPaths.Should().BeNull(); - result.PropertiesToAdd.Count.Should().Be(2); - result.PropertiesToAdd.ContainsKey("SdkResolverHonoredGlobalJson"); - result.PropertiesToAdd.ContainsKey("SdkResolverGlobalJsonPath"); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // DotnetHost is the path to dotnet.exe. Can be only on Windows. + result.PropertiesToAdd.Count.Should().Be(4); + result.PropertiesToAdd.Should().ContainKey(DotnetHost); + } + else + { + result.PropertiesToAdd.Count.Should().Be(3); + } + result.PropertiesToAdd.Should().ContainKey(MSBuildTaskHostRuntimeVersion); + result.PropertiesToAdd[MSBuildTaskHostRuntimeVersion].Should().Be("mockRuntimeVersion"); + result.PropertiesToAdd.Should().ContainKey("SdkResolverHonoredGlobalJson"); + result.PropertiesToAdd.Should().ContainKey("SdkResolverGlobalJsonPath"); result.PropertiesToAdd["SdkResolverHonoredGlobalJson"].Should().Be("false"); result.Version.Should().Be(disallowPreviews ? "98.98.98" : "99.99.99-preview"); result.Warnings.Should().BeEquivalentTo(new[] { "Unable to locate the .NET SDK version '1.2.3' as specified by global.json, please check that the specified version is installed." }); @@ -584,6 +608,7 @@ public SdkResolver CreateResolver(bool useAmbientSettings = false) GetEnvironmentVariable, // force current executable location to be the mocked dotnet executable location () => ProcessPath, + (x, y) => "mockRuntimeVersion", useAmbientSettings ? VSSettings.Ambient : new VSSettings(VSSettingsFile?.FullName, DisallowPrereleaseByDefault));